# Prevent band — deterministic pre-commit / pre-edit gate — design

audience: AI coding agents first. Optimize for activation, not prose. Do not prettify into narrative.
SoT for this phase. Prior art (point, do NOT re-inline): `2026-06-17-security-gate-design.md` (the bands + corpus
contract), `2026-06-17-resolution-completeness-design.md` (#40, the cross-file resolver this band reuses),
`docs/validation/2026-06-18-oracle-xfile-c02-catch.md` (the oracle = first registered detector, CATCH-measured).

## Goal (BLUF)

Stop bug classes BEFORE they land by running the **deterministic, no-LLM detectors** (oracle self-deal, deps,
headers, future structural rules) at commit/edit time — so subagents are spent ONLY on the bugs that need
reasoning. The end state is a pre-write gate for *every* programmatically-findable class; this spec builds the
architecture that scales there and registers the first three detectors.

**This deterministic gate IS the irreducible floor (`docs/ARCHITECTURE.md` §10.1): EVERY scanned file gets it,
UNCONDITIONALLY. Routing (§10) is ADD-only — it ADDS LLM specialist passes in DETECT where a category signal
fires; there is NO SUBTRACT, so it NEVER removes a class from this floor.** A file declared out-of-scope
(`.gateignore`) softens NOTICE WORDING only; the detectors still run.

**One thesis: a detector is an executable behind a stable contract; the gate is a thin dispatcher over a registry
of them.** That contract makes detector language free (the oracle is already TypeScript, not Python), lets the
detector set grow toward hundreds without a rewrite, and keeps the one expensive-to-change piece — the contract +
dispatch protocol — frozen from day one. This is "do it properly so you don't redo it": the *contract* is what you
don't redo; the dispatcher behind it is a contained, swappable piece.

## The three axes — the framing this whole design rests on (read first)

A detector has THREE independent properties. Do not collapse them; the block-vs-warn rule depends on keeping them
apart.

| axis | means | oracle today | buys you |
|---|---|---|---|
| **deterministic** | same input → same output, no LLM, no RNG | YES ✓ | "no dice roll" — reproducible, never randomly varies |
| **precise** | no false alarms | NO ✗ (the `referee\|referrer` over-flag) | safe to HARD-BLOCK raw |
| **complete** | never misses a real instance | NOT guaranteed ✗ (resolution gaps, #40) | "all bugs in 1 run" |

**Decision rule that falls out:** determinism is necessary for blocking but NOT sufficient — a deterministic
detector can deterministically false-alarm. **Block only on PRECISION.** A precise detector blocks; an
imprecise-but-deterministic detector WARNS; a human-confirmed warning instance becomes a precise per-instance block
via the ratchet. Claiming a deterministic gate catches "all bugs in 1 run" conflates determinism with completeness
— forbidden (it is the false-clean over-claim the project bans, carried below as no-false-clean).

**What this layer actually gives (vs the LLM find side) — answered on the axes, because "the odds look identical"
is the natural objection.** The oracle does NOT widen COMPLETENESS: coverage (which files + siblings are in scope)
comes from the SHARED resolver, so a resolution gap blinds the oracle and the LLM equally — adding the oracle buys
nothing against the miss-from-no-coverage failure. It COSTS precision (it over-flags → it warns). The entire gain is
on the DETERMINISM axis, and it is decisive for one measured reason: **[MEASURED] no unseeded LLM config ever caught
C02, while the oracle catches it on EVERY run within its coverage.** So the catch probability is NOT identical — it
is the LLM's measured ~0 (a dice roll that lands on miss) converted to a deterministic 1.0. **Reinforced [MEASURED]:
even feeding the oracle's CORRECT structural flag to Opus to "confirm" it REGRESSES recall — k=3, 3/3 inverted, the
LLM deleted the catch the oracle had (`docs/validation/2026-06-18-oracle-log-llm-interpretation-k3.md`). So the
LLM-over-log path is not merely unhelpful; it is actively harmful, and the ratchet (deterministic flag + one-time
human policy assertion → per-instance block), NOT interpretation, is the only sound flag→block bridge.** That same
determinism is
the ONLY thing that makes pre-write prevention possible at all: a k≥3 stochastic LLM union is too slow, too costly,
and too non-deterministic to gate every commit. The project thesis = migrate each programmatically-expressible class
across that line (dice-roll → deterministic), shrinking what still needs a roll. "All bugs in 1 run, no dice roll"
is reached class-by-class, exactly here — never by one detector being complete.

## Architecture — components and their boundaries

```
 trigger adapters            runner (pure)                detectors (executables, any language)
 ┌───────────────┐    files  ┌──────────────────────┐    ┌────────────────────────────────┐
 │ git pre-commit├──────────►│ select by glob+trigger│    │ oracle    (bun,  imprecise, S9) │
 │ (staged)      │  +trigger │ build context (deps)  ├───►│ deps      (py,   precise,   S11)│
 ├───────────────┤           │ run each detector     │    │ headers   (py,   imprecise, S11)│
 │ CC pre-edit   │           │ apply block policy    │◄───│ …hundreds, one manifest each   │
 │ (daemon clnt) │  DEFERRED │ + ratchet + coverage  │    └────────────────────────────────┘
 └───────────────┘           └──────────┬───────────┘
                                         │ needs_context="deps"
                                  ┌──────▼───────┐
                                  │  resolver    │  (collect_deps / run_oracle_set — the #40 validated logic,
                                  │ (shared)     │   extracted from gate.py; gate.py AND runner both import it)
                                  └──────────────┘
```

Five components. Each is independently testable and hides real complexity behind a stable boundary.

### 1. Detector contract (the frozen interface — do NOT change lightly)

A detector is ANY executable. The runner invokes it as a subprocess and speaks JSON. This is the one-way door;
freeze it now.

**Input** — runner → detector, argv + stdin:
- argv: the scan target. `scope:per-file` → the matching changed file(s) (a cross-file detector also receives its
  resolved dep files). `scope:repo` → the repo root (the detector scans the directory itself, e.g. `pnpm audit`).
- stdin (JSON): `{"trigger":"pre-commit","repo_root":"…"}` (context the detector may need; ignorable).

**Output** — detector → stdout (JSON), SARIF-aligned (the industry-standard static-analysis finding shape):
```json
{
  "detector": "oracle",
  "status": "ok",
  "findings": [
    {"ruleId":"S9-self-deal","level":"warning","class":"S9",
     "message":"accrueAffiliateCommission guards {owner|referrer} but NOT {owner|referee}",
     "file":"…/service.ts","line":356,"symbol":"owner|referee"}
  ],
  "coverage": {"scanned": ["…/service.ts"], "unresolved": []}
}
```
- `level` ∈ `error` | `warning` | `note` (SARIF levels). `error` = blockable (precise). `warning` = surfaced, not
  blocked unless ratcheted.
- `status` ∈ `ok` | `degraded` | `error`. `degraded` + non-empty `coverage.unresolved` = the detector ran but
  could not see everything → the runner MUST surface this and MUST NOT print "clean" (no-false-clean).
- `symbol` = the discriminator the ratchet and the corpus already use (`canonical_symbol`).

**Manifest** — each detector ships `detector.json` BESIDE it (same convention as cells live with detectors):
```json
{
  "id": "oracle",
  "exec": ["bun", "domains/security/detectors/oracle/oracle2.ts"],
  "scope": "per-file",
  "scope_globs": ["**/*.ts", "**/*.tsx"],
  "needs_context": "deps",
  "triggers": ["pre-commit"],
  "precision": "imprecise",
  "class": "S9"
}
```
- `scope` ∈ `per-file` | `repo`. `per-file` → argv = each matching changed file (oracle, headers). `repo` → argv =
  the repo root; the detector is selected when a staged file matches `trigger_globs`, then scans the directory ONCE
  (NOT per changed file). deps = `repo` (it runs `pnpm audit` over the tree, keyed off a lockfile change). This is
  the dimension that makes the contract fit detectors that are not per-file scanners.
- `trigger_globs` (REQUIRED for `scope:repo`, ignored for `per-file`) → which staged paths trigger the repo scan,
  e.g. `["**/pnpm-lock.yaml","**/package.json"]`.
- `needs_context` ∈ `none` | `deps`. `deps` → the runner calls the resolver first and passes resolved dep files.
- `triggers` ∈ subset of `pre-edit` | `pre-commit`. **A cross-file detector declares `pre-commit` ONLY** — pre-edit
  sees a file mid-edit with siblings that may not reflect the change, so cross-file resolution is unsound there.
  This field is WHY the contract carries triggers: it makes the soundness constraint declarative, not implicit.
- `precision` ∈ `precise` | `imprecise` → the default `level` and the block-vs-warn decision (below). **A detector
  may declare `precise` (→ block) ONLY if it ships a GREEN cell proving ZERO false-positive under its declared
  `scope_globs`.** This is the block-AUTHORIZING gate: it keeps any detector whose precision is not corpus-proven —
  the imprecise oracle, AND any detector whose precision depends on correct scoping (headers, below) — out of the
  hard-block path until proven. No GREEN-clean cell → `imprecise` → warn, regardless of what the manifest claims.

The oracle manifest above shapes the per-file cross-file case. The other two v1 detectors do NOT fit the
oracle's shape — their manifests below carry the new dimensions:
```json
// deps — repo-scope: argv=repo_root, triggered by a lockfile change, NOT a per-file scan.
// precise: pnpm audit reports ACTUAL CVEs (intrinsic precision); GREEN = clean lockfile → no findings.
{"id":"deps","exec":["python3","domains/security/detectors/deps/deps_audit.py","--emit","json"],
 "scope":"repo","scope_globs":[],"trigger_globs":["**/pnpm-lock.yaml","**/package.json"],
 "needs_context":"none","triggers":["pre-commit"],"precision":"precise","class":"S11"}

// headers — per-file, but precision is NOT intrinsic: it holds ONLY when scope_globs point at the repo's real
// response-header config file. Ships scope_globs:[] (matches NOTHING → never false-blocks an arbitrary .ts) and
// precision:imprecise (warn). A repo wires scope_globs to its header-config path AND adds a GREEN cell to EARN
// precise→block. Default = no-false-block.
{"id":"headers","exec":["python3","domains/security/detectors/headers/headers_scan.py","--emit","json"],
 "scope":"per-file","scope_globs":[],"trigger_globs":[],
 "needs_context":"none","triggers":["pre-commit","pre-edit"],"precision":"imprecise","class":"S11"}
```
**Detector conformance (v1 work, not free):** the existing detectors predate this contract and emit their own
native output — oracle2.ts prints EXTRACTED+FLAGS prose, `deps_audit.py`/`headers_scan.py` print their own shapes.
v1 adds a `--emit json` mode to EACH registered detector that maps its native result to the contract Finding JSON
(single source of truth — extend the detector, do NOT wrap it in a runner-side parser; a per-detector parser in the
runner would re-couple the runner to each detector's language and kill the polyglot property). The runner speaks
ONLY the contract JSON.

### 2. Registry (`prevent/registry.py`)

Loads every `domains/*/detectors/*/detector.json` (same glob discipline as `bench.py`). Returns the detector set.
Selection rule, applied per changed file: **a detector runs iff (file matches any `scope_globs`) AND (the active
trigger ∈ `triggers`).** With hundreds registered, each save runs only the handful whose globs match — this, plus
the daemon (below), is the performance answer, not the dispatcher's language.

### 3. Resolver (the shared cross-file context-builder — `gate.py` deterministic helpers, imported)

The cross-file context-builder is `collect_deps`, `build_bundle`, `run_oracle`, `run_oracle_set` — the #40-validated
logic that today lives in `orchestrator/gate.py`. **v1: the runner IMPORTS these deterministic helpers from
`gate.py` by PATH** (`importlib.util.spec_from_file_location`, the exact pattern `tests/test_oracle_xfile.py`
already uses: `gate = _load("sg_gate", …/gate.py); gate.collect_deps(…)`). Importing `gate.py` only defines
functions — its argparse/LLM `main()` is `if __name__ == "__main__"`-guarded, so no LLM runs at import. The runner
calls ONLY the deterministic helpers, NEVER `gate.py`'s LLM path (`one_roll`/`union_rolls`) — the blocking gate is
no-LLM by construction.

**Physical extraction to `resolver.py` is DEFERRED** (not done in v1). Rationale (advisor-caught): extracting these
out of `gate.py` would break the `gate.collect_deps` / `gate.run_oracle_set` module-attribute surface that the #44
catch-proof (`test_oracle_xfile.py`) asserts against — a refactor of the one validated cross-file asset, for no v1
benefit. Reversibility is identical either way: when a Rust dispatcher actually lands (the only event that forces a
clean resolver boundary), extract THEN and re-bind `gate.collect_deps = resolver.collect_deps` to preserve the
attribute surface, gated by re-running the full suite. Until then, "import the helper from where it is validated"
beats "move validated code to make the diagram tidy" (YAGNI). The boundary that matters now is the *import point*
(runner depends on named deterministic helpers, not on gate.py's LLM path), and that holds without moving a line.

### 4. Runner (`prevent/runner.py` — the core, pure)

A pure function: `run(changed_files, trigger, registry, repo_root) → Report`. No I/O of its own beyond invoking
detectors. Steps (ladder):
1. For each changed file, select applicable detectors (registry rule).
2. For each selected detector needing `deps`, call the resolver to build the dep-file set for that file.
3. Run each detector as a subprocess (parallelizable), parse its JSON, collect findings + coverage + status.
4. Apply the block policy (below) → blocking findings, warnings, coverage report.
5. Return a `Report`; the CLI maps it to an exit code (nonzero ⇔ any blocking finding).

### 5. Trigger adapters

- **git pre-commit (v1):** `prevent/prevent.py --trigger pre-commit` reads staged files via
  `git diff --cached --name-only`, calls the runner one-shot, exits with the runner's code. Wired by APPENDING a
  `# prevent-band v1 BEGIN/END`-marked block to `.git/hooks/pre-commit` AFTER the existing `# slopgate-hook v1`
  block (coexist — never overwrite slopgate's block).
- **CC PostToolUse pre-edit (DEFERRED — protocol frozen, impl gated):** a thin client sends one file to the daemon.
  Built only once a latency-sensitive single-file detector exists to justify it (YAGNI). The runner is the same
  pure function either way.

### Daemon (`prevent/daemon.py` — DEFERRED, protocol specced now)

The pre-write path's performance answer is the recognized resident-server pattern (LSP / `eslint_d` /
rust-analyzer): a process that holds the registry + detectors warm and wraps the runner in a request loop, so
cold-start (interpreter boot, manifest load) is paid ONCE, not per-save. **v1 does not build it** — the oracle (the
only registered cross-file detector) is `pre-commit`-only, which is latency-tolerant and needs no daemon. The
protocol is frozen now so building the daemon later touches nothing else:
```
request : {"trigger":"pre-edit","files":["…"],"repo_root":"…"}\n
response: <Report JSON>\n
```

## Block policy — precision decides, the ratchet bridges (decision rules)

Apply in order; stop at the first that holds. A finding from a file in the staged/edited set:
1. `level == "error"` (a precise detector) → **BLOCK**.
2. `level == "warning"` AND the instance `(file, symbol)` is in `prevent/confirmed.json` → **BLOCK**
   (regression-lock — a human confirmed this exact instance; it is precise by construction).
3. otherwise → **WARN** (printed, exit 0).

Plus, independent of findings:
4. any detector `status == "degraded"` with non-empty `coverage.unresolved`, OR `status == "error"` → print a
   loud **COVERAGE-INCOMPLETE** line naming what was not scanned. Never print "clean" when coverage is incomplete
   (no-false-clean). This does NOT block (a broken/limited detector must not wedge every commit — that trains devs
   to bypass the gate, the cardinal gate sin), but it MUST be visible.

**The ratchet = the Find→Prevent link.** When the LLM/find side confirms a real, structurally-expressible bug, its
`(file, symbol)` is appended to `confirmed.json`; thereafter the deterministic detector that flags that instance
BLOCKS it (rule 2) — a confirmed bug cannot return. v1 ratchet = per-instance regression-lock only. Auto-deriving a
NEW general detector from a confirmed find (the full learning loop) is deferred (§NOT covered).

`prevent/confirmed.json` shape (matched against `finding.file` suffix + `finding.symbol`, case-insensitive —
mirrors the corpus `canonical_symbol` substring discipline):
```json
[{"class":"S9","file":"apps/web/src/server/referrals/service.ts","symbol":"owner|referee","confirmed":"2026-06-18","ref":"#44"}]
```

**Exit codes** (the runner returns a `Report`; the CLI maps it): `0` = no blocking finding (commit/edit allowed;
warnings + coverage still printed). `1` = ≥1 blocking finding under `prevent.py --trigger pre-commit` (git
pre-commit halts on nonzero). The pre-edit adapter (deferred) maps a blocking `Report` to exit `2` (CC PostToolUse
block convention). A detector crash/timeout never sets a blocking code by itself (fail-open + COVERAGE-INCOMPLETE).

## Data flow

**pre-commit (v1):** `git commit` → hook → `prevent.py --trigger pre-commit` → staged files → runner → [select
oracle/deps/headers by glob] → [oracle needs deps → resolver builds dep set → run oracle on target+deps via
`run_oracle_set`] → collect findings → block policy → exit 0 (allow) or nonzero (block, print findings +
coverage).

**pre-edit (deferred):** Write/Edit → PostToolUse client → daemon (warm) → runner(single file, `pre-edit`) →
single-file detectors only → Report → hook exit 2 blocks the edit, 0 allows.

**Staged ≠ working-tree (v1 known gap, surfaced not hidden):** the adapter SELECTS files via `git diff --cached`,
but the resolver reads file CONTENT from disk (working tree). Under a partial stage (`git add -p`) or an edit-after-
stage, the gate scans working-tree content, not the staged blob being committed. v1 accepts this — working-tree
content is what the resolver's disk-based sibling reads already see, and the common case is staged==working-tree —
and treats any divergence as a caveat, never a silent clean. Sound staged-blob materialization (`git show :0:<file>`
into a temp tree the resolver reads from) is deferred (§NOT covered); slopgate runs `--staged`, so matching its exact
read semantics belongs to that follow-up. This weakens the no-false-clean guarantee at the staged/working-tree
boundary ONLY; within a file's scanned content the guarantee holds.

## Language policy (the Rust question, decided)

Decision rules, not a language war:
- **Detectors: any language.** Each is an executable behind the contract. The oracle is TypeScript today. Write a
  measured-hot detector in Rust if it pays — zero friction, it is just another manifest. This is the
  modular/expandable architecture.
- **Runner + resolver + daemon: Python now.** Reason: the validated resolver (#40 — `collect_deps`,
  workspace-alias, barrel-follow) is already Python; a Rust rewrite would re-implement AND re-validate trusted
  logic = the exact redo to avoid. The dispatcher is I/O-bound orchestration (the CPU cost is inside the
  detectors), so Python is not the bottleneck.
- **Rust is RESERVED, not refused.** If profiling the *dispatcher* (not the detectors) ever shows it bottlenecks at
  scale, rewrite that one thin piece against the frozen protocol — without touching the resolver or any detector.
  Rust where measured; never Rust-everything-on-faith. A Rust monolith now would be LESS expandable (it pressures
  every detector toward Rust and rewrites the validated resolver).

Honest answer to "is Rust slower to write than Python": not as a language — new dispatcher code, barely. Slower
*here* only because (1) it would redo+revalidate the existing Python resolver and (2) the contract/ratchet shape is
still settling, where edit-run beats compile-borrow-check. Both are timing costs, not Rust being worse.

## Error handling

- **Detector crash / non-JSON stdout / timeout** → record `status=error` for that detector; fail-open on its
  findings (do not block) BUT surface it in the COVERAGE-INCOMPLETE line (rule 4). Per-detector timeout (default
  10s pre-commit; tighter for pre-edit), mirroring slopgate's `timeout`-guarded edit-hook.
- **Resolver returns dropped deps** (cap hit / unresolved import) → the dep set is partial → the dependent
  detector's coverage is incomplete → surfaced, not silently clean.
- **Registry: a malformed `detector.json`** → skip that detector, surface it in coverage (a missing detector =
  reduced coverage, never a false clean).
- **No staged files / no applicable detector** → exit 0, but print "no detectors applicable" (not "clean").

## Testing (cells + the runner's own canaries)

Each registered detector already has RED/GREEN cells with a `canonical.json`. The Prevent band adds its own:
- **runner block cell** — RED: stage `oracle/cells/caller_vuln.ts` → runner exits nonzero AND `owner|referee`
  appears in a finding. GREEN: stage `caller_safe.ts` → exit 0. (Reuses the #44 cross-file cells — the runner path
  must reproduce the oracle's cross-file CATCH end-to-end through resolver + `run_oracle_set`.)
- **selection test** — a `.ts` file selects the oracle; a `.md` file selects none; a cross-file detector is NOT
  selected under `--trigger pre-edit`.
- **block-policy tests** — a precise `error` finding blocks; an `imprecise` `warning` does NOT block; the same
  warning with its `(file,symbol)` in `confirmed.json` DOES block (ratchet).
- **no-false-clean test** — a detector returning `status=degraded` + `unresolved` makes the runner print
  COVERAGE-INCOMPLETE and NEVER "clean", while still exiting 0.
- **precise-detector tests** — deps (real-CVE cell) and headers (missing-header cell) emit `error` → block
  directly (no ratchet needed).
- **daemon protocol test** — DEFERRED with the daemon.

Run with `rtk proxy python3 -m pytest` (RTK truncates routed pytest stdout). Tests load modules by PATH
(`importlib.util.spec_from_file_location`), skip cleanly where `bun` is absent (oracle runtime).

## Architecture Decisions

Accepted boundaries (deletion test = if removed, does complexity scatter to callers?):
- **resolver as a named boundary (import the `gate.py` deterministic helpers; physical `resolver.py` DEFERRED)** —
  KEEP the boundary (the runner depends on the named deterministic helpers, NOT on gate.py's LLM path); DEFER the
  physical extraction. Advisor-caught: extracting now breaks the `gate.collect_deps` / `gate.run_oracle_set` module
  attributes the #44 catch-proof asserts against, for zero v1 benefit — reversibility is identical either way.
  Extract only when a Rust dispatcher forces it, re-binding `gate.collect_deps = resolver.collect_deps` and gating on
  the full suite. Medium (the boundary holds without moving a line). Deletion test still applies to the *boundary*:
  collapse it and resolution logic leaks into the runner.
- **registry.py** — KEEP. Delete → manifest discovery + glob-selection scatter into the runner; the daemon needs
  the same discovery point. Medium.
- **runner.py (pure function)** — KEEP. The core; pure so it serves both one-shot (pre-commit) and resident
  (daemon) callers unchanged. Deep.
- **detector contract (subprocess + JSON)** — KEEP. Delete → detectors couple to the runner's language; the whole
  polyglot/expandable property dies. Deep, one-way door.

Accepted collapses:
- **ratchet folded into runner v1** — a `is_ratcheted(finding, confirmed)` function reading
  `prevent/confirmed.json`, NOT its own component. Single store, ~20 lines; a separate unit now is a decorative
  boundary (single-adapter). Split out when it grows beyond per-instance matching.
- **daemon NOT built v1** — protocol frozen, impl deferred. No latency-sensitive single-file detector exists yet to
  justify it (YAGNI); the one cross-file detector is pre-commit-only.

Rejected candidates (one-line reason):
- **Rust-from-scratch monolith now** — rewrites + re-validates the trusted Python resolver (the redo to avoid) and
  pressures every detector toward Rust (less expandable). Rust reserved for a measured dispatcher bottleneck.
- **Hard-block on raw oracle output** — the oracle is imprecise (deterministic ≠ precise); raw blocking
  false-blocks clean commits → devs bypass → gate dies. Imprecise → warn; ratchet → block.
- **Prevent band calls the LLM bands** — the blocking gate is no-LLM by definition; the LLM stays on the find/audit
  side; the ratchet is the only link.
- **Coupling to / extending slopgate's Rust core** — slopgate is reference only; the oracle is cross-file +
  multi-stage + TS-compiler-API and cannot be a single-file ast-grep rule. The Prevent band is security-gate's own
  generalized gate; the two hooks coexist.

## NOT covered / deferred (no-false-coverage — these are honest no-ops in v1)

- **Daemon implementation + pre-edit trigger** — protocol frozen, build gated on a latency-sensitive single-file
  detector existing.
- **#13 oracle payment-field lexicon hole** — an off-name money-move can slip the oracle. Fast-follow. Until fixed,
  the oracle's `coverage` MUST report its lexicon scope so an unmatched money-move surfaces as COVERAGE-INCOMPLETE,
  never a silent clean.
- **Auto-deriving a NEW general detector from a confirmed LLM find** — v1 ratchet only regression-LOCKS a confirmed
  `(file,symbol)`; generalizing a find into a fresh deterministic detector is the full Find→Prevent learning loop,
  deferred.
- **Rust dispatcher** — reserved for a measured dispatcher bottleneck against the frozen protocol.
- **npm / yarn workspaces resolution** — the resolver inherits whatever #40 delivers (pnpm-first; npm/yarn
  best-effort).
- **Cross-file PRECISION at n≥3** — the oracle's `referee|referrer` over-flag is a known precision item (#13-class);
  measuring/raising cross-file precision is the open problem this band makes visible, not one it closes.
- **Sound staged-blob scanning** — v1 scans working-tree content on disk (the resolver's read path); under partial
  staging the committed blob can differ. Materializing staged blobs (`git show :0:<file>`) into a temp tree the
  resolver reads from is deferred; check slopgate's `--staged` read semantics and match them. Surfaced as a caveat
  (Data flow), never a silent clean.
- **Ratchet pins `(file, symbol)`** — a confirmed instance whose file is RENAMED (or symbol refactored) silently
  drops its regression-lock (the ratchet stops matching) = a quiet no-false-clean weakness. Acceptable for v1
  (the deterministic detector still flags it as a warning; only the auto-BLOCK promotion lapses). A rename-robust key
  (content hash / AST anchor) is deferred.
