# Bug Registry + Scan Primitives — Design

> audience: AI coding agents first. Imperative, BLUF-ordered. **Status:** design (brainstorm→plan→sdd).
> Points to the SoT, never re-inlines it: `docs/handoffs/2026-06-19-session-handoff.md` (THE GOAL + FIRST TASK),
> `docs/ARCHITECTURE.md` (§5 the registry; §"executable resolution gated HARDER"; the ≥2-consumer primitive gate),
> `CLAUDE.md` (band ladder, validation discipline). Vocabulary: testing-native ONLY.

## BLUF
The "100% bug registry" is **not a new store** — it is `ledger.py`'s existing **derived** projection
(ARCHITECTURE §5), EXTENDED to render every defect's full lifecycle: **Discovered → Re-discovery → Resolution →
Prevention**. The minimal primitive set adds **exactly one** new executable primitive — **`SolutionAdapter`**
(pluggable fix strategy). Everything else **collapses** into existing pieces (Detector, cell, `confirmed.json`,
the ledger projection) by the deletion + single-adapter test. Hot-path stays **Python** with language-agnostic
`exec`+JSON contracts (port a specific detector to Rust only on measured need). Proven end-to-end on **S4 (SQLi)**:
drop in a cell + a solution manifest → the ledger renders the full lifecycle with **no engine edit** —
*"add a bug = register, not rewrite."*

## Decisions locked (clarifying Qs, 2026-06-20)
- **Scope:** design + advisor-lock the contracts + EXTEND `ledger.py` to the lifecycle view + **build the first
  real `SolutionAdapter` end-to-end on S4**.
- **Hot-path language:** Python now; contracts stay language-agnostic (JSON manifest + subprocess `exec`, already
  the convention in `prevent/contract.py`/`registry.py`); port a *specific* hot detector to Rust only if a real
  pre-write hook is measured too slow. The measured 16s prevent latency is the `pnpm audit` subprocess (Rust would
  not speed it), not hook overhead.

## The lifecycle — one registry row per defect
1. **Discovered** — provenance (audit / `fix_sha` / discovered_by / when / finding_ref) + the corpus **cell**.
2. **Re-discovery guarantee** — responsible **detector(s)** + **band** + measured **recall** (recall stays in
   `bench`; the registry references it, never re-measures — `ledger.py` derives set membership only).
3. **Resolution** — one-or-more **`SolutionAdapter`s** (pluggable fix strategies).
4. **Prevention** — a programmatic PREVENT **detector** bound to a surface + the **ratchet**; backup = an LLM-rule
   reference (a coding-agent directive, for classes not yet structurally expressible).

## Primitive-set decision — the deletion / single-adapter test (the core design work)
A field earns its own primitive ONLY if **≥2 concrete adapters** need it (ARCHITECTURE primitive gate); else it is
plain data on an existing primitive.

| Candidate | Verdict | Why |
|---|---|---|
| **Registry index / RegistryEntry** | **COLLAPSE** → the derived `ledger.py` row | `ledger.py` IS the §5 registry; doctrine FORBIDS a hand-maintained parallel store ("DERIVES from the distributed SoT, NEVER hand-maintains one"). A `RegistryEntry` class adds nothing over the derived dict. |
| **ProvenanceRecord** | **COLLAPSE** → `cell.provenance` sub-schema (plain data) | One real consumer (ledger dedup key). Deletion test: it is just data on the cell. `ledger._repo_of` already reads `cell.provenance.repo`. |
| **PreventionAdapter** | **COLLAPSE** → existing Detector + plain `prevention` ref | Programmatic prevention = the existing prevent **Detector** (trigger+glob+ratchet). Surface routing already exists DETECT-side (`detect.py:select_detectors` via `applies_to`). The LLM-rule backup is a *doc pointer* (data), not executable. No new executable interface earns existence. |
| **Detector** | **KEEP** (exists) — unchanged this cycle | `contract.py` + `registry.py` + `runner.py`. Surface/prevention bindings already present; no S4-driven change needed. |
| **`SolutionAdapter`** | **KEEP** — the ONE new primitive | ≥2 plausible adapters per class (S4: parameterize / escape / ORM-builder); hides real complexity behind a stable `propose(cell\|finding) → Resolution`; conformance-testable (vuln→fix/suggestion, safe→quiet). |

## Contracts to lock (advisor-gate)

### `SolutionAdapter` (NEW — the only new executable primitive)
- **Drop-in dir** `domains/<domain>/solutions/<id>/` + **`solution.json`** manifest, discovered by glob
  `domains/*/solutions/*/solution.json` (mirrors the detector convention). **Reuse is partial — state it honestly:**
  `prevent/registry.py:load()` is hardcoded to `domains/*/detectors/*/detector.json`, so solutions need it
  generalized to `load(root, kind="detector"|"solution")` (or a sibling `load_solutions`) — NOT free. The
  `runner.py` subprocess exec path + per-detector timeout DO reuse as-is (same `exec`+stdin-JSON contract).
  `bench.load_cells()` globs corpus + `detectors/*/cells` only — it will **not** discover a solution's conformance
  cells, so solution-conformance is its **own** small harness (`tests/test_solution_*`), not free reuse. Manifest:
  ```json
  {"id":"sql-parameterize","class":"S4","strategy":"parameterize-query",
   "applies_to":{"class":"S4","signal":"sql\\.raw|\\$\\{"},
   "exec":["python3","domains/security/solutions/sql-parameterize/solve.py"],
   "rung":"located-suggestion","kind":"semantic"}
  ```
- **`exec` convention** identical to detectors. stdout = a **Resolution**.
- **Input contract — CONSUME the finding, NEVER re-detect (load-bearing — this is what makes it an adapter, not a
  lookup table).** stdin JSON `{finding, file_content}` where `finding` is the Re-discovery output (file + symbol +
  class + line). The adapter reads the located sink from `file_content` at `finding`'s symbol and emits the
  *transformation* for THAT sink. It MUST NOT re-scan to find the bug — detection is the detector's job; an adapter
  that re-detects silently re-implements detection and fails the single-adapter test. (This is also what makes
  "apply to SAFE cell → quiet" meaningful: given the canonical finding against already-fixed `file_content`, the
  adapter sees the sink is already parameterized and **abstains**.)
- **New emitter in `contract.py`:** `resolution(adapter_id, cls, rung, status, location{file,line,symbol},
  suggestion, patch=None, unresolved=())`. **no-false-clean:** cannot-safely-determine → `status="degraded"` +
  suggestion-only, **NEVER** a `patch`. (Disambiguation: `contract.resolution()` = a FIX proposal; unrelated to
  `orchestrator/resolver.py`, which is band-2 cross-file *import* resolution — different meaning of "resolve".)
- **GATING (ARCHITECTURE §"resolution gated HARDER"):** auto-apply ONLY `rung=="syntactic-local"` AND
  `status=="ok"`. `rung=="located-suggestion"` emits a located suggestion the agent/human applies — never a silent
  rewrite. A wrong autofix writes insecure code AND false assurance — strictly worse than no fix.
- **ADMISSION — conformance asserts SPECIFICITY, not generic advice (the deep-vs-lookup-table proof):** ships cells.
  Apply to the class **VULN** cell → reaches the **SAFE** cell (syntactic rung: AST/byte-equivalent) OR emits a
  located suggestion that names **the concrete replacement shape** — for S4, `inArray(kbArticles.spaceId,
  spaceIds)` / bound placeholders, NOT merely "parameterize this query". Apply to the **SAFE** cell → quiet /
  abstain (idempotent — no double-fix). **Negative test:** a non-matching finding (wrong class / unrelated symbol)
  → nothing emitted. Generic-advice-passes or fires-on-anything = the adapter did NOT earn its primitive; demote
  `resolution` to plain row data (as `prevention` was). Ships only if green.

### `cell.provenance` (EXTEND `canonical.json` — Discovered; plain data, not a type)
Optional `provenance`: `{audit, fix_sha, discovered_by, discovered_when, finding_ref}`. Backward-compatible (absent
→ unknown, never a false claim). `ledger._repo_of` already reads `provenance.repo`; extend it to surface the full
Discovered column.

### Registry = `ledger.py` extension (derived, never stored)
- Add `solution_classes(d)` (mirror `detector_classes`) over `solution.json` manifests; grade **Resolution** per
  defect: `auto` (a syntactic adapter covers the class) | `suggested` (a located adapter covers it) | `none`.
  This replaces today's placeholder string `"requires-resolution" / "n/a (no SolutionAdapter)"` (ledger.py:121).
- Add **Discovered** (provenance) + **Resolution** columns to the one-row-per-defect projection + the `--md`
  render. The **Prevention** column already derives from `ratcheted` (`confirmed.json`) + responsible detector(s).
- **CI `--check` policy:** Resolution is **GRADED, not a hard gap** — a defect with no `SolutionAdapter` surfaces
  `"none"`, never exit-1 (consistent with ledger's grade-don't-erase discipline). *(Open for advisor: should a
  ratcheted class with NO resolution be a soft warn? Default: graded only.)*

## Data flow
`cells` (corpus + `detectors/*/cells`) + detector manifests + `confirmed.json` + **solution manifests** →
`ledger.build()` → lifecycle projection (Discovered | Re-discovery | Resolution | Prevention) + gaps. Adding a
defect touches **no** detector/runner/engine code — drop a cell (+ optional provenance) + a solution manifest; the
ledger derives the rest.

## Worked proof — S4 (the "add-a-bug = register" property)
- **Existing cell:** `domains/security/corpus/S4-kb-spaceids-sqli` — `listArticlesBySpaceIds` interpolates
  `spaceIds` into a raw `sql.raw` `ARRAY[...]::uuid[]` literal (`'${id}'`) → SQLi; fix = parameterized
  `inArray(kbArticles.spaceId, spaceIds)` (`fix_sha 9b0c485`).
- **Add:** `domains/security/solutions/sql-parameterize/{solution.json, solve.py}` + its cells. `rung =
  located-suggestion` (which interpolations are user-controlled is semantic). `propose(vuln)` → suggestion naming
  `listArticlesBySpaceIds` + the `sql.raw` sink: "replace raw interpolation with a parameterized `inArray` /
  bound placeholders". `propose(safe)` → quiet.
- **Result — the full row, including the HONEST empties (no-false-coverage in the worked example itself):**
  Discovered ✓ (provenance + cell) | Re-discovery = **band-1 only** (S4 is a `BLIND_SPOT`, baseline-floor —
  no specialist) | Resolution = **suggested** | Prevention = **none** (band-1 only, no S4 ratchet entry —
  `confirmed.json` holds only an unrelated S9 entry `#44` → S4 NOT ratcheted). A clean-looking row that hid the
  missing Prevention would itself be a false-coverage claim.
- **The edit boundary (one-time vs per-bug):** `ledger.py` takes a **single foundational extension now**
  (`solution_classes` + the Discovered/Resolution columns). *Thereafter* adding a defect is **register-only,
  zero-engine-edit** — drop a cell + a solution manifest, no `ledger`/runner/registry/gate change. S4 exercises
  the per-bug property after that one-time change; the two must not be conflated.

## Error handling
no-false-clean throughout: a `SolutionAdapter` that cannot safely fix emits located-suggestion + `degraded`, never
a `patch`. Executable resolution is gated harder than detection. `ledger.py` keeps grade-don't-erase (a missing
resolution is surfaced, never hidden).

## Testing
- **`SolutionAdapter` conformance (admission):** vuln→fix/suggestion right-reason (names THIS sink), safe→quiet,
  idempotent. (in the solution's `cells/` + `tests/`.)
- **`contract.resolution()` shape:** no-false-clean — cannot-determine → suggestion-only, never a `patch`.
- **ledger lifecycle projection:** S4 row shows Discovered + Resolution; **add-a-bug=register E2E** — drop the
  manifest → projection updates with no engine edit.
- **`check.sh`:** `ledger.py --check` stays green (Resolution graded, introduces no new hard gap).

## Architecture Decisions (collapses accepted / rejected)
- **ACCEPTED collapse** — `RegistryEntry` → derived ledger row (deletion test: nothing scatters);
  `ProvenanceRecord` → `cell.provenance` data (single-adapter: one consumer); `PreventionAdapter` → existing
  Detector + ratchet + plain `prevention` ref (no new executable interface earns existence).
- **KEPT new** — `SolutionAdapter` (single-adapter PASSES: ≥2 fix strategies per class; **deep** — callers cannot
  tell how a fix is derived; internals replaceable).
- **Resolution grade is ADMISSION-GATED (no-false-coverage, mirrors the detector side)** — a `suggested`/`auto`
  grade is a coverage claim, so a solution declaring a `rung` MUST ship its exec script AND a `cells/` conformance
  artifact, or `ledger.py --check` fails (`solution-no-conformance` gap). Without this, a bare `solution.json` with
  no `solve.py` would grade `suggested` and `check.sh` would stay green — the exact blind spot the ledger exists to
  prevent, in the new column. **Existence-gated structurally** (like detector `ships_safe`) — `--check` proves the
  conformance artifact EXISTS, not that it is GREEN. Behavioral conformance rests on the solution's own pytest, which
  is by-convention: `check.sh` does not yet auto-run a solution over its `cells/`. So a future solution with a trivial
  `solve.py` + an empty `cells/x.json` + no test would still grade `suggested` undetected — NARROWED by the existence
  gate, not closed. FOLLOW-UP (named, not built — parity-with-detectors, no PoC scope creep): a behavioral
  solution-conformance gate (`bench --solutions`, mirroring `bench --routing` which RUNS each detector over its cells)
  that executes every declared-`rung` solution over its `cells/` and fails on a non-right-reason result.
- **KEPT unchanged** — the Detector contract (no S4-driven change; surface binding already exists DETECT-side).
- **Rust REJECTED for now** — YAGNI; measured latency is the audit subprocess, not hook overhead. Contracts kept
  language-agnostic so a specific hot detector ports later as a swap, not a rewrite.

## Vocabulary
testing-native ONLY: detector / domain / corpus / cell / band / oracle / roll / recall; **adapter** (licensed) —
`SolutionAdapter` is an adapter. NEVER module / package / layer / tier / seam / plugin.
