gates: give two of them a margin, and make the probe see components

The probe printed one blended number per policy, so a component pinned at 0.000
across every rung was invisible. Four gates hid there for a month. It now reports
floor / best-below-oracle / oracle / ceiling for 25 components across 8
environments, and a component flat across every rung is fatal.

bot-detection GATE_SLACK = 1 — fires 5/32 on the real rollouts, was 0/32. The
max(1, reference_caught - SLACK) guard is verified by construction, not by
sampling: without it the required count reaches 0 and an EMPTY accusation list
clears the gate. Observed reference_caught is 4-6, so no amount of sampling would
have found that hole.

schema-migration GATE_MARGIN = 0.15 — fires 4/32, was 0/32. The cost is disclosed
and bounded: the naive split clears it on 5.5% of 1,000 unseen seeds, fenced by
an assert at 10%. Margin 0.10 keeps the leak at zero and fires 0/32, i.e. stays
dead. A live gradient with a bounded leak beats a clean corpse.

redaction-pressure is NOT given a margin, and that is the result rather than a
failure. The only setting that fires at all leaves half the secrets standing and
pays a four-of-seven ruleset on five seeds in six — a margin that pays for
inaction is strictly worse than a dead gate. Recall maxes at 0.852 and no rollout
ever cleared both clauses in one episode. It is genuinely hard, not miscalibrated.

⚠️ The per-component check did not catch the defect it was built for. Reverting
schema-migration's margin to 0.0 — restoring the exact dead gate — printed ok and
exited 0, because the near-oracle rung scrapes the unmargined gate on ~2 seeds in
24 and that kept best<oracle non-zero. Every assertion bounded how much a margin
may PAY; none noticed if it stopped existing. migration() now carries the mirror
of bot-detection's guard, and reverting the margin fails with "the margin is dead
and the component carries no gradient between the crude answer and the exact one".

canary-trap's oracle-minus-one rung is documented as degenerate rather than
quietly relied on: it is identical to the oracle to four decimals, so it measures
specificity and gate at the ceiling, not mid-ladder as its comment claimed.

The CI lock policy asks git instead of the disk. It was checking the working
tree, where a lock file is a normal by-product of uv sync, so it passed in a clean
checkout and failed on every machine that had run an eval.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 23:14:05 -07:00
co-authored by Claude Opus 5
parent 122a858b61
commit 3305be4ff7
12 changed files with 1107 additions and 95 deletions
@@ -158,6 +158,33 @@ class Outcome:
@property
def clean(self) -> bool:
"""Every secret gone, nothing else touched. Six clauses, all exact, deliberately.
⚠️ This gate scored 0.000 mean and 0.000 max over the 32 rollouts in
`outputs/run-20260821-1401`, and unlike `bot-detection`'s and `schema-migration`'s
it was swept and **left alone**. The sweep is here so it is not redone:
gate fires on 32 real rollouts `plausible`
exact (shipped) 0/32 = 0.000 0.000
recall margin 0.10, collateral 0, innocent 0 0/32 = 0.000 0.000
recall margin 0.20, collateral 1, innocent 32 0/32 = 0.000 0.000
recall margin 0.50, collateral 2, innocent 160 3/32 = 0.094 0.833
Every margin loose enough to fire on the measured population pays `plausible` — a
ruleset carrying four of the seven rules, which is to say one that never found three
of the secret types — on five seeds in six. Every margin tight enough to keep
`plausible` at zero fires on none of the 32. There is no setting that does both,
because the failure is joint and not a threshold: recall maxed at 0.852 with a
minimum of four secrets left standing, `collateral_hits == 0` held on 6 of 32, and
no rollout managed high recall and zero collateral at the same time.
So this one is (c) genuinely hard, not (a) mis-thresholded, and a margin here would
buy nothing measurable in exchange for redefining what "redacted" means in a
redaction environment. Half the secrets left in the document is not a near miss.
`probe.py` reports the component as a step at the oracle and names it; that is the
honest state, and the fix when it comes is a harder model or an easier corpus, not
a looser gate. Measured by `tools/regate.py` and `tools/regate_ladder.py`.
"""
return (
self.removed_secrets == self.secrets_total
and self.partial_secrets == 0