<?xml version="1.0" encoding="UTF-8"?>
<spec xmlns="https://vibevm.org/spec/1">
  <title id="root">Tool Spec: `rust-ai-native-conform-frontend` — the Rust frontend for the language-neutral conform engine</title>
  <status stage="spec" state="done"/>
  <p p="1"><fact id="status-line" status="impl/done">*Status: **SHIPPED with this package** — `crates/rust-ai-native-conform-frontend` (`id = "rust-syn"`) + `crates/rust-ai-native-conform` (binary **`rust-ai-native-conform`**), parsing `.rs` in-process with `syn`.*</fact></p>
  <p p="2"><fact id="RUST-IS-THE-PILOT-THE-OTHERS-ARE-PROJECTIONS" status="impl/done">*Rust is the pilot language: `go-ai-native-conform-frontend` and `typescript-ai-native-conform-frontend` are projections of the discipline this frontend proved first. This document is the last of the three surface specs to be written, and it is written to the form its own projections established.*</fact></p>
  <p p="3"><fact id="NO-SIDECAR-IS-THE-STRUCTURAL-DIFFERENCE" status="impl/done">*The one structural difference from both projections: Rust needs **no extractor sidecar**. Go spawns a stdlib-only `go-extract` and TypeScript a node process, each over an NDJSON bridge, because neither language parses itself from inside a Rust binary. Rust does — `syn` is a library — so the fact producer is a function call, not a process, and the bridge protocol, the content-addressed materialisation of the extractor, and the degraded-file note that protects against a crashing sidecar all have no counterpart here.*</fact></p>
  <section id="division" title="1. The division of labour with the native Rust tooling">
    <p p="4"><fact id="kind-line-division" status="impl/done">`req r1`</fact></p>
    <p p="5"><fact id="RUSTC-CARRIES-THE-TYPE-CORRECTNESS-HALF" status="spec/done">Rust's own toolchain carries the **type / correctness** half: `cargo build` (the compile gate), `cargo clippy` (the shipped lint census), `cargo fmt --check`, and the test suite as evidence providers.</fact></p>
    <p p="6"><fact id="THAT-HALF-IS-WELL-TYPED-AND-LOCALLY-SANE" status="spec/done">Those answer *"is this well-typed and locally sane?"* — the half the language does natively and well.</fact></p>
    <p p="7"><fact id="frontend-answers-the-structural-half-lead" status="impl/done">This frontend answers the **other** half — the *structural / architectural* rules no Rust tool expresses:</fact></p>
    <list ordered="false" p="8">
      <item><fact id="RULE-R-001-FLAG-SITES" status="impl/done">the cell-construction rule `R-001` (flag-sites): a cell's `&lt;Type&gt;::new(...)` constructor appears only in the selection registry module (`[rust] registry_file`), mounted only when both `registry_file` and `registry_gated_crate` are set;</fact></item>
      <item><fact id="RULE-R-002-CELL-ISOLATION" status="impl/done">`R-002` (cell isolation): a cell module imports seams and core only, never a sibling cell;</fact></item>
      <item><fact id="RULE-UNSAFE-GATE" status="impl/done">`unsafe-gate`: `unsafe` stays inside designated `[rust] audit_crates` or under a fn-grain `#[spec(deviates = …, reason = …)]` testimony (B-025: marked `DeviationAcknowledged`, visible in the IR, never failing the gate — not suppressed);</fact></item>
      <item><fact id="RULE-SEAM-HAS-DOCTEST" status="impl/done">`seam-has-doctest`: every public seam — a `pub` item at a gated crate's `src/lib.rs`, or a `pub trait` anywhere under `src/` — carries a compiled doctest;</fact></item>
      <item><fact id="RULE-PUB-DOCTEST" status="impl/done">`pub-doctest`: every public type seam (`struct` / `enum` / `trait` / `union`) in a `[rust] gated_pub_doctest` crate carries a compiled doctest or a `#[spec(documents)]` edge;</fact></item>
      <item><fact id="RULE-ERROR-ENUM-CITES-REQ" status="impl/done">`error-enum-cites-req`: a thiserror enum in a gated crate carries a `#[spec]` REQ edge — the attribute half of the Class-F seam-error contract;</fact></item>
      <item><fact id="RULE-ERROR-MESSAGE-CITES-REQ" status="impl/done">`error-message-cites-req`: a thiserror variant's `#[error("…")]` Display text carries a `spec://` REQ — the message half of the same contract;</fact></item>
      <item><fact id="RULE-CELL-HAS-ORACLE" status="impl/done">`cell-has-oracle`: every `#[cell]`-manifested type is referenced by an integration test in its crate (the differential / characterization oracle);</fact></item>
      <item><fact id="RULE-CELL-NAME-IS-COMPUTED" status="impl/done">`cell-name-is-computed`: a cell's type name follows the computed convention (Pascal(variant) + seam, B-038);</fact></item>
      <item><fact id="RULE-FILE-LENGTH" status="impl/done">`file-length`: a source file over the root `max_file_lines` budget (R3-003, position is a resource);</fact></item>
      <item><fact id="RULE-INVARIANT-COMMENT-POSITION" status="impl/done">`invariant-comment-position`: an invariant marker — a labeled, colon-bearing tag from the configured vocabulary, never a forceful word in prose — buried in the middle third of a long file, fed by the root `invariant_comment_markers` / `invariant_comment_min_file_lines` keys;</fact></item>
      <item><fact id="RULE-NO-UNWRAP-IN-DOMAIN" status="impl/done">`no-unwrap-in-domain`: `.unwrap()` / `.expect()` stays out of domain logic (test scope and fn-grain deviations honored);</fact></item>
      <item><fact id="RULE-AMBIENT-ENV" status="impl/done">`ambient-env`: `std::env::{var,var_os,set_var,remove_var}` reads stay at the composition root (`[rust] env_roots`) — an R-001 projection of the same flag-at-the-seam discipline;</fact></item>
      <item><fact id="RULE-DECLARED-TEST-MATRICES" status="impl/done">`declared-test-matrices`: a swept test matrix (a `2^n` bit-mask loop bound, or a ≥3-deep Cartesian nest of range `for` loops, in test context only) is generated, not declared (R-060);</fact></item>
    </list>
    <p p="9"><fact id="RUST-RULES-DIFFER-FROM-THE-GO-PROJECTION-IN-SHAPE" status="impl/done">*Where this list differs from the Go projection (measured, not copied from the twin): Go folds its domain bans into one `RULE-BAN-CENSUS-AS-FACTS` umbrella, where Rust splits them into the individual rules `unsafe-gate`, `no-unwrap-in-domain`, and `ambient-env`; Go carries the seam-error contract as one rule (`go-seam-error-cites-req`, both halves), where Rust splits it into `error-enum-cites-req` (the attribute half) and `error-message-cites-req` (the message half); and the deviation escape hatch is not a standalone rule here — it is honored as `in_deviation` testimony stamped on the `UnsafeUse` / `UnwrapUse` / `EnvRead` facts (B-025). The `lint-suppression-needs-reason` rule is wired into the Rust gate too, but it is fed by SARIF-ingested `LintDiagnosis` facts (foreign linters such as clippy), not by this frontend's facts — so it belongs to the T-sem citation path (§5), not to this list.*</fact></p>
    <p p="10"><fact id="ONE-ENGINE-ONE-GRAMMAR-ONE-BASELINE" status="impl/done">Routing these through conform keeps **one rule engine, one finding grammar, one ratchet baseline** across all three languages, with the rules defined once over `conform_core::Fact` and fed by any frontend — a rule cannot drift between projections.</fact></p>
  </section>
  <section id="extractor" title="2. What the frontend is">
    <p p="11"><fact id="kind-line-extractor" status="impl/done">`req r1`</fact></p>
    <p p="12"><fact id="RUST-FRONTEND-IS-A-FACT-PRODUCER" status="impl/done">A fact producer: parse a `.rs` file and emit the language-neutral fact stream the rules consume.</fact></p>
    <p p="13"><fact id="RUST-FRONTEND-IS-IN-PROCESS-SYN" status="impl/done">**In-process by construction** — `syn` parses the file inside the same binary, so there is no subprocess, no materialised extractor, no wire protocol and no toolchain requirement beyond the one already compiling the project.</fact></p>
    <p p="14"><fact id="UNPARSEABLE-INPUT-IS-TOLERATED" status="impl/done">An unparseable file yields **zero facts and no panic** — the same contract the sidecar frontends express as a `degraded` note, reached here by returning an empty vector.</fact></p>
    <list ordered="false" p="15">
      <item><fact id="FACT-KINDS" status="impl/done">**Fact kinds** (the rust-syn vocabulary): `Item` (a declared fn / struct / enum / trait with verbatim `spec(...)` / `cell(...)` / `verifies(...)` attribute text, the `pub` flag, and a doc-fence flag — the compiled-doctest candidate), `Import` (a `use` declaration: importing module → imported path), `Ctor` (a `&lt;Type&gt;::new(...)` construction site — the R-001 cell-construction signal), `UnsafeUse` (an `unsafe` block / `unsafe fn` / unsafe impl method, with `in_test` and fn-grain `in_deviation` scope), `ErrorVariant` (a `#[error("...")]` thiserror variant with its owning enum's attribute text — the Class-F signal), `FileMetrics` (whole-file physical line count, one per parsed file — the file-length signal), `UnwrapUse` (a `.unwrap()` / `.expect()` call site, with test and deviation scope), `EnvRead` (a `std::env::{var,var_os,set_var,remove_var}` access site — the ambient-env signal, with test and deviation scope), `InvariantComment` (a comment carrying an invariant marker, from a raw-text scan — `syn` drops plain `//` comments so the AST cannot supply them), and `TestSweep` (a swept-matrix signal in test context only — `bitmask` for a `2^n` loop bound, `nested-loops` for a ≥3-deep Cartesian nest of range `for` loops). The `TsUnsafe` / `TsEnvRead` / `TsSeamError` / `GoUnsafe` / `GoConformance` variants belong to the other frontends and are never emitted here; `LintDiagnosis` arrives via SARIF ingest, not this frontend.</fact></item>
      <item><fact id="RUST-PARSES-TWICE-AND-THE-DUPLICATION-IS-THE-COST" status="impl/done">**Rust parses twice, not once.** Go's twin claims "one extraction, two consumers" (the conform frontend eats `facts`, the specmap scanner eats `markers`, one parser). That does NOT hold for Rust: the specmap scanner (`core-ai-native-specmap::rscan::scan_source`) runs its OWN independent `syn::parse_file` pass over the same `.rs` text and emits `CodeItem` / `Edge` / `Warning` for the spec/code index, while this frontend's `syn::parse_file` pass emits `Fact`s for the rules. Two separate crates, two separate `syn` parses, two separate models — no shared parser, no shared vocabulary. The conformity Rust buys by parsing in-process everywhere does not extend to a single shared parse; the duplication is the recorded cost.</fact></item>
    </list>
  </section>
  <section id="frontend" title="3. The frontend crate">
    <p p="16"><fact id="kind-line-frontend" status="impl/done">`req r1`</fact></p>
    <p p="17"><fact id="FRONTEND-IMPLEMENTS-THE-ENGINE-TRAIT" status="impl/done">`rust-ai-native-conform-frontend` implements the engine's `Frontend` trait: an `id()` of `"rust-syn"`, a `version()` that bumps when the fact schema grows (retiring cache slots wholesale), and an `extract(file, package, module, text) -&gt; Vec&lt;Fact&gt;` that parses in-process.</fact></p>
    <p p="18"><fact id="VERSION-IS-A-CACHE-KEY-NOT-A-RELEASE" status="impl/done">**The frontend version is a cache key, never a release number.** It is at **`"10"`** (frontend v10), and each bump retires every cached fact slot keyed by the old value. Each bump is a change to what the extraction EMITS — a new fact variant or field added (the history adds `ErrorVariant`, `FileMetrics`/`UnwrapUse`, `UnsafeUse`, `EnvRead`, `InvariantComment`, `TestSweep`, and the `is_pub`/`has_doctest` fields), or the scope of an existing fact narrowed (the marker vocabulary shrank to five labeled tags in v8; the nested-loop signal narrowed to range iterables in v10) — so the cached fact set from the old extraction no longer matches and must retire wholesale.</fact></p>
    <p p="19"><fact id="FACTS-ARE-CONTENT-ADDRESSED" status="impl/done">Facts are keyed `(file content-hash, frontend id+version)` in the engine's content-addressed store — a 1-file diff re-extracts 1 file.</fact></p>
  </section>
  <section id="topology" title="4. Topology: the `[rust]` policy section">
    <p p="20"><fact id="kind-line-topology" status="impl/done">`req r1`</fact></p>
    <p p="21"><fact id="CONFORM-TOML-GAINS-A-RUST-SECTION" status="impl/done">`conform.toml` gains a `[rust]` section, the ten keys of the engine's `RustConfig`: `roots` (source roots to scan; a `&lt;dir&gt;/*` entry expands each subdirectory as one crate, any other entry is a literal crate dir; default `["crates/*"]`), `exclude_substrings` (a file whose repo-relative path contains any of these is skipped; default `["/generated/"]`), `gated` (the crates the Class-F/G gates apply to — the unit list, since Rust's gate unit is the crate; default `[]`), `[[rust.exempt]]` `{unit, reason}` (crates deliberately outside `gated`, each with a recorded reason; default `[]`), `gated_pub_doctest` (crates whose whole public type surface is gated for doctests; default `[]`), `audit_crates` (designated audit crates, exempt wholesale from the unsafe and ambient-env gates; default `[]`), `env_roots` (repo-relative files where reading the ambient environment is sanctioned; default `[]`), `registry_file` (the one legal cell-construction site, R-001; `None` disables R-001; default `None`), `registry_gated_crate` (the crate R-001 gates, meaningful only with `registry_file`; default `None`), and `[[rust.floor_disable]]` `{step, reason}` (floor steps this project disables, each with a recorded reason; default `[]`).</fact></p>
    <p p="22"><fact id="ROOT-KEYS-ARE-LANGUAGE-NEUTRAL-AND-LIVE-AT-THE-ROOT" status="impl/done">The **language-neutral** keys sit at the root of `conform.toml`, NOT under `[rust]`, because they model no language. The Rust rules read four: `max_file_lines` feeds `file-length`; `invariant_comment_markers` / `invariant_comment_min_file_lines` feed `invariant-comment-position`; and `sarif_reports` feeds the SARIF ingest whose `LintDiagnosis` facts the `lint-suppression-needs-reason` rule cites (the T-sem citation path, §5). What each root key means, and what it defaults to, is described once, in `ENGINE-CONFORM §6` (the policy file) — this surface names which ones the Rust rules read, not their values. The per-language tables `[rust]` / `[typescript]` / `[go]` are root keys too, each owning its own section; and nine retired flat root keys (`roots`, `exclude_substrings`, `gated_crates`, `gated_pub_doctest`, `audit_crates`, `env_roots`, `registry_file`, `registry_gated_crate`, `exempt`) are now loud tombstones — their presence parses but `Config::load` rejects each with a targeted move hint, never serde's generic unknown-field error.</fact></p>
    <p p="23"><fact id="EVERY-CRATE-GATED-OR-EXEMPT" status="impl/done">The every-unit-gated-or-exempt invariant is enforced by the engine on every check, exactly as for the sibling stacks — Rust's gate unit is the **crate** (Go's is the package, TypeScript's the cell — the B-029 ruling), so `[rust] gated` / `[[rust.exempt]]` is the expand-as-you-conform ratchet over crates.</fact></p>
  </section>
  <section id="honesty" title="5. The honest note">
    <p p="24"><fact id="SYN-IS-A-PARSER-NOT-A-TYPE-CHECKER" status="impl/done">The structural gate is only as good as its facts, and `syn` is a PARSER, not a type checker: it sees syntax and attributes, not resolved types.</fact></p>
    <p p="25"><fact id="TYPE-DEPENDENT-RULES-ARE-OUT-OF-SCOPE" status="impl/done">Rules that would need type information (e.g. "this call's receiver is a seam type") are out of this tool's scope.</fact></p>
    <p p="26"><fact id="THE-DIVISION-IS-THE-THREE-TIER-SPLIT" status="impl/done">The division is deliberate — the same three-tier split (T-lex / T-syn / T-sem) `ENGINE-CONFORM` §1 defines; this frontend is the T-syn tier.</fact></p>
    <p p="27"><fact id="RUSTS-T-SEM-TIER-IS-CLIPPY-AND-THE-GAP-IS-A-TYPE-AWARE-LINT-LIBRARY" status="impl/done">**Rust's T-sem tier is not empty, and the gap is narrower than "no vehicle".** By the Go twin's own framing — "Go's T-sem tier is the toolchain itself" (`vet`, `staticcheck`, `exhaustive`) — the toolchain's lint engine IS the T-sem tier; Rust's toolchain lint engine is **clippy**, the direct parity analogue. And where the sibling stacks merely list their toolchain linters as evidence providers in prose, this engine reads clippy's verdicts back IN as `Fact::LintDiagnosis` via SARIF ingest (B-026: the root `sarif_reports` key, `sarif::load_reports`), so a Discipline rule may CITE a clippy diagnosis as the evidence for its own claim (`Fact::cites_lint`, the `lint-suppression-needs-reason` rule) — the deepest T-sem wiring of the three stacks, not an empty one. **The genuine, narrower gap** the draft was reaching for: there is no DISCIPLINE-AUTHORED type-aware lint library — no `dylint`-class vehicle carrying Discipline-specific type rules (none ships in this stack, which pins stable Rust 1.93 / edition 2024) — so the type-dependent rules the T-syn frontend cannot express (it sees syntax, not resolved types) stay out of the tool's scope. That gap is recorded rather than silent, and the grammar such a vehicle would speak (`violates REQ …; fix surface: …`) is already spoken by the conform rules themselves — and, through the citation path, by an ingested clippy diagnosis.</fact></p>
  </section>
  <section id="boundaries" title="6. What this document does NOT cover">
    <p p="28"><fact id="THE-RULES-THEMSELVES-LIVE-IN-THE-ENGINE-SPEC" status="impl/done">The rules' semantics live in `ENGINE-CONFORM`, one definition for all three languages; this document covers only what the RUST side reads, emits and configures.</fact></p>
    <p p="29"><fact id="THE-GUIDE-IS-THE-AUDIENCE-DOCUMENT" status="impl/done">`GUIDE-AI-NATIVE-RUST.xml` is the author-facing document — how to write code that passes; this one is the surface: what the tool reads and what it accepts.</fact></p>
  </section>
</spec>
