# DETECT orchestrator (`orchestrator/detect.py`) — design

audience: AI coding agents first. BLUF-ordered, imperative, [MEASURED]/[TARGET]-tagged. This is **refactor-3's own
spec**, explicitly deferred from the design SoT — do not re-derive the design-level decisions here, they are settled
upstream:
- `docs/specs/2026-06-18-modular-hierarchy-design.md` §refactor-3 (the mapper→DETECT dispatch split + contract-
  unification work item) and §"Two-dispatcher architecture".
- `docs/specs/2026-06-18-modular-domain-detectors-design.md` §"The Opus DETECT orchestrator" (the 4-step flow) and
  §"Architecture decisions" (baseline always-on + matched specialists).

This doc turns that settled intent into a buildable component spec (architecture, components, data flow, error
handling, testing). It does NOT decide the fan-out scope (how many specialists exist) — DETECT is parameterized over
**whatever detectors are registered**, so it is decoupled from that user-gated lever (see Open Questions).

## BLUF — what DETECT is

DETECT is the **production scan harness**: per attack-surface target, it selects `baseline` + every matched specialist
detector by their `applies_to` manifest fields, dispatches the gate **k=1** per (target, detector), unions findings
across detectors, semantic-merges paraphrases, and emits a per-target report + a run-level coverage map. The SAME
per-target core, pointed at the corpus instead of a repo (`--bench`), is what finally **measures k=1 production
recall** — the number the standing 99%-real-catch goal is defined against (currently ~86% bare baseline, [MEASURED]
not 99% — but that ~86% was **hand-judged**, not bench-scored; `run_bench` is a new, reproducible instrument the floor
must be **re-baselined through** before comparison — see Component E instrument-comparability pin). It reuses, does not rewrite: `mapper` keeps the
deterministic enumerate half; `gate.py` stays the per-(target,detector) engine (resolver deps + oracle legs + k rolls
+ within-detector merge); `semantic_merge.merge_groups` stays the one merge engine, now reused cross-detector.

Production = **k=1 per detector per target** (steady state). The k=3 same-model union stays the FLAKINESS AUDIT
(a post-99% diagnostic), reachable via `--k 3`. This split is settled in the hierarchy SoT §k=1-vs-k=3; do not relitigate.

## Scope — the smallest first cut that proves k=1 production recall

IN (MVP):
1. **Map** — reuse `mapper.enumerate_surface(root)` (or load a cached map via `--map <path>`).
2. **Triage** — a NEW `applies_to` router: `baseline` (always-on) + every `kind=="llm"` detector whose `applies_to`
   matches the target (kind ∈ `applies_to.kinds` OR `applies_to.always` OR `applies_to.signal` regex hits the file
   content). Deterministic manifest match; **never narrower** than the match.
3. **Dispatch k=1** — subprocess `gate.py --detector <id> --k 1 --emit <json> --report <md> --config-dir …` per
   (target, detector), bounded concurrency.
4. **Cross-detector union → semantic-merge** — collect each detector's contract-shape findings, union them, run
   `semantic_merge.merge_groups` across detectors, emit a per-target merged report.
5. **Run-level coverage map + merged findings** — assemble mapper's 3 buckets (scanned / budget-dropped /
   not-enumerated) augmented with per-target STATUS, plus a machine-readable merged findings file for recall
   measurement. **No-false-clean throughout.**

Plus the enabling **contract-unification** change to `gate.py`: a new additive `--emit <path>` that serializes the
LLM groups + oracle findings into the frozen `prevent/contract.py` shape (so both arms — prevent runner and DETECT —
consume ONE finding shape).

OUT (deferred — additive, reversible; see Open Questions): the elusive-bug hammer; Opus-judgment triage *widening*;
repo-level deterministic detectors (deps/headers S11, run once per repo not per target); per-subprocess oracle
de-duplication (correctness-fine today, a perf optimization).

## Components

All modules path-loaded (`importlib.util.spec_from_file_location`, NO `__init__.py`), stdlib-only, no new deps. The
path-load idiom is `mapper.py._load(name, path)` — copy it. Testing-native vocab only.

### A. `gate.py` change — `--emit <path>` (the contract-unification work item)

**Rule:** add an additive `--emit <path>` flag. When set, AFTER the existing `groups`/`oracle_results` are computed
(gate.py:284–320), serialize them into the `prevent/contract.py` shape and write JSON to `<path>`. The markdown
report path is UNCHANGED — `--emit` is purely additive, so a gate run without `--emit` is byte-identical to today
(zero regression; lock this with a test).

**Mapping (LLM groups → contract findings):** for each group `g` in `groups`:
- `contract.finding(rule_id=<detector-id or "baseline">, level=LEVEL[g["sev"]], cls=<detector covers, joined>,
  message=g["title"], file=<target relpath>, line=0, symbol="")`
- then attach two ADDITIVE keys for the cross-detector merge + audit signal: `f["sev"]=g["sev"]` (original
  critical/high/medium/low/unrated) and `f["rolls"]=len(g["rolls"])`, `f["of"]=k`. Additive keys are
  contract-safe (consumers ignore unknown keys; the frozen shape is a SUBSET, not violated).
- `LEVEL = {"critical":"error","high":"error","medium":"warning","low":"note","unrated":"note"}`.

**Mapping (oracle → contract findings):** for each `(f, out)` in `oracle_results` where `oracle_status(out)` is not
silent: emit `contract.finding(rule_id="oracle", level="error", cls="S9", message=<one-line oracle summary>,
file=<relpath>, line=0, symbol="")`. An `unreliable` oracle result goes to `coverage.unresolved`, never dropped.

**Status + coverage (no-false-clean):** `contract.emit()` writes to STDOUT, so do NOT call it — gate writes a FILE.
Build the same dict `{"detector":<id>, "status":…, "findings":[…], "coverage":{"scanned":[…], "unresolved":[…]}}`
using `contract.finding(...)` per finding and replicating `emit`'s status rule (`status = "degraded" if unresolved
else "ok"`), then `json.dump` it to `--emit <path>`.
- `coverage.scanned` = `[target] + [dep relpaths]`.
- `coverage.unresolved` = dropped deps (each `"<relpath>: <reason>"`) + a `"semantic-merge degraded"` marker when
  `merge_degraded` + any `unreliable` oracle file. Non-empty `unresolved` ⇒ `status="degraded"` (the contract auto-
  degrades; DETECT treats degraded as COVERAGE-INCOMPLETE).
- `line`/`symbol` precision (parsing the finding body for exact location) is a deferred refinement, not MVP. For
  recall measurement the `message` (group title) text is sufficient — `bench.is_flagged` (`bench.py:35–42`)
  substring-matches the cell's `canonical_symbol`/`class` against the finding STRING only; it reads no `file`/`line`.

`contract` is path-loaded inside `gate.py` only when `--emit` is set (lazy; keeps the no-emit path dep-free).

### B. `detect.py` — `select_detectors(target_path, kind, detectors)` (the NEW router)

**Rule:** return `baseline` + every `d` in `detectors` with `d.get("kind")=="llm"` and APPLIES, where APPLIES =
`d["applies_to"].get("always")` OR `kind in d["applies_to"].get("kinds", [])` OR (`d["applies_to"].get("signal")`
regex `re.search`-matches the target file's content). `baseline` is obtained by a **guarded id lookup**
(`next((d for d in detectors if d["id"]=="baseline"), None)` — a generator-with-default, NOT a bare `next(...)`
which raises `StopIteration` on absence; a `None` result triggers the run-level hard stop below as a clean
`COVERAGE-INCOMPLETE` abort with a message, never an uncaught exception) and always included even if its own
`applies_to` would also match (dedupe by id).
A detector with NO `applies_to` is treated as non-matching (never silently run; never silently skipped — it simply
does not route here, and DETECT only claims coverage for what it routed).

**Routing is ADD-only — there is NO SUBTRACT.** `select_detectors` is the ROUTED-DEPTH half of the floor/depth
split (canonical invariants: `docs/ARCHITECTURE.md` §10; routing detail + `bench --routing` gate:
`docs/specs/2026-06-17-attack-surface-mapping-design.md` §"Class-routing"). A specialist is ADDED on
`applies_to` match (`kinds` ∪ `signal` ∪ `always`); absence of the signal = not added. **[DECISION 2026-06-19:
no `sink_signal`.]** A class-exclusion SUBTRACT was measured redundant (it collapses into `applies_to.signal`)
and unsafe (imprecise sink-grep = oracle-class noise) — `bench --routing` step 0 proved it has no target but
the forbidden `baseline` floor. `select_detectors` MUST be **never narrower than the manifest match**; it never
removes a class. The deterministic floor (PREVENT band-3, every scanned file) is never subtracted.

**Baseline-absent is a RUN-LEVEL hard stop, not a per-target footnote.** baseline is the multi-class floor under
EVERY target; without it there is no floor and every "0 findings" is suspect. So if the id lookup finds no `baseline`
in the loaded detectors (manifest missing, or in `registry.load`'s `skipped`), `run`/`run_bench` must abort the whole
run as `COVERAGE-INCOMPLETE` BEFORE scanning any target — never degrade per-target, never emit a clean. This is
categorically unlike a missing specialist (reduced coverage on its classes only): a missing baseline is reduced
coverage on EVERYTHING. (Lesson source: UPDATE-19 — a leg silently losing its reach reads as a false clean.)

**No-false-narrowing:** the router is the deterministic floor. It is allowed to be WIDER (an Opus widening pass is a
deferred enhancement) but MUST NOT be narrower than the manifest match. Reading file content for `signal` is bounded
(read once per target, reuse across detectors).

Discovery reuses `prevent/registry.load(domains_root) -> (detectors, skipped)`; `skipped` (malformed manifests) is
carried into the run-level coverage as COVERAGE-INCOMPLETE (a missing detector is never a false clean).

### C. `detect.py` — `dispatch_one(target, detector, k, cfg, model, effort, out_dir) -> dict`

**Rule:** subprocess `gate.py` exactly as `mapper.dispatch` does (`[sys.executable, gate.py, target, --detector
<id>, --k <k>, --emit <json>, --report <md>, --config-dir <cfg>, --model, --effort]`, plus aliases passthrough).
Return the parsed contract JSON on success. On `returncode != 0` OR missing/unparseable `--emit` JSON, return a
synthetic `{"detector":<id>,"status":"error","findings":[],"coverage":{"scanned":[],"unresolved":["gate failed"]}}`
— a failed gate is COVERAGE-INCOMPLETE, **never** an empty-findings clean (mirrors `mapper.dispatch` returning None
→ "NEVER clean").

### D. `detect.py` — `detect_target(target, kind, detectors, …) -> dict`

**Rule:** `select_detectors` → dispatch each selected detector (bounded `cf.ThreadPoolExecutor`, like gate) →
collect contract JSONs → reconstruct merge groups from each finding (`{title:f["message"], sev:f["sev"],
rolls:set(range(f["rolls"]))}`) → `semantic_merge.merge_groups(all_groups, config_dir, model, effort)` across
detectors → write a per-target merged markdown report. Return `{target, kind, detectors:[ids], status, findings,
merged_report_path}` where `status` = `"degraded"` if ANY selected detector returned degraded/error OR merge
degraded, else `"clean"` if zero merged findings, else `"findings"`. **`clean` is reachable ONLY when every selected
detector ran ok with zero findings** — any degradation forces `degraded` (COVERAGE-INCOMPLETE), never clean.

### E. `detect.py` — `run(root, …)` (production scan) + `run_bench(cells, …)` (recall) + `main(argv)`

DETECT has **two output modes that share the `detect_target` core** but serialize differently, because the two
consumers want different shapes. Do NOT conflate them.

**Rule (`run` — production scan over a real repo):** build the map (`--map` cache or `mapper.enumerate_surface` +
`mapper.prioritize`) → for each enumerated target up to `--max-targets` (budget): `detect_target` → accumulate.
Assemble the run-level coverage map = mapper's 3 buckets (`enumerated_scanned` / `enumerated_budget_dropped` /
`not_enumerated`) PLUS a per-target `status` on each scanned entry, PLUS `skipped_detectors` (from registry) and a
`coverage_incomplete` list (every degraded/error target). Emit: (1) a run-level report (markdown), (2) a
machine-readable **per-target findings JSON** (contract-shape, keyed by target relpath) for CI / human triage. This
mode does NOT feed `bench.py --findings` — bench is keyed by corpus cell id, not repo path (see `run_bench`).

**Rule (`run_bench` — k=1 recall measurement over the corpus, the headline number):** for each corpus cell, run
`detect_target` with the cell's vuln input as the target. **`kind` is derived the SAME way production does** —
`mapper`'s kind-inference on the cell file (the path `run` uses), **blind to `canonical.json`'s `class`**. Routing
the matched specialist *because the cell's class is known* is teaching-to-test (inflates recall above what production
catches); leaving `kind` blank under-routes vs production (deflates it). Either way the measured number stops
mirroring production. (Project rule: `teaching-to-test-is-not-validation`.) Then serialize the **VERIFIED bench
`--findings` contract**:

```
{ "<cell_id>": ["<merged group title 1>", "<merged group title 2>", …], … }   # values are PLAIN STRINGS
```

This is the exact shape `bench.cmd_findings` consumes (`bench.py:68–85`): a flat dict of `cell_id → list[str]`, where
`is_flagged` (`bench.py:35–42`) lower-cases each string and substring-matches the cell's `canonical_symbol` OR
`class` — it reads NO `file`/`level`/dict fields, ONLY the string. So `run_bench` emits, per cell, the list of
**merged group titles** (`g["title"]` from `detect_target`'s merged findings — the full finding message, the richest
text the gate produces, not a truncated label).

**Instrument-comparability pin (MEASURED — the deliverable's whole point, do NOT skip).** `run_bench`'s substring
recall is a NEW, reproducible instrument; it is NOT the instrument that produced the historical ~86% floor. That
floor was **hand-judged** on the k=3 union of finding titles+bodies and computed as `(10×1.0 + 7×0.667)/17 = 86%`,
NOT bench-substring-scored (SoT: `docs/validation/2026-06-18-baseline-generalization-recall.md`, which states bench
substring auto-match is used *only where `canonical_symbol` is a literal code symbol*). Therefore: **from DETECT
onward the 99% goal is defined against `run_bench`'s automated substring recall**, and the ~86% floor MUST be
re-baselined through `run_bench` before any comparison — never quote the hand-judged 86% and an automated `run_bench`
number on the same axis (this is the project's own `k3-union-audit-config-is-not-k1-production-rate` doctrine: state
the instrument that produced the number).

**Phrase-canonical no-false-clean (MEASURED hole).** `bench.is_flagged` substring-matches `canonical_symbol`. For
cells whose symbol is a literal code token (`getSchedulingConnectionById`, `touchSession`, `assignee_id`, …) this
scores correctly. For **phrase-canonicals** it underflags — S5 `escape email interpolation` (a catch says "unescaped
{{}} interpolation → XSS"), S7 `webhook signature fail-closed` (the canonical names the FIX direction; a real catch
says "fail-OPEN"), S6 `jiraAdapter baseUrl unguarded fetch` (never a contiguous substring of any finding), S10
`auth-code single-use atomic`. A right-reason catch can substring-miss → recall-0 → a **false clean**. So `run_bench`
MUST classify each cell deterministically: **autoscorable** = `canonical_symbol` occurs as a contiguous
case-insensitive substring of the cell's own vuln source (a real code token); otherwise **phrase**. An autoscorable
cell scores recall normally. A phrase cell that the substring-match misses is recorded in `coverage_incomplete` as
`autoscore-unscorable` (stays hand-judged), **never** counted as recall-0. (Fast-follow: literal-ize the phrase
`canonical_symbol`s so the whole corpus is autoscorable.)

A cell that degrades at the SCAN level (any selected detector error/degraded, merge degraded) is emitted with its
value list present but ALSO recorded in the sibling `coverage_incomplete` set — a degraded cell is
COVERAGE-INCOMPLETE, never silently a recall-0 clean.
Pin (regression-locked by `tests/test_bench.py`): for an autoscorable cell, an empty list ⇒ recall 0 + the
`--require` regression gate fails.

**Rule (`main`):** argparse — `root` (positional, production scan); `--bench` (run `run_bench` over the corpus
instead of a repo scan; loads cells via `bench.load_cells`); `--map <path>` (reuse cached map); `--k` (default
**1**, production; `--k 3` = flakiness audit); `--model` (default sonnet); `--effort` (default medium);
`--config-dir` (blind catch-test); `--max-targets <N>` (budget; default cover all enumerated); `--out-dir <dir>`
(reports + emit JSONs); `--findings-out <path>` (bench-shape JSON, `--bench` mode); `--detectors-root` (override
domains root, hermetic test). Print a summary; write reports/findings to `--out-dir`.

## Data flow

```
mapper.enumerate_surface(root) ─► [(target, kind, why)]  ─prioritize─►  per target:
   detect_target:
     select_detectors(target, kind, registry.load(...))  ─► [baseline, <matched specialists…>]
        per detector ─► subprocess gate.py --detector id --k1 --emit g.json --report g.md
           gate.py: collect_deps ─► oracle per in-scope file ─► k rolls ─► union_rolls ─► within-detector merge
                    ─► --emit: groups+oracle ─► prevent/contract.finding shape ─► g.json
        ◄─ contract JSON per detector
     reconstruct groups ─► semantic_merge.merge_groups (CROSS-detector) ─► per-target merged report + status
   ◄─ per-target {status, findings}
run (repo scan):  3-bucket coverage_map + per-target status + skipped/incomplete ─► run report + per-target findings JSON (keyed by repo path)
run_bench (corpus): per cell ─► detect_target ─► {cell_id: [merged group titles as STRINGS]} ─► bench.py --findings ─► is_flagged substring-match ─► k=1 recall
```

## Error handling — no-false-clean / no-false-coverage (load-bearing)

A target serializes as **clean ONLY** when every selected detector ran (returncode 0, contract `status=="ok"`, no
degraded roll, no `unreliable` oracle, no merge degradation) and produced zero merged findings. Otherwise:
- gate subprocess failed → that target is `degraded` (COVERAGE-INCOMPLETE), surfaced in `coverage_incomplete`.
- LLM roll degraded (empty/401) → gate already routes it to contract `unresolved` → `status=degraded` propagates.
- semantic-merge degraded → findings shown un-deduped + target `degraded` (recall undersold, never oversold).
- target enumerated but over `--max-targets` budget → `enumerated_budget_dropped` bucket, NOT clean.
- detector manifest malformed → `skipped_detectors`, surfaced as reduced coverage.
- detector with no `applies_to` → simply not routed; DETECT claims no coverage for it (honest, not a false clean).

This mirrors the two settled disciplines: `prevent/contract.py`'s status auto-degradation and `mapper.coverage_map`'s
disjoint buckets. DETECT introduces NO new clean-derivation rule — it composes the two existing ones.

## Testing

Deterministic-first (LLM-live is rate-only evidence, never a ship gate — per create-tests `llm-live-selftest-not-a-
ship-gate`). All in `tests/`, run `rtk proxy python3 -m pytest`.

1. **`select_detectors` unit tests (pure, deterministic, no LLM):** fixture registry of manifests → assert routing.
   Cases: baseline always included; kind-match includes the specialist; signal-regex match on content includes it;
   `always:true` includes it; no `applies_to` ⇒ not routed; dedupe baseline; **never narrower** (a target matching N
   specialists routes baseline+N). This is the core new logic — exhaustively unit-tested.
2. **`gate.py --emit` contract-shape test (deterministic):** run `gate.py --dry-run`-style or a stubbed-roll path,
   assert the emitted JSON validates against `prevent/contract.py` (every finding has the frozen keys; `status`
   degrades when `unresolved` non-empty). **Regression lock:** assert a gate run WITHOUT `--emit` produces a
   byte-identical markdown report to a pinned golden (additive-flag proof).
3. **`detect_target` no-false-clean tests (deterministic, gate stubbed):** stub `dispatch_one` to return (a) all-ok
   zero-findings → target `clean`; (b) one detector `error` → target `degraded`, NOT clean; (c) merge degraded →
   `degraded`; (d) findings present → `findings`. Lock that `clean` is unreachable under any degradation.
4. **`run` coverage-map test (deterministic):** small fixture repo → assert 3 buckets disjoint + budget-dropped
   surfaced + skipped detectors surfaced.
5. **`run_bench` autoscore-classification no-false-clean (deterministic, gate stubbed):** stub `detect_target` so its
   merged titles do NOT contain the canonical. (a) Cell with a literal-symbol `canonical_symbol` present in vuln
   source (e.g. `getSchedulingConnectionById`) → classified `autoscorable`; an empty/missing match scores recall 0
   (true miss). (b) Cell with a phrase `canonical_symbol` absent from source (e.g. `escape email interpolation`) and a
   substring-missing finding → recorded in `coverage_incomplete` as `autoscore-unscorable`, **NOT** recall 0. Locks
   the phrase-canonical false-clean closed.
6. **`run_bench` routing-blindness (deterministic, mapper+select stubbed):** set the cell's `canonical["class"]` to
   one class and stub `mapper` kind-inference to return a *different* kind; assert `select_detectors` is called with
   the **mapper-derived kind**, never the canonical class (teaching-to-test guard).
7. **LLM-live recall smoke (rate-only, NOT a gate):** run `detect.py --bench` on ≥1 **autoscorable** corpus cell
   (literal-symbol canonical, e.g. S3 `getSchedulingConnectionById`) with the real registry, k=1, blind
   `--config-dir /tmp/sg_cfg`; confirm the emitted `{cell_id: [titles]}` makes `bench.py --findings` flag the
   canonical. Record as a measured recall data point, never as a pass/fail ship gate.

## Architecture Decisions

(Per brainstorm Phase-2: deletion / single-adapter / seam tests. All four units are NEW; the gate change is additive.)

- **ACCEPTED — `gate.py` invoked as a SUBPROCESS (not path-load `one_roll`).** Deletion test: if DETECT path-loaded
  `one_roll`, it would have to re-wire `collect_deps` + `run_oracle_set` + per-roll `union_rolls` + within-detector
  merge itself — gate's entire internal pipeline scatters into DETECT. Subprocess keeps that complexity behind gate's
  stable CLI. Matches the existing `mapper.dispatch` precedent. (Cost: process spawn per (target,detector) + the
  redundant per-subprocess oracle run — accepted for MVP, listed as a perf Open Question.)
- **ACCEPTED — contract-unification lands as `gate.py --emit` (additive).** Deletion test: without a structured
  emit, DETECT must parse gate's markdown report to cross-merge — fragile string-scraping of a human report (rejected,
  see below). `--emit` gives both arms ONE finding shape (the SoT contract-unification work item) for a small
  additive change. Reversibility: two-way door (a flag; the no-emit path is unchanged).
- **REJECTED — parse gate's markdown report for cross-merge.** Fragile: couples DETECT to report prose formatting;
  any report wording change silently breaks recall. The `--emit` JSON is the robust contract.
- **ACCEPTED — `select_detectors` (the `applies_to` router) lives in `detect.py`, single consumer.** Single-adapter
  test: only DETECT routes by `applies_to`; `prevent/registry.applicable_per_file` routes by `scope_globs`/`triggers`
  (the pre-commit prevent arm) — a genuinely different concern. Building a shared abstraction now would be a decorative
  seam over one caller. Collapse: keep it in `detect.py`; reuse only `registry.load` for discovery. If a second
  `applies_to` consumer appears, promote then.
- **ACCEPTED — `semantic_merge.merge_groups` reused as the ONE merge engine (cross-detector).** Single-adapter test:
  there must be exactly one semantic-merge implementation; gate uses it within-detector, DETECT uses it cross-detector.
  The additive `sev`/`rolls` keys on emitted findings exist precisely so DETECT can reconstruct the `{title,sev,rolls}`
  group shape `merge_groups` already speaks — no second merge.
- **Depth scores:** `select_detectors` = medium (hides the match logic behind a stable list-of-detectors interface);
  `detect_target`/`run` = deep (callers see a status+findings result, not the dispatch/merge/coverage internals);
  `--emit` = shallow-but-additive (a serialization of existing state — correctly shallow, it's a contract adapter).
  Not over-decomposed: 4 units + 1 additive flag for a harness that does map→route→dispatch→merge→cover.
- **REJECTED for MVP — rewrite `mapper` into the orchestrator.** mapper's enumerate half is sound + tested; only its
  dispatch half is superseded. Reuse, don't rewrite (settled upstream).

## Open Questions (deferred / user-gated — DO NOT resolve in this spec)

Deferred (additive, reversible — own follow-up specs):
- **Re-baseline the ~86% floor through `run_bench` + literal-ize phrase `canonical_symbol`s (fast-follow, REQUIRED
  before any 99% comparison).** The hand-judged ~86% and `run_bench`'s automated substring recall are different
  instruments (Component E pin). The fast-follow: (1) literal-ize the phrase canonicals (S5/S6/S7/S10) to real code
  tokens so the whole corpus is autoscorable, then (2) run the bare baseline through `run_bench` to get the
  comparable floor the 99% goal is judged against. Until then phrase cells are `autoscore-unscorable`, not scored.
- **Opus-judgment triage WIDENING.** MVP = deterministic manifest match. An Opus pass that *widens* selection on
  ambiguous targets (never narrows) is the SoT's "JUDGMENT-capable" step — additive on top of the router.
- **Elusive-bug hammer** (re-attack where the surface is rich; the k=3 audit + elusive-set machinery).
- **Repo-level deterministic detectors** (deps/headers, S11) — run once per repo, not per target; a separate
  repo-scoped pass, not part of per-target DETECT.
- **Per-subprocess oracle de-duplication** — the oracle re-runs inside every detector's gate subprocess on the same
  target (correctness-fine, deterministic). Optimize later (run oracle once per target, skip in gate via a flag).
- **Per-finding `line`/`symbol` precision** — parse the finding body for exact location; MVP uses file+message.
- **Cross-detector roll-count fidelity (MINOR).** `detect_target` reconstructs groups as `rolls:set(range(f["rolls"]))`,
  so two detectors that independently agree on one bug union to `{0}` (cardinality 1) — the merged report understates
  agreement as "1 roll". Recall is unaffected (presence is all bench reads), so it is fine for MVP; if the merged
  report ever surfaces a confidence/agreement signal, carry detector provenance separately from fake roll indices.
- **Coarse `cls` on baseline findings (MINOR).** `--emit` sets `cls=<covers joined>`, so baseline findings carry
  `"S1,…,S11"`. Acceptable — bench matches on the message string, not `cls` — but know the class label is coarse;
  refine only if a consumer needs per-finding class.

USER-GATED (surface, do NOT resolve — DETECT is parameterized over whatever detectors exist, so MVP does not depend
on this):
- **Fan-out scope** — committing to all 12 specialists + the **#16 / #36 / oracle-lexicon** priority is a user
  one-way-door decision.
- **Spec-vs-memory conflict to settle (user):** the modular-domain-detectors spec declares "#16 + #22 unified,
  ungated" while project memory records #16 prompt-rewrite as advisor-forbidden-autonomous and the fan-out scope as
  user-gated. These disagree; the user's steer settles it. DETECT does not touch #16, so this does not block the build.
