Extending the engine to a new language#
The two additive Python providers are the reference for adding a language. The released
python-import-surface shows how to add a deliberately bounded provider without changing prior
behavior. The released python-module-graph shows how to add a structured parser, explicit
resolution inputs, and fail-closed uncertainty. This checklist is grounded in their code, not
aspiration.
The seam#
An extractor is one class implementing RepositoryFactsExtractor
(src/graph.ts):
interface RepositoryFactsExtractor {
readonly kind: 'ast' | 'line-scan';
extract(repoDir: string, revision: string): ArchitectureGraph;
}
It consumes a ResolvedExtraction config (src/extractors.ts —
resolveExtraction merges the blueprint's extraction block with its constraints so the
constraint list is the single source of truth for what to scan) and emits an
ArchitectureGraph: components + edges + a coverage envelope. Determinism is a hard
contract: no wall-clock, every array sorted before return — same tree + same blueprint ⇒
byte-identical graph (the determinism tests are the oracle).
The checklist#
- Profile — add your
<lang>-…-surfacevalue toExtractionProfileSchema(src/schema.ts) with a doc line stating which constraint types the profile supports and which it refuses. Widen-only: never touch the existing values. Regenerate the published schemas (npm run generate-schemas/ scripts/generate-schemas.ts) — the schema-parity test fails otherwise. - Provider — a new
src/<lang>-extractor.ts. Decide honestly what you can extract. The legacy Python import-surface is line-oriented and preserves its released contract; the structured module graph uses a pinned grammar and records dynamic loading as uncertainty rather than pretending it has a target. Every fidelity limit goes incoverage.unsupported, and every documented miss gets a test asserting it is NOT detected (a capability note that cannot fail is a bug). - Registry row — append to
EXTRACTOR_PROVIDERS(src/extractor-registry.ts). A single-provider language registers the same provider for both kind flags and declareskindNote. An enum value without a registry row throws at dispatch — LOUD, never a silent empty scan. - Refusals — any constraint type your provider cannot observe must be REFUSED at the
gate and CLI (see the
python-import-surface+forbiddenEgressrefusals in src/gate.ts / src/cli.ts), never silently scored as a pass on a surface that cannot see the violation. - Free constraint classes —
forbiddenFile(overcoverage.scannedFiles) andforbiddenPattern(overcoverage.patternScan) need no language AST: populate both envelopes (the sharedscanPatterns/toRelSortedhelpers) and those constraints work immediately. - Fixture pair — a conformant and a seeded-drift tree under
fixtures/<lang>-surface/, one blueprint, opposite verdicts by real exit codes through the BUILT CLI, wired as a CI leg (.github/workflows/ci.yml). A gate that cannot go red is not a gate. - Corpus defects — seed real defects into
SEEDED_CORPUS(src/corpus.ts) + corpus/MANIFEST.json (the drift gate keeps them in sync), add the conformant tree to the clean control set, and let the measured-recall CI leg cover your language. The recall floor only rises. - Verdict stability — prove the registry change left the existing profiles
byte-identical (tests/extractor-registry.test.ts)
and sync the self-blueprint (
.blueprints/engine.blueprint.json) for your new source files — the self-gate SYNC tests will tell you exactly what is missing.
What "done" means#
A language provider is done when: the RED/GREEN pair runs in CI, its corpus defects are caught in the measured-recall leg, its honesty tests pin the documented misses, and the self-gate is green. Not before.