# Anti-canary coverage gate (goal-item #7) — design

audience: AI coding agents first. Imperative, BLUF. Points to SoT; never re-inlines it.

## BLUF
Build the **anti-canary coverage gate**: the negative-polarity twin of `recall_gate.py` (#5).
It makes the false-raise (GREEN-on-safe / precision) axis a MACHINE check, prompt-hash-bound, k≥3-enforced —
the one conformance axis with no enforcing gate today. Three deliverables, maximally DRY:
1. `domains/security/recall/precision_records.json` — negative-polarity rate records (data; recall-record schema, `metric:"anti-canary-green-rate"`).
2. `check.sh` runs `recall_gate.py --records …/precision_records.json` — reused AS-IS (zero functional change to #5). This is the TEETH.
3. `ledger.py` CONSUMES `recall_gate.run()` over the precision file → upgrades the `discriminator_tested` grade from `ships_safe` presence to a MEASUREMENT grade. GRADED, never a hard gap.

NO new gate file. NO new primitive. NO `bench.py` mode. NO mutant artifacts.

## Naming — disambiguate up front (do NOT conflate)
Goal-item #7 is titled "discriminator-coverage gate" — a MISNOMER. Two distinct negative axes (spec §4):
- **anti-canary** = a *safe* cell; detector must stay GREEN on it (no false-raise). This gate.
- **discriminator-mutant** = rename/cosmetic/polarity/null mutation of a vuln cell (anti-overfit). **0 artifacts exist.**

This gate delivers the **anti-canary** axis. The **discriminator-mutant** axis is a documented rate-pending BLIND_SPOT —
NOT built (YAGNI + done-bar). The gate output MUST state explicitly it does not claim mutant coverage, else it is the
soft false-coverage it exists to prevent.

## Why this gate exists (the bounding justification — keep scope here)
The negative axis is not merely "unenforced" — there is a concrete, PROVEN regression path it closes:

> A prompt edit can FIX recall while REGRESSING precision, re-roll recall to green, and SHIP — with the
> false-raise regression invisible to every gate.

`recall_gate.py` STALEs the *recall* rate on any prompt edit (sha256-binding), but nothing forces re-measuring
*precision*. The **comment-FP cluster** (CLAUDE.md "Measured state"; `docs/validation/2026-06-19-citation-prompt-recall.md`)
is this regression made real, not hypothetical. This gate closes it: precision records get the SAME prompt-hash STALE,
so a prompt edit STALEs BOTH `records.json` AND `precision_records.json` → `check.sh` reds until BOTH axes are re-rolled.

**This teeth claim is VERIFIED, not assumed:** `recall_gate.py` is ALREADY aggregated in `check.sh` (`check.sh:25` — `run "recall-gate" python3 recall_gate.py`). The recall-side STALE teeth already fire there. The precision line is the SECOND recall_gate invocation in `check.sh` (same binary, `--records` pointed at the precision file) — symmetric with the recall line, not a first-of-kind. (NOTE: CLAUDE.md's "Run it" section lists check.sh as 3 gates — STALE; the actual file runs 5: pytest, `ledger.py --check`, `bench.py --routing`, `bench.py --solutions`, `recall_gate.py`. Out of scope to fix CLAUDE.md here; flagged for a later doc pass.)

**The teeth are STALE-on-drift, not a new behavioral runner, not a floor.** Bound scope to that.

## Scope — in / out
IN: the anti-canary (GREEN-on-safe / precision) RATE axis for LLM detectors, recorded + validated + drift-bound, with the coverage surfaced in the ledger.

OUT (each with its deletion-test reason — do NOT build):
- **Deterministic-detector anti-canary** — already pytest-covered: `tests/test_s11_band3.py::test_deps_red_flags_canonical_green_silent` (`assert not any(sym in green)`), `::test_headers_red_flags_canonical_green_silent` (`assert green == []`), `tests/test_oracle_xfile.py::test_green_caller_resolves_fixed_helper_and_oracle_silent_on_canonical`. A re-runner = redundant (deletion test fails). Also: oracle ships a DOCUMENTED residual FP (`referee|referrer` flagged in BOTH vuln+safe, excluded by the specific-pair canonical_symbol — `domains/security/detectors/oracle/cells/canonical.json`). A naive "GREEN-on-safe for all oracle output" gate would FALSE-RED. Deterministic anti-canary belongs in pytest (encodes the expected residual), NEVER this gate.
- **A `bench.py --discriminators` mode** — the goal-memory guess. recall_gate already provides the validator; ledger already provides the coverage oracle. A bench mode would duplicate both (deletion test fails).
- **Discriminator-mutant artifacts** — 0 exist; building 19 = out of done-bar + YAGNI. Surface as rate-pending BLIND_SPOT.
- **Live LLM rolls in CI** — forbidden (deterministic-CI doctrine; `check.sh` gates are all deterministic). Anti-canary rates come from RECORDED rolls only, exactly like recall.
- **A hard precision FLOOR** — report-only + per-record waiver (mirror recall_gate). An absolute floor would wrongly red a cell with a legitimate documented imprecision (same reason S2-role 3/9 ships waived for recall). Teeth = STALE, not floor.

## The contract being locked — the negative-polarity record (LOCK THIS; advisor reviews before cursor builds)
Identical to a recall record (`recall_gate.py` REQUIRED = `cell_id, detector, catches, k, measured_against, metric`), reinterpreted by polarity:

| field | recall record | anti-canary (precision) record |
|---|---|---|
| `cell_id` | corpus cell | SAME corpus cell (the cell whose `safe` artifact was rolled) |
| `detector` | e.g. `baseline` | SAME |
| `catches` | rolls that CAUGHT the canary on vuln | rolls that stayed GREEN (did NOT re-raise the canonical) on safe |
| `k` | rolls on vuln | rolls on safe |
| `measured_against` | sha256(current prompt) | SAME sha256 — this binding is the STALE teeth |
| `metric` | e.g. `per-roll-union` | **`anti-canary-green-rate`** ← the polarity discriminant |
| `waiver` (optional) | `{below_floor_ok, reason}` | SAME semantics |

- The discriminant is the **`metric` value** + the **file the record lives in** (`precision_records.json`). NO new schema, NO new field beyond the metric label. `recall_gate.evaluate_record` is metric-agnostic → validates these IDENTICALLY (k≥3 hard, STALE hard, MALFORMED hard, floor report-only, waiver). Confirmed by reading `recall_gate.py:108-144` + the `--records` arg (`recall_gate.py:189`).
- Schema string: reuse `"recall-records/v1"` (the file is a records bag; the `metric` value carries polarity). Wrap as `{"schema":"recall-records/v1","records":[…]}` like `records.json`.
- **CONSTRAINT — `cell_id` MUST name a CORPUS cell, never a detector cell.** `recall_gate.load_corpus` enumerates `corpus/*` only (not `domains/*/detectors/*/cells/`). A precision record naming a detector cell → `UNKNOWN_CELL` → HARD red. The seed (S2 team-owner corpus cell) satisfies this. The live-fire zync stubs (`cardcom.ts`/`stripe.ts`, the comment-FP precision evidence) are NOT corpus cells → they CANNOT be seeded as records (would hard-fail); they stay documented in the validation doc only. Document this constraint inline in `precision_records.json` (a leading `_comment` field) so a future detector-cell precision record does not surprise-red.

## Seed data — faithful k≥3 transcription ONLY (no fabrication)
Source: `docs/validation/2026-06-19-citation-prompt-recall.md` "Discriminator — 17/17 safe PASS".
- **BUILD-PHASE FIRST STEP — check the raw sweep emit JSONs, do NOT anchor on the doc prose.** The seed FULLNESS is the marginal protective value of #7 over the existing recall gate: with only S2 (n=1), a prompt edit already reds `check.sh` from the 16 stale recall records, so precision adds exactly 1 cell of obligation. If the recorded k=3 sweep emit JSONs (the per-roll safe-cell outputs behind the "17/17" adjudication) captured safe rolls PER CELL at k≥3, transcribe EVERY such cell faithfully from the MEASUREMENT — turning the token seed into corpus-wide teeth. Locate the emit JSONs the validation doc was generated from (search the validation/sweep artifacts; the cursor task pins the path). Do NOT re-roll (done-bar forbids new rolls) — seed only what was ALREADY rolled k≥3. If safe was rolled k=1 (e.g. S2 escalated to 3/3 only because it was contested), n=1 is honest and stands.
- Transcribe ONLY cells with an **explicit numeric k≥3** anti-canary count. The doc gives **S2 team-owner safe = 3/3** explicitly (`catches:3,k:3`). The bulk "17/17 PASS" is AGGREGATE adjudication — recording those as k=3 would FABRICATE the roll count (forbidden; recall_gate HARD-fails k<3 anyway). Include any cell with an explicit numeric k≥3 count (from the emit JSONs OR the doc); otherwise leave it out.
- `measured_against` = `2154ff82435c60781c46a6c91fc29f9b9a8acc62e87a79306a4ad37cc0a84886` (current `baseline.prompt.txt` sha256 — VERIFIED identical to records.json's hash; the discriminator was measured on the same shipped citation prompt → born hash-current, NOT STALE). Cursor MUST recompute and confirm equality before committing.
- Resolve the exact `cell_id` from `domains/security/corpus/*/canonical.json` (class==S2, team-owner role-modification defect) and cross-check it appears in `records.json`. Do NOT guess the id (recall_gate UNKNOWN_CELLs a wrong id → hard fail).
- Every cell WITHOUT a faithful k≥3 record → NO record → surfaced by the ledger as `present` (safe cell ships, rate unmeasured) — a documented rate-pending gap, the no-false-coverage truth. This is EXPECTED and correct, not a failure.

## Component design

### 1. `precision_records.json` (data)
Path `domains/security/recall/precision_records.json`. Holds the seed above. Plain data — earns its place: ≥1 k≥3 record = real seed + real teeth.

### 2. `check.sh` — the teeth (reuse recall_gate AS-IS)
Add a line invoking `python3 recall_gate.py --records domains/security/recall/precision_records.json`.
- HARD exit 1 on STALE / SINGLE_RUN(k<3) / MALFORMED / UNKNOWN_CELL of any precision record (the drift teeth).
- BELOW_FLOOR / UNMEASURED stay report-only (exit 0).
- ZERO functional change to `recall_gate.py`. OPTIONAL cosmetic only: a `--label` arg defaulting to `recall` so the printed header reads `precision-rate gate` when pointed at the precision file. If added, it is the SOLE touch to #5's file and is purely cosmetic (no logic). Prefer omitting it unless the recall-flavored header is genuinely misleading in CI output.

### 3. `ledger.py` — consume recall_gate, upgrade the grade (GRADED, not a gap)
`ledger.py` already grades `discriminator_tested = ships_safe(cls)` (`ledger.py:188`) — PRESENCE only ("a safe file exists"). That is the shallow soft-false-coverage the advisor flagged: it renders `discrim=y` for classes that have NO measured anti-canary rate.

Upgrade it to a MEASUREMENT grade by CONSUMING recall_gate's derived set (the reconcile #5's author explicitly deferred to cursor — `recall_gate.py:38-41`):
- `import recall_gate`; call `recall_gate.run(PRECISION_RECORDS, DEF_CORPUS, DEF_DETECTORS, floor, strict_floor=False)` (pure, no printing — `recall_gate.py:147`). Take `result["rate_validated"]` — the per-record DETAIL list (each `{cell_id, rate, k, metric, status, waived}`). RATIONALE: line 95's rate-surfacing constraint needs `rate`/`k`; the bare-id `rate_validated_cells` cannot carry them. Guard the missing-file shape with `.get("rate_validated", [])` (the key is absent when the file is missing).
- New `precision_grade(cls, cells_by_cls, prec_by_cell, ships_safe_fn)` — tri-state, mirrors `resolution_grade` (`ledger.py:84`), where `prec_by_cell = {d["cell_id"]: d for d in rate_validated}`:
  - `"measured"` — some cell of `cls` ∈ `prec_by_cell` (a k≥3, hash-current anti-canary record exists).
  - `"present"` — `ships_safe(cls)` true but not measured (safe cell exists, rate unmeasured = rate-pending).
  - `"none"` — no safe cell.
- Replace the `discriminator_tested` boolean in the class grade (`ledger.py:188`) with this tri-state. Update the `_md` `discrim` column (`ledger.py:230`) to render the word (`measured`/`present`/`none`) instead of `y`/`-`.
- **CONSTRAINT — `measured` MUST NOT mask a below-floor rate.** `recall_gate`'s `rate_validated_cells` INCLUDES `BELOW_FLOOR` cells (that status is not in `HARD` — floor is report-only by design). So a sub-floor anti-canary cell would still grade `measured` and read green. The render MUST surface the actual rate so a failing rate is visible — render `measured 3/3` (the rate string) in the `discrim` column, NOT a bare `measured`. (For the 3/3 seed this is moot, but the schema must not hide a future regression.) Source the rate from the validated record's `catches/k`; do NOT add a floor gate (teeth stay STALE-only).
- Add ONE explicit line to the registry `--md` output (and the JSON summary `note`): the discriminator-MUTANT axis is unbuilt (0 artifacts, rate-pending) — the gate does NOT claim mutant coverage. Do NOT add a uniformly-`none` per-class mutant column (YAGNI).
- **GRADED, never a gap.** `precision_grade` MUST NOT append to `gaps[]`. `ledger.py --check` stays green (mirrors `resolution_grade`'s "GRADED, never a CI failure"). The hard teeth live in `check.sh`'s recall_gate invocation, not here.

## Conformance / done-bar (user-locked)
Real completion for this measurement-blocked item =
1. mechanism built (the 3 deliverables);
2. conformance-green on AVAILABLE cells (the seeded k≥3 record(s) validate; recall_gate exit 0 on the precision file);
3. documented rate-pending gap (every unseeded cell surfaced `present`; mutant axis surfaced unbuilt).
NEVER a k≥3 anti-canary RATE *claim* over the corpus this session — the rate is pending real anti-canary rolls.

Green-bar: `check.sh` exit 0 — all 5 EXISTING gates (pytest + `ledger.py --check` + `bench.py --routing` + `bench.py --solutions` + `recall_gate.py`) PLUS the new recall_gate-on-precision line, all green. New pytest for the ledger precision_grade (measured/present/none on a fixture). recall_gate's OWN test suite untouched (zero functional change).

## Architecture decisions (deletion test on every new piece)
- `precision_records.json` — DATA, not a primitive. Earns its place: ≥1 k≥3 record = seed + teeth. KEEP.
- `check.sh` recall_gate-on-precision line — WIRING. Delete → lose the STALE teeth on the negative axis. KEEP.
- `precision_grade` in ledger — EXTENDS the existing coverage oracle; consumes recall_gate's set (no logic duplication). Delete → lose the no-false-coverage visibility on the negative axis + revert to the shallow presence grade. KEEP.
- NEW gate file / NEW primitive / `recall_gate` rewrite / `bench` mode — REJECTED, all fail the deletion test (recall_gate + ledger already supply validator + oracle). Maximally DRY: the only NEW code is `precision_grade` (~10 lines) + its render; everything else is data + wiring + reuse.

## SoT pointers (read before changing structural anything)
- `docs/specs/2026-06-17-security-gate-design.md` §4 (admission gate: canary/anti-canary/discriminator), §2/§6 (statistical validation, no-false-coverage).
- `docs/ARCHITECTURE.md` §5 (registry = derived coverage oracle; FAIL CI on a structural gap), §4 (≥2-consumer primitive gate).
- `recall_gate.py` (the reused validator; `run()` pure, `--records` parameterized, metric-agnostic).
- `ledger.py` (`build`/`resolution_grade`/`ships_safe`/`_md`).
- `domains/security/recall/records.json` (the positive-axis sibling — mirror its file shape).
