<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">ENGINE — conform: the cross-language conformance engine, v0.1</title>
  <status stage="spec" state="done"/>
  <p p="1"><fact id="status-line" status="impl/done">**Status.** Design, beta.</fact></p>
  <p p="2"><fact id="IMPLEMENTS-T3-OWN-ENGINE-BORROWED-FRONTENDS" status="impl/done">Implements the Charter's T3 decision: **our own engine, borrowed frontends.**</fact></p>
  <p p="3"><fact id="rationale-compiler-is-a-page-reimplementation-is-weeks" status="spec/done">Rationale (owner-decided): asking a compiler about its own language is one page of code; rebuilding name resolution over tree-sitter is weeks.</fact></p>
  <p p="4"><fact id="BALANCE-IS-EXPLICIT-THROUGH-ESCALATION-TIERS" status="spec/done">The balance is made explicit through escalation tiers, not ad-hoc judgment. *Specified, not built: the whole tier vocabulary of §1 is unimplemented. There is no `Tier` type, no `tier` field and no escalation path in `core-ai-native-conform` or in any frontend crate, stack CLI or host driver; `T-lex` / `T-sem` appear nowhere in the tree and `T-syn` only inside three doc comments that quote this document. What is actually explicit is the **frontend** choice, made by the caller at the call site — which is a different mechanism, and an ad-hoc one.*</fact></p>
  <p p="5"><fact id="derives-from-lead" status="impl/done">**Derives from.**</fact></p>
  <list ordered="false" p="6">
    <item><fact id="DERIVES-A1-EXPLANATION-CHAINS" status="impl/done">Charter A1 (findings carry explanation chains),</fact></item>
    <item><fact id="DERIVES-A2-CACHED-INCREMENTAL" status="impl/done">A2 (extraction is cached, incremental),</fact></item>
    <item><fact id="DERIVES-A3-BELOW-THE-ALGORITHMIC-FLOOR" status="impl/done">A3 (conformance is below the algorithmic floor — no LLM in the checking path),</fact></item>
    <item><fact id="DERIVES-A5-RULES-SHIP-WITH-CHECKERS" status="impl/done">A5 (rules ship with checkers).</fact></item>
  </list>
  <section id="tiers" title="1. Escalation tiers">
    <p p="7"><fact id="RULE-RECORD-DECLARES-ITS-TIER" status="spec/done">Every rule record declares `tier` — the minimum analysis depth its check requires. *Specified, not built: no rule record declares a tier, because a rule record has no such field. The shipped contract is `pub trait Rule { fn id(&amp;self) -&gt; &amp;'static str; fn why(&amp;self) -&gt; &amp;'static str; fn check(&amp;self, facts: &amp;[SourceFacts]) -&gt; Vec&lt;Finding&gt;; }` (`conform/src/finding.rs:53-57`) — three methods, none of them `tier`. All fifteen shipped rules take whatever facts they are handed.*</fact></p>
    <p p="8"><fact id="ENGINE-RUNS-THE-CHEAPEST-ADEQUATE-FRONTEND" status="spec/done">The engine runs the cheapest adequate frontend; escalation is declared, never improvised. *Specified, not built: the engine performs no selection, so it can neither pick the cheapest nor escalate. `store.rs:95-118` exposes one entry point per language, each taking a caller-supplied `&amp;dyn Frontend` — the choice is made outside the engine, by whoever calls it, which is precisely the improvisation this sentence rules out.*</fact></p>
    <table p="9">
      <tr>
        <td>Tier</td>
        <td>Capability</td>
        <td>Backend</td>
        <td>Cost</td>
      </tr>
      <tr>
        <td><fact id="ROW-TIER-T-LEX" status="spec/done">**T-lex**</fact></td>
        <td><fact id="ROW-TIER-T-LEX-CAPABILITY" status="spec/done">textual invariants (forbidden tokens, file layout, license headers)</fact></td>
        <td><fact id="ROW-TIER-T-LEX-BACKEND" status="spec/done">ripgrep-class scan (MIT/Unlicense) — *Specified, not built: this tier has no implementation and no backend. No ripgrep-class scanner is wired into the engine, and no forbidden-token, file-layout or license-header rule exists in the roster (`rules/mod.rs:21-25`).*</fact></td>
        <td><fact id="ROW-TIER-T-LEX-COST" status="spec/done">~free</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-TIER-T-SYN" status="spec/done">**T-syn**</fact></td>
        <td><fact id="ROW-TIER-T-SYN-CAPABILITY" status="spec/done">structure: items, attributes, imports, spans, hashes @status:impl/done</fact></td>
        <td><fact id="ROW-TIER-T-SYN-BACKEND" status="spec/done">tree-sitter (MIT) universal; `syn` (MIT/Apache-2.0) for Rust precision — *Half built: the `syn` half is real and running (`rust-ai-native-conform-frontend`, whose own module doc calls itself "the Rust T-syn frontend"). The **universal tree-sitter backend does not exist** — `tree-sitter` / `tree_sitter` return no hit in any crate or manifest in the repository, so there is no universal path and each language got a bespoke frontend instead. The capability column is accurate; the tier that would name it is not.*</fact></td>
        <td><fact id="ROW-TIER-T-SYN-COST" status="spec/done">cheap, incremental @status:impl/done</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-TIER-T-SEM" status="spec/done">**T-sem**</fact></td>
        <td><fact id="ROW-TIER-T-SEM-CAPABILITY" status="spec/done">types, name resolution, macro/template expansion, real import graphs</fact></td>
        <td><fact id="ROW-TIER-T-SEM-BACKEND" status="spec/done">per-language compiler frontend (§2) — *Partly built, but not as a tier: one real compiler frontend ships. `typescript-ai-native-conform-frontend` reaches the TypeScript Compiler API through the packaged `tools/ts-extract` sidecar, and the Go stack drives `gopls` as an LSP oracle in `go-ai-native-tcg`. Neither is reachable as a **tier**: both are plain `Frontend` implementations a caller names directly, there is nothing to escalate from, and no rule declares it needs them.*</fact></td>
        <td><fact id="ROW-TIER-T-SEM-COST" status="spec/done">expensive; cached hard</fact></td>
      </tr>
    </table>
    <p p="10"><fact id="rule-examples-lead" status="impl/done">Rule examples:</fact></p>
    <list ordered="false" p="11">
      <item><fact id="EXAMPLE-R-021-FORBIDDEN-IDIOM" status="impl/plan">R-021 forbidden-idiom scan → T-lex/T-syn; *Specified, not built (→ B-038): R-021 is not a rule in this engine. `R-021` returns zero hits across every crate in the repository, and no forbidden-idiom scan of any kind ships. It is cited as a ban in the language guides and authored nowhere.*</fact></item>
      <item><fact id="EXAMPLE-R-002-IMPORT-GRAPH-ISOLATION" status="spec/done">R-002 import-graph isolation → T-syn (Rust) / T-sem (C++ where headers lie); *The rule is real; the mapping is not. `CellIsolation` carries id `"R-002"` (`conform/src/rules/structure.rs:77-91`) and is projected per language as `TsCellIsolation` and `GoCellIsolation`. What does not exist is either side of the arrow — no tier assigns it a depth, and there is no C++ frontend to escalate to.*</fact></item>
      <item><fact id="EXAMPLE-R-020-NAMING-VS-MANIFEST" status="impl/plan">R-020 naming-vs-manifest → T-syn + specmap index; *Specified, not built (→ B-038): neither the rule nor the join. `R-020` returns zero hits across every crate, and the conform engine does not depend on the specmap crate at all — no manifest lists it — so a rule combining structural facts with the specmap index cannot be written today without a new dependency edge.*</fact></item>
      <item><fact id="EXAMPLE-TYPE-FLOW-RULES" status="spec/done">type-flow rules (future) → T-sem.</fact></item>
    </list>
  </section>
  <section id="frontends" title="2. Frontends — borrowed, behind one trait">
    <fence lang="rust" p="12">trait Frontend {
    fn lang(&amp;self) -&gt; Lang;
    fn tier(&amp;self) -&gt; Tier;
    fn extract(&amp;self, files: &amp;[SourceFile]) -&gt; Result&lt;Vec&lt;Fact&gt;, FrontendError&gt;;
}</fence>
    <table p="13">
      <tr>
        <td>Lang</td>
        <td>T-syn</td>
        <td>T-sem</td>
        <td>License posture</td>
      </tr>
      <tr>
        <td><fact id="ROW-FRONTEND-RUST" status="impl/done">Rust</fact></td>
        <td><fact id="ROW-FRONTEND-RUST-T-SYN" status="impl/done">`syn` in-process</fact></td>
        <td><fact id="ROW-FRONTEND-RUST-T-SEM" status="impl/done">rust-analyzer crates or `rustc_driver` (nightly caveat) — *Specified, not built: the T-sem column names software that is absent. `rust-analyzer`, `rustc_driver`, `ra_ap` and `hir` return no hit in the engine, in `rust-ai-native-conform-frontend`, or in any manifest. The T-syn column is exact and running.* @status:spec/done</fact></td>
        <td><fact id="ROW-FRONTEND-RUST-LICENSE-POSTURE" status="impl/done">MIT/Apache-2.0 — clean</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-FRONTEND-CPP" status="spec/done">C++ — *Specified, not built, in full: there is no C++ frontend at either depth. `tree-sitter`, `tree-sitter-cpp`, `libclang` and `clang-sys` return no hit anywhere in the repository, no C++ crate exists in any workspace, and the `Fact` model (`conform/src/facts.rs:25`) carries no C++ variant. This row is a design intention; the three language stacks that ship are Rust, TypeScript and Go.*</fact></td>
        <td><fact id="ROW-FRONTEND-CPP-T-SYN" status="spec/done">tree-sitter-cpp</fact></td>
        <td><fact id="ROW-FRONTEND-CPP-T-SEM" status="spec/done">**libclang** via `clang-sys` — the one-page-AST path</fact></td>
        <td><fact id="ROW-FRONTEND-CPP-LICENSE-POSTURE" status="spec/done">Apache-2.0 w/ LLVM exception — clean</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-FRONTEND-TS-JS" status="impl/done">TS/JS</fact></td>
        <td><fact id="ROW-FRONTEND-TS-JS-T-SYN" status="impl/done">tree-sitter / SWC (Apache-2.0) — *Specified, not built, and ruled a non-build: neither tree-sitter nor SWC returns a hit anywhere in the repository, and the B-023 study (2026-08-03/04) recommends never building one — the shipped sidecar already parses at this depth (next cell), so a second syntactic parser would be a duplicate in any outcome.* @status:spec/done</fact></td>
        <td><fact id="ROW-FRONTEND-TS-JS-T-SEM" status="impl/done">TypeScript compiler API via a Node **sidecar process** — *Built, at parser depth: `tools/ts-extract`'s `extract.ts` drives the Compiler API's parser surface only — `createSourceFile`, `forEachChild`, `createScanner`, the JSDoc readers — and never constructs a `Program` or calls `getTypeChecker`, so no type-level fact is extracted and the cell's implied T-sem is in substance T-syn (B-023 evidence + the owner's counter-probe). The checker deepening of this same sidecar is deferred by the owner's ruling, 2026-08-04, verbatim: «давай B-023 отложим до тех пор, пока не появится ещё какое-то правило кроме "as_cross с не локальной областью". Не нужно забывать об этом, это нормальное продолжение развития, просто это кандидат на середину или конец бэклога» — it waits for a second type-requiring rule.* @status:spec/done</fact></td>
        <td><fact id="ROW-FRONTEND-TS-JS-LICENSE-POSTURE" status="impl/done">Apache-2.0 — clean</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-FRONTEND-GO" status="impl/done">Go</fact></td>
        <td><fact id="ROW-FRONTEND-GO-T-SYN" status="impl/done">`go/parser`+`go/ast` via a stdlib-only **`go run` sidecar** (go-extract)</fact></td>
        <td><fact id="ROW-FRONTEND-GO-T-SEM" status="impl/done">gopls / `go vet` as evidence providers — *Built, but at another layer, and not as evidence providers. Both tools run: `go vet ./...` is step 2 of the Go floor (`go-ai-native-cli/src/floor.rs:115-120`) and `gopls` is driven as a long-lived LSP oracle by `go-ai-native-tcg`. Neither reaches conform — conform ingests no output from either, so nothing they know becomes a fact. Read this cell as naming where semantic depth lives in the Go stack, not as a conform frontend.* @status:spec/done</fact></td>
        <td><fact id="ROW-FRONTEND-GO-LICENSE-POSTURE" status="impl/done">BSD-3 — clean</fact></td>
      </tr>
      <tr>
        <td><fact id="ROW-FRONTEND-PYTHON" status="spec/done">Python — *Specified, not built, in full: there is no Python frontend at either depth. `rustpython`, `symtable`, `cpython` and any Python sidecar return no hit in the engine, its manifests or any frontend crate, and the `Fact` model carries no Python variant. The B-023 study (2026-08-03/04) finds both columns feasible on the ready sidecar precedent (go-extract / ts-extract) and prefers the CPython sidecar over an in-process RustPython parser — but the frontend has no consumer: no `python-ai-native-lang` package exists, so building it now would be dead engine code. It waits on a product decision about a Python stack (B-023's disposition, owner-ruled 2026-08-04).*</fact></td>
        <td><fact id="ROW-FRONTEND-PYTHON-T-SYN" status="spec/done">RustPython parser (MIT) in-process</fact></td>
        <td><fact id="ROW-FRONTEND-PYTHON-T-SEM" status="spec/done">CPython `ast`/`symtable` via sidecar</fact></td>
        <td><fact id="ROW-FRONTEND-PYTHON-LICENSE-POSTURE" status="spec/done">PSF / MIT — clean</fact></td>
      </tr>
    </table>
    <p p="14"><fact id="TIER-VOCABULARY" status="spec/done">**The tier vocabulary (document taxonomy).** T-lex / T-syn / T-sem name the depth an enforcement mechanism reads at: **T-lex** — lexical (tokens, pragmas, build tags; no parse), **T-syn** — syntactic (parser-grade extraction), **T-sem** — semantic (compiler/type-grade). The taxonomy classifies rules and frontends in prose and tables; it is deliberately not (yet) a code-level type — the rule registry's tier column is a later-phase mechanism.</fact></p>
    <p p="15"><fact id="SIDECAR-PROTOCOL-IS-NDJSON-OVER-STDIO" status="impl/done">Sidecar protocol: newline-delimited JSON over stdio, versioned; sidecars emit Facts, nothing else.</fact></p>
    <p p="16"><fact id="FRONTEND-CRASH-DEGRADES-VISIBLY-NEVER-SILENTLY" status="spec/done">A frontend whose toolchain is broken is a **hard error**: each stack's driver probes its frontend before extraction and fails the run (`typescript-ai-native-conform/src/lib.rs:66-70`), so the gate can never report green over zero facts. A per-file extraction failure surfaces on stderr and yields an empty fact set for that file. *Specified, not built: there is no `skipped (frontend unavailable)` report status — `Finding` carries no status field.*</fact></p>
    <p p="17"><fact id="FOREIGN-LINTERS-ARE-EVIDENCE-PROVIDERS" status="impl/done">**Foreign linters as evidence providers.** clippy, eslint, ruff, clang-tidy run as-is; their output is ingested as facts via **SARIF** (the OASIS static-analysis interchange format).</fact></p>
    <p p="18"><fact id="LINTERS-ARE-CITED-NOT-REIMPLEMENTED" status="spec/done">We neither reimplement their checks nor fork them — we *cite* them: a Discipline rule may be `check: { tool: "clippy", id: "...", status: enforced }`, and conform's job is orchestration + the checks no generic linter can know (manifest-vs-name, specmap coverage, cell isolation). *The posture holds; the record shape and two of the three examples do not. Foreign linters really are run as-is and never reforked — the floor shells out to `cargo clippy`, `go vet`, `staticcheck`, `exhaustive`, `prettier`, `tsc` and `eslint`. But the citation is a **floor step**, not a rule field: the `check: { tool, id, status }` shape returns zero hits across the engine, so no rule can cite a linter finding. Of the three checks named as conform's own, **cell isolation ships** (R-002); manifest-vs-name does not (R-020 is unauthored) and specmap coverage cannot, since conform does not depend on the specmap crate. Orchestration, too, lives one layer up in each stack's `floor`, not in the engine.*</fact></p>
  </section>
  <section id="facts" title="3. The fact store">
    <p p="19"><fact id="fact-store-is-the-ledgers-facts-class-lead" status="impl/done">Language-neutral normalized facts; this is the ledger's "facts class" (LEDGER §3) instantiated:</fact></p>
    <fence lang="json" p="20">{ "fact": "item",   "lang": "rust", "path": "crates/vibe-resolver/src/naive.rs",
  "kind": "struct", "symbol": "vibe_resolver::naive::NaiveDepSolver",
  "span": [29, 41], "hash": "sha256:…", "attrs": ["spec(implements=…, r=2)"] }
{ "fact": "import", "from": "vibe_resolver::naive", "to": "vibe_core::manifest" }
{ "fact": "flag_read", "symbol": "…", "site": "crates/vibe-cli/src/registry.rs:88" }</fence>
    <list ordered="false" p="21">
      <item><fact id="KEY-IS-FILE-HASH-PLUS-FRONTEND-VERSION" status="impl/done">**Key:** `(file content-hash, frontend id+version)`. Facts never rot semantically — re-extraction happens only when the file or the frontend changes. This is what makes conformance incremental: a 1-file diff re-extracts 1 file.</fact></item>
      <item><fact id="STORE-IS-CONTENT-ADDRESSED-NEVER-COMMITTED" status="impl/done">Store: content-addressed, local + CI-cache; never committed (derived data with a deterministic producer).</fact></item>
    </list>
  </section>
  <section id="rules" title="4. Rules as queries">
    <p p="22"><fact id="RULES-ARE-RUST-TRAIT-IMPLS-COMPILED-IN" status="impl/done">v0.1: rules are Rust implementations of one trait — `fn check(&amp;self, facts: &amp;[SourceFacts]) -&gt; Vec&lt;Finding&gt;`, alongside `fn id()` and `fn why()` (`core-ai-native-conform/src/finding.rs:51-56`) — compiled into the engine. *The earlier sketch's `specmap: &amp;Index` parameter is dropped: the conform crate carries no dependency on the specmap crate, so the parameter was impossible, not merely absent.*</fact></p>
    <p p="23"><fact id="QUERY-DSL-IS-DELIBERATELY-DEFERRED" status="spec/done">A declarative query DSL (datalog-flavored) is deliberately deferred: we will know its right shape after ~30 real rules exist, not before (Open Question 2).</fact></p>
    <p p="24"><fact id="FINDINGS-CARRY-THE-A1-CHAIN" status="impl/done">**Findings** carry the A1 chain: rule id → why (axiom trace) → span → involved facts → deviation status (a matching `deviates` record downgrades the finding to `deviation-acknowledged`).</fact></p>
    <p p="25"><fact id="OUTPUT-IS-SARIF-PLUS-THE-RATCHET-BASELINE" status="impl/done">Output: SARIF (so IDEs and CI render findings for free) + the ratchet baseline file (`conform-baseline.json`): pre-existing findings are frozen per scope; new ones fail the gate; the baseline only shrinks.</fact></p>
  </section>
  <section id="determinism" title="5. Determinism and gates">
    <p p="26"><fact id="SAME-INPUTS-BYTE-IDENTICAL-SARIF" status="impl/done">Same inputs → byte-identical SARIF (stable ordering, no wall-clock).</fact></p>
    <p p="27"><fact id="DETERMINISM-TESTED-BY-RUN-TWICE-DIFF" status="impl/done">Tested the way vibevm tests its resolver and codegen: run twice, diff.</fact></p>
    <p p="28"><fact id="GATE-EXIT-CODE-IS-THE-ACCEPTANCE-CRITERION" status="impl/done">Gate command: `conform check --baseline conform-baseline.json --scope crates/vibe-resolver` — exit code is the acceptance criterion the Playbook relies on; no human judgment in the loop (A3).</fact></p>
  </section>
  <section id="policy" title="6. The policy file — where a key lives, and why">
    <p p="29"><fact id="kind-line-policy" status="impl/done">`req r1`</fact></p>
    <p p="30"><fact id="THE-ENGINE-OWNS-THE-SCHEMA-THE-CONSUMER-OWNS-THE-VALUES" status="impl/done">The gate is
config-driven: `conform.toml` at the project root is the **consumer's**
policy, and this engine owns its **schema**. The split is PROP-024's — the
policy stays with the consumer, the engine ships in the package — so a
project decides which crates are gated without forking a rule, and no
project can invent a key the engine does not define.</fact></p>
    <p p="31"><fact id="A-KEY-IS-ROOT-WHEN-IT-MODELS-NO-LANGUAGE" status="impl/done">**The rule that places a key:
a key sits at the ROOT when it models no language, and inside a
`[&lt;language&gt;]` section when it does.** The language sections are
homogeneous by construction — each owns the same `roots` / `gated` /
`exempt` shape over its own unit — while a key the engine reads
identically whatever produced the facts has no business being written
three times.</fact></p>
    <p p="32"><fact id="THE-ROOT-KEYS-ARE-DESCRIBED-HERE-AND-NOWHERE-ELSE" status="impl/done">**The root keys are
described here, once.** `max_file_lines` — the per-file line budget every
frontend feeds (default 600). `invariant_comment_markers` — the
invariant-marker vocabulary, the labeled colon-bearing tags; empty
disables the rule that reads it. `invariant_comment_min_file_lines` — the
file length below which that rule stays silent, because on a short file
"thirds" mean nothing (default 120). `sarif_reports` — where a floor step
deposits foreign-linter SARIF reports for the gate to read back in; one
deposit point for every linter, since each report names its own tool and
the engine never needs to know which language it concerns. Beside them sit
the per-language tables `[rust]`, `[typescript]` and `[go]`, each described
by its own stack's conform-surface document.</fact></p>
    <p p="33"><fact id="A-SURFACE-DOCUMENT-CITES-THE-ROOT-KEYS-RATHER-THAN-RESTATING-THEM" status="impl/done">**A
stack's conform-surface document names which root keys its rules read and
cites this section for what they mean — it does not restate their
defaults.** Three surface documents restating one default is three writers
for one truth, and the third copy is where it goes stale. The
per-**language** section is the surface document's own to describe in
full, because there the key and its meaning genuinely belong to that
stack.</fact></p>
    <p p="34"><fact id="A-RETIRED-KEY-BECOMES-A-LOUD-TOMBSTONE-NOT-A-REMOVAL" status="impl/done">**A key that moves
is retired as a loud tombstone, never deleted.** Nine flat root keys moved
into `[rust]` and each remains declared, typed to accept any shape, so its
presence is caught and rejected with a targeted move hint rather than
serde's generic unknown-field error. A silently-ignored stale key is a
project believing it is gated when it is not — the same disease as a dead
exclusion, and the same cure: say the name out loud.</fact></p>
  </section>
  <section id="open" title="7. Open questions">
    <list ordered="true" p="35">
      <item><fact id="OPEN-RUST-T-SEM-BACKEND" status="spec/done">rust-analyzer crates vs `rustc_driver` for Rust T-sem (stability vs fidelity) — decide when the first T-sem Rust rule actually lands; none of the Phase ≤4 checks need it.</fact></item>
      <item><fact id="OPEN-QUERY-DSL-SHAPE" status="spec/done">Query DSL: shape and whether rules become data (loadable rule-packs) — after 30 in-tree rules.</fact></item>
      <item><fact id="OPEN-FACT-SCHEMA-VERSIONING" status="spec/done">Fact schema versioning across frontend upgrades — proposal: schema carries `v`, store segregates by version, mixed reads forbidden.</fact></item>
      <item><fact id="OPEN-PERFORMANCE-ENVELOPE" status="spec/done">Performance envelope targets (full-workspace cold scan budget; warm incremental budget) — set from Phase 4 measurements.</fact></item>
    </list>
    <p p="36"><fact id="UNEXERCISED-FRONTEND-OR-TIER-IS-REMOVED" status="impl/done">*Any frontend or tier specified here that is not exercised by Playbook Phase 4 is either removed from this document or annotated in place as **specified, not built** — never carried as unmarked aspiration.*</fact></p>
  </section>
</spec>
