Skip to content

fix: regression gate reported exit 0 on real regressions (#134) - #140

Open
kiki830621 wants to merge 3 commits into
mainfrom
idd/134-gate-fail-open
Open

fix: regression gate reported exit 0 on real regressions (#134)#140
kiki830621 wants to merge 3 commits into
mainfrom
idd/134-gate-fail-open

Conversation

@kiki830621

@kiki830621 kiki830621 commented Aug 2, 2026

Copy link
Copy Markdown
Member

The release gate said pass when it should have said fail. That is fail-open — categorically worse than a noisy log, because a gate that produces the reassuring output of a present gate while being absent is worse than no gate at all: everything downstream of it was trusting a verdict that could not fail.

Refs #134

Reproduced before fixing

All three printed ✓ regression gate: all 1 corpora within tolerance and exited 0, with a 0.99 measurement against a 0.05 golden:

golden: NaN      → ✓ c1 [en] wer: golden nan → measured 0.9900 (+nan)        EXIT=0
golden: "NaN"    → ✓ c1 [en] wer: golden nan → measured 0.9900 (+nan)        EXIT=0
tolerance: 1e308 → ✓ c1 [en] wer: golden 0.0500 → measured 0.9900 (+0.9400)  EXIT=0

The third line is the one worth staring at. It contains no unusual number and is byte-for-byte indistinguishable from a genuine pass — because the threshold it was judged against was never printed.

Ten paths, found over three rounds

Round 1 fixed the three above plus four more. Two verify rounds then found that the fix had closed one handle of a two-handled lever, and that the empty-set guard had closed only cardinality zero.

Non-finite numbers. json.load accepts bare NaN / Infinity, and every comparison against NaN is false. Validation happens after float(), the only place it works: {"golden": "NaN"} is a legal JSON string, so parse_constant never fires while float("NaN") still yields nan.

An unbounded tolerance. Needs no unusual number and defeats every existing defence. Capped at 0.25.

An inflated golden bought exactly the slack a wide tolerance was refused. The decision is actual - golden > tol, so the gate passes anything at or below golden + tolerance — that sum is the effective threshold and the two fields enter it identically. A golden of 2.0 with an honest tolerance of 0.02 admitted every physically possible error rate, and the tolerance newly rendered on the pass line read a reassuring 0.0200, because the number that had moved was not the one being shown. The bound is now on the sum (≤ 0.5 against a committed maximum of 0.1749).

A truncated comparison still reported success. Both sides of the comparison derive from the same baseline.json, so deleting entries shrinks the baseline, the work list and the measured set together and the gate prints all 1 corpora within tolerance — by pure deletion, no unusual value anywhere. The expected corpus set is now pinned in benchmarks/baseline-meta.json and is an invariant of the gate: for the repo's own baseline a missing or malformed meta is fatal, not a warning.

Also closed: a run that compared nothing, a measurement judged against a different metric's golden, a repeated JSON key hiding the real threshold, a boolean laundered into a measurement (including false, which laundered into a perfect score), and OverflowError escaping the numeric converter.

Bounds

field bound why this direction
golden + tolerance (0, 0.5] the effective threshold — the only bound invariant to trading one field against the other. Committed maximum 0.1749, so 2.9× headroom
tolerance (0, 0.25] does independent work below the sum bound: golden 0.1 + tolerance 0.3 sums to 0.4 and clears it, but a 30-point tolerance applies whatever the measurement turns out to be
error_rate [0, 100] governs the measured side, which the sum bound never touches. A large measurement correctly fails, so this is a garbage filter, not a security boundary

golden has no separate constant. The sum bound forces golden < 0.5, so any ceiling above that can never fire — mutation testing put the old GOLDEN_MAX = 2.0 at 0 failed assertions before and after. It was removed rather than propped up with a test written to justify it. (Its stated rationale was wrong as well as inert: "CER can exceed 1.0 via insertions" describes a measurement, which ERROR_RATE_MAX governs.)

Passing lines now show their threshold

before:  ✓ c1 [en] wer: golden 0.0500 → measured 0.0600 (+0.0100)
after:   ✓ c1 [en] wer: golden 0.0500 → measured 0.0600 (+0.0100 ≤ tolerance 0.0200)

Auditing a pass is what this log is for, and the single number capable of exposing a rigged pass appeared only on failing lines. The same argument later applied to the completeness anchor, which now prints completeness: 12 corpora pinned by … — verified instead of being observable only by the absence of its warning.

Stated plainly, since the point is that the prose must not flatter the gate: the tolerance ceiling and the comparison are both inclusive, so a regression of exactly 0.25 against a golden of 0 does pass. The bound refuses the absurd values; the rendered tolerance exposes the merely-too-lenient ones.

One correction to the issue

#134 warns that this conflicts with #132's byte-identical regression lock. It does not — that lock (RegressionWorklistTests.pinnedWorklist) pins the worklist stage's TSV, a different script and a different stage. Verified by exhaustion: the only exact-stdout assertions in the repo target baseline-worklist.py.

Tests

474 tests / 88 suites green (+29 over main).

Non-vacuousness by mutation — each guard deleted independently, counting failed assertions. Re-runnable: ./scripts/mutation-check.sh, committed with the change, which restores the tree and fails if anything scores zero.

guard assertions
effective-threshold bound 9
completeness anchor 7
boolean guard 6
isfinite 5
TOLERANCE_MAX 3
OverflowError 3
ERROR_RATE_MAX 2
echo cap 2
the gate's anchor wiring 1

That last row is the one that mattered most: a one-token typo in the shell heredoc (.get("corpora").get("corpora_SET")) used to revert the entire completeness guard with a 100% green suite, because nothing ran regression-gate.sh.

Not in scope

#133 (gate does not enforce design D2's single fixed reference model) and #127 (pin-reference-model.sh path sink) are separate open issues on the same script family. Malformed document shapes — a top-level array, a missing corpus or language key — still surface as tracebacks rather than named gate errors; all fail closed, and a shape guard after json.load is the follow-up. Coupling goldens to the provenance record (so a golden that moves while seeded_at stands still is itself a gate error) is the durable answer to the residual inflation window and is likewise a follow-up.

Three payloads made the compare stage print "all corpora within
tolerance" and exit 0 with a 0.99 measurement against a 0.05 golden.
Fail-open is categorically worse than a noisy log: a gate that says
pass when it should say fail is not a weakened gate, it is an absent
one still producing the reassuring output of a present one.

Non-finite numbers: json.load accepts bare NaN/Infinity, and every
comparison against NaN is false, so `diff > tol` was false and the
entry passed. Validation now runs AFTER float() — the only place it
works, since {"golden": "NaN"} is a legal JSON string that never
triggers parse_constant while float("NaN") still yields nan.

Unbounded tolerance: this path needs no unusual number at all, which
is what made it hard to see. tolerance 1e308 rendered a 94-point
regression as an ordinary pass line. tolerance now carries the
tightest bound of the three fields.

Bounds are asymmetric by design: golden and tolerance are dangerous
when large (both make the comparison vacuous), error_rate only when
non-finite (a large measurement correctly fails).

Passing lines now render the tolerance they were judged against.
Auditing a pass is this log's whole purpose, and the one number that
could expose a rigged pass appeared only on failing lines.

Refs #134
A run that compared NOTHING reported success: with both sides empty
every loop was a no-op, so the gate printed "all 0 corpora within
tolerance" and exited 0. A fetch that produced no measurements read as
"this release is safe". A gate that verified no corpora has not
passed, it has not run.

A measurement could be judged against a different metric's golden —
`metric` was validated on the baseline side and never compared across,
so a WER number could be scored against a CER threshold.

A repeated JSON key hid the real threshold: Python keeps the last
value, so {"golden": 0.9, "golden": 0.01} silently became 0.01. The
existing duplicate-corpus check does not see this — that compares
entries, this is one key repeated inside a single entry.

A boolean laundered into a measurement: bool subclasses int, so
float(true) is 1.0 and a bare `true` passed every bound.

TOLERANCE_MAX tightened 0.5 → 0.25. No fixed ceiling separates
generous from disabled on its own, so the bound handles absurd values
and the newly-rendered tolerance handles merely-too-lenient ones.

Refs #134
@kiki830621
kiki830621 force-pushed the idd/134-gate-fail-open branch from 4428d3b to 7c84d28 Compare August 2, 2026 03:28
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #140

Engine

manual fan-out (4 lens Agents + sequenced Devil's Advocate, model: opus, file-based output) + Codex (gpt-5.6-sol, effort xhigh, via codex-call HTTPS) — 6 independent verifiers, two model families.

Backend note: canonical pai-ensemble 2.20.0 was available; this session operates under a standing instruction not to launch Workflow runs unprompted, so the skill's declared quality-equivalent Tier 3 path was used. Ensemble composition unchanged.

Diff-freshness gate (#228): PASS — frozen 7c84d28 == PR head at aggregate time.

Process notes

  • Codex leg failed once (error: The network connection was lost) and was retried once; the retry succeeded. Recorded so a future reader does not assume the cross-model leg ran first time.
  • Working-tree contention. Two lenses ran in-place mutation tests on scripts/lib/baseline-compare.py while others were reading it; the security and regression lenses both detected this, switched to pristine git show HEAD: copies, and said so in their reports. The Devil's Advocate was instructed up front to isolate its mutation work. Verified after every stage: git status --porcelain empty, working-tree sha256 f419872a… == HEAD blob. No finding in this report rests on a mutated tree — but the two reviewers who caught it deserve the credit for the report's integrity, not the process.

Aggregate

FAIL — 2 blocking, 5 follow-up.

The PR closes everything issue #134 enumerated. It does not close the class the issue is named after, and both blocking findings are instances of that class reached without any anomalous value.

Requirements coverage — issue #134

Every atomic requirement is FULLY addressed and was independently reproduced: bare NaN / Infinity / -Infinity / 1e999; the string forms "NaN" / "Infinity" / "1e999"; tolerance: 1e308; both-sides-empty; cross-metric mismatch; duplicate JSON key; boolean laundering; the tolerance now rendered on passing lines.

Both mutation-testing claims in the PR body were verified exactly, not accepted — dropping the isfinite block → 5 assertions fail (claimed 5); TOLERANCE_MAX = inf → 7 (claimed 7). Two reviewers reproduced this on independent harnesses, and the composition matches, not just the totals.

The umbrella requirement — 「gate 對『真實 regression』不得 exit 0」 — is PARTIALLY addressed. See F1 and F2.

The issue's warning that this work would collide with #132's byte-identical lock is wrong, and the PR's rebuttal is right — verified by exhaustion rather than accepted: the only exact-stdout assertions in the repo are three in RegressionWorklistTests.swift, all against the worklist stage (baseline-worklist.py), a different script. No test pins compare-stage stdout, and regression-gate.sh consumes only the exit code.


Findings (merged, deduped; severity = max, except where the Devil's Advocate adjudicated)

# Severity Finding Source Action
F1 HIGH (adjudicated) GOLDEN_MAX = 2.0 leaves golden inflation open. The predicate is actual > golden + tol, so golden and tolerance are the same lever — bounding one at 0.25 while the other admits 2.0 caps the effective threshold at 2.25, above every physically possible error rate. Reproduced against the real committed benchmarks/baseline.json with one field changed and every tolerance left honest: a 100%-WER corpus prints and the run exits 0. No extreme value is needed — golden: 0.5 waves through 0.51, and golden: 0.30 / measured 0.29 produces a completely innocuous line. requirements + logic + security + regression + DA Blocking
F2 HIGH The empty-set fix closes cardinality 0 but not cardinality 1-of-N. regression-gate.sh derives both sides from the same baseline.json (the worklist stage reads it; the assembler iterates it and passes it through verbatim), so nothing anywhere holds an independent expected-corpus set. Deleting 19 of 20 entries from the baseline yields ✓ regression gate: all 1 corpora within tolerance, exit 0 — the same failure class the PR fixed, one cardinality up, reached by pure deletion with no unusual value at all. baseline-worklist.py:82 rejects an empty baseline, which is exactly why the truncated case reads as normal. Codex only — see the adjudication note below Blocking
F3 MEDIUM Goldens have no provenance coupling to benchmarks/baseline-meta.json, although the premise needed for the check is already established for free: regression-gate.sh:69-107 verifies model_files_sha256 before benchmarking, so when the gate passes its model pin, "the model did not change" is a proven fact. A golden that moved while seeded_at stayed put is therefore mechanically suspicious — it is the signature of "raised until the red went away", as opposed to a genuine re-seed, which the gate's own triage message (:99 "re-seed goldens, then re-pin") says must move the meta too. One assertion turns the whole F1 class from undetectable into a swift test failure, and unlike a numeric ceiling it does not have to guess a threshold. DA (new) Follow-up — better than the constant fix
F4 MEDIUM GOLDEN_MAX and ERROR_RATE_MAX are 0-of-57 load-bearing: set either to inf and the entire suite stays green. Every non-finite golden payload in the tests is caught earlier by math.isfinite, and no test uses a finite golden > 2.0 or error_rate > 100. Reproduced on two independent harnesses. The PR body's non-vacuousness table is honest about what it covers; the gap is that the "Bounds are asymmetric on purpose" table presents three reasoned guards when only one is enforced. regression + DA In-scope fix (2 one-line assertions)
F5 MEDIUM OverflowError escapes number()float() on a large bare JSON integer raises it, and the except catches only (TypeError, ValueError). Emits a raw traceback to stderr, which regression-gate.sh does not capture, so the CI log shows a stack trace where the verdict belongs. Fail-closed, so not a bypass — but it contradicts the CHANGELOG's "A traceback is not a verdict". The "version-dependent, 3.9-only" caveat in one report is wrong: a 400-digit literal is far below CPython 3.11+'s 4300-digit int↔str cap and reproduces identically on 3.14.6. The gap is universal. logic + security + Codex + DA (corrected) In-scope fix (one word)
F6 MEDIUM Documentation defects, all verifiable: the PR body's asymmetry table states tolerance | (0, 0.5] while the code is 0.25; test counts are stale (+8 / 453 vs actual +12 / 457) because the body was written against commit 1 of 2; four of the six fixed paths appear nowhere in the body. The CHANGELOG contradicts itself — the bullet says 0.5 and a paragraph 30 lines later records the tightening to 0.25 — and documents an intra-PR value that never shipped, which is not what a changelog is for. Separately, both the CHANGELOG and the code comment claim 0.25 refuses "a quarter-of-the-transcript regression"; the bound is inclusive and the comparison passes on equality, so a regression of exactly 0.25 passes. requirements + security + logic + regression In-scope fix
F7 MEDIUM corpus identity is never type-validated, and True == 1 == 1.0 makes them the same dict key. A measurement labelled true is judged against the baseline for corpus 1 and rendered under a third name — the corpus-side twin of the metric mismatch this PR did close. Not reachable through the real pipeline (the assembler copies corpus from the baseline, so both sides agree by construction); reachable through the standalone stdin entry point, which is exactly the assumption the file's own safe() docstring makes about itself. logic + Codex Follow-up
F8 MEDIUM Only metric is cross-validated between sides; model and language are not, and the comparison is keyed on corpus alone. A measurement declaring a different model is silently accepted. This is adjacent to open issue #133 (the gate does not enforce design D2's single fixed reference model) — the PR correctly scopes #133 out, but this is the same gap seen from the compare stage. Codex Follow-up
F9 LOW The raw-traceback class is 78 of 207 structural probes (38%), not the 4–5 shapes each lens reported. Two sites nobody named: baseline.metric as a list or dict → TypeError: unhashable type inside the PR's first validation guard, and measured[0] = {}KeyError. The proposed one-word OverflowError fix closes exactly 3 of 78 — complete for number(), but crediting it with discharging the CHANGELOG's "like every other" would be wrong by a factor of 25. The class needs a shape guard after json.load. logic + security + regression + DA (scope corrected) Follow-up
F10 LOW The mid-render KeyError: 'language' fires on the ✗ REGRESSION line, so the operator sees green lines followed by a stack trace with the regressing corpus never named. Fail-closed, but it is the report's own thesis one level down. logic + DA (sharpened) Follow-up
F11 LOW New numeric guards run in the compare stage — after the full 12-corpus benchmark loop — contradicting regression-gate.sh:53-57's own stated principle for placing the model-pin check early ("fails the gate BEFORE any benchmark spends minutes"). The worklist stage runs first, already parses the baseline, and validates zero numeric fields. So tolerance: 1e308 costs a complete benchmark run before the gate objects, on the two paths that skip swift test: the standalone regression-gate.sh invocation README documents, and BESTASR_BASELINE=<other file>. DA (new) Follow-up
F12 LOW Rejection messages format with {:g} (6 significant digits), so golden = 2.0000001 is rejected with must be at most 2 (got 2). In a change whose thesis is that the deciding number must be visible, an error that renders the rejected value as identical to the threshold is unactionable. Also: IEEE-754 edges — "0.25000000000000001" may parse to the same binary float as 0.25 and pass; -1e-9999 underflows to -0.0, which passes >= 0. logic + Codex Follow-up
F13 INFO The committed-baseline test is a one-way ratchet toward looser bounds. It asserts exit == 0 on the real file, so it also fails if GOLDEN_MAX is ever tightened below the largest committed golden. Harmless today (max 0.1549), but commit a golden: 1.5 and any later attempt to tighten the bound below 1.5 breaks a test — presenting to the next maintainer as "the fix breaks CI". Another reason to tighten now rather than later. DA (new) Follow-up
F14 INFO regression-gate.sh itself has zero automated coverage — the shell layer through which every verdict passes (COMPARE_RC capture, the FAILED_RUNS / set -e interaction, the assembler heredoc). Verified sound by hand this round; that verification is preserved nowhere. Pre-existing, not attributable to this PR. DA (new) Follow-up

Adversarial pass — the severity dispute, adjudicated

The four lenses split on F1: logic said CRITICAL, security HIGH / fix before merge, requirements MEDIUM, regression MEDIUM and explicitly not blocking. Because all four saw the same evidence and the same reproduction, taking the maximum would have buried a genuine disagreement about reasoning inside a max(). The Devil's Advocate was asked to rule on it.

Ruling: HIGH, fix before merge — upholding security and overturning the other three.

  • logic's CRITICAL, overturned downward. Every technical claim verified, but no constant in this file can be a security boundary: anyone who can edit benchmarks/baseline.json can edit GOLDEN_MAX on the line above it. CRITICAL implies an adversary the fix stops; there is none. The same argument applies to TOLERANCE_MAX — i.e. to the PR's own accepted fix — which is what these bounds actually are: error-detection guards against an honest mistake or a lazy triage, not access controls. The threat model that matters is "the gate is red and someone needs it green", which is a real recurring event here, since the gate's own failure message offers re-seeding goldens as a legitimate triage outcome and nothing separates "re-seeded after verified drift" from "raised until the red went away".
  • regression's "not a regression, PR strictly improves", overturned. The premise is true and the conclusion does not follow: both propositions hold at once — the PR improves and leaves the named class open — and the first was used to suppress the second. The issue was deliberately named after the class rather than the payloads to prevent exactly that scoping. There is also a structural cost nobody else noted: the PR routes the path of least resistance onto the unguarded lever. After merge, someone who tries tolerance: 0.9 hits a gate error and must stop and think; someone who instead raises golden from 0.05 to 0.30 gets a green line — and raising the golden is the documented legitimate remedy.
  • regression's "CI catches it", overturned. The committed-baseline test sets measured = golden, so diff == 0 everywhere: it is purely a bounds-acceptance test and cannot detect comparison semantics. It blocks golden > 2.0 and golden < 0; it does not block golden: 1.5, 0.5, or 0.30. Committed goldens span [0, 0.1549], so the unguarded inflation window is (0.1549, 2.0] — the entire realistic range. And ci.yml explicitly does not run the gate ("Deliberately NOT the regression gate — the gate runs at release time"), so the one CI-side guard is that bounds test, which covers the tail rather than the body.
  • regression's "the issue only asked for isfinite() + an upper bound", overturned on a misreading. The requirement reads 「必須是有限數且落在合理範圍」 — the parenthetical math.isfinite() + 上界 names the mechanism, not the acceptance criterion — and the three items are prefixed 「至少需要」, a floor rather than a specification.
  • requirements' MEDIUM, overturned upward. Its mitigation that the baseline is "committed, reviewed" was checked rather than assumed: git log shows a single author and a single commit, there is no CODEOWNERS, and this repo's workflow routinely has AI agents open PRs. "Reviewed" means reviewed by whoever wrote it — the reviewer regression gate 對真實 regression 回報 exit 0(fail-open):非有限數 + tolerance 從不被渲染 #134's own thesis says cannot be relied on, because a rigged line is byte-for-byte indistinguishable from a genuine pass.

The decisive argument was cost, and it was measured. Replaying all 57 assertions against tightened bounds: GOLDEN_MAX at 2.0, 1.0, and 0.5 all give 0 / 57 failures, including the committed-baseline test (max committed golden + tolerance = 0.1749, so 0.5 leaves 2.9× headroom). And the error directions are not comparable: a bound set too tight fails loudly at swift test on every PR, naming the file; a bound set too loose fails silently at release, as a green line. The recommended form is to bound golden + tolerance rather than either field alone, since the sum is the effective threshold and is the only bound invariant to trading one lever against the other.

The code comment justifying 2.0 was also refuted empirically rather than rhetorically: it cites insertion-inflated CER, which is a property of a measurement and is already governed by ERROR_RATE_MAX = 100.0. With GOLDEN_MAX = 0.5, an honest golden of 0.05 against a measured 1.5 still correctly fails as a regression rather than being rejected as out-of-bounds.

On F2, and an honest limit of the adversarial pass

The Devil's Advocate ran a systematic 207-probe fuzz — every JSON type substituted into every field position, each probe carrying a real 0.99-vs-0.05 regression so that any exit 0 is a fail-open by construction — and reported no structurally novel fail-open, with eight named attacks recorded as failed. That negative result is genuine and strengthens the confidence in the six closed paths.

But it has a scope limit the pass did not state: the probe dimension is type substitution with both sides present. Cardinality was never a probe dimension, so that fuzz could not have found F2 regardless of how many probes it ran. The cross-model leg found it from a different starting question — what entitles this decider to believe it received a complete input — and F2 was then confirmed independently by reading regression-gate.sh:161-182 and baseline-worklist.py: both sides of the comparison are derived from the same file, so no independent expected-count anchor exists anywhere in the pipeline.

Recorded because a negative result is only as strong as the space it actually covered.

Scope check

Clean. Three files, +374/−2, all within issue #134's surface. The re-cut that moved the FluidAudio pin to PR #142 left no residue — verified: Package.swift / Package.resolved are identical to main, and grep -i 'fluidaudio|0\.15\.[45]|#122' over both the diff and the PR body returns zero hits (the three FluidAudio strings in CHANGELOG.md are pre-existing released entries on main).

Sister issues are untouched and not made harder: scripts/pin-reference-model.sh (#127) is not in the diff, and the compare stage never reads the model field at all, so nothing here constrains a future #133 fix.

The four extra paths from the second commit exceed the issue's three enumerated "Expected" items, but the issue's own 命名 section fixes the scope as 「gate 對真實 regression 回報 exit 0」 rather than "NaN handling", and all four are instances of that verdict going wrong. Not scope creep by the issue's own definition.

One near-miss worth recording: jfk has golden: 0, and the golden lower bound is inclusive while tolerance's is exclusive. Had the author reused low_exclusive=True for golden out of symmetry, the real committed baseline would have been rejected. The asymmetry is correct — and nothing tests it except the committed-baseline test, which is the single most valuable test in this PR.

Next

F1 and F2 gate the merge. F1 is one constant plus three assertions at a measured cost of zero failing tests; F5 is one word and should ride along. F6's documentation defects should be corrected before merge for the reason the PR itself argues: in a change whose thesis is "the reassuring output of a gate that is not what it says", shipping a body that misstates its own bound is the wrong note to end on. F3 is the durable answer to the whole F1 class and belongs in a follow-up issue.

Verify was run at 7c84d28; re-run after the blocking findings are addressed.

Verification found that bounding `tolerance` had closed one handle of a
two-handled lever and left the other free. The decision is
`actual - golden > tol`, so the gate passes anything at or below
`golden + tolerance` — that sum is the effective threshold, and the two
fields enter it identically. A golden of 2.0 with an honest tolerance of
0.02 admitted every physically possible error rate, and the tolerance
this same issue had just put on the passing line read a reassuring
0.0200, because the number that moved was not the one being rendered.
Reproduced against the real committed baseline with one field edited.

The bound is now on the sum (<= 0.5, against a committed maximum of
0.1749). It is the only bound invariant to trading one handle for the
other; a per-field bound always leaves its partner as a substitute.

`golden`'s own constant is deleted rather than kept. With the sum bounded
it can never be the guard that fires, and mutation testing put it at zero
failed assertions both before and after. A constant no test can
distinguish from its absence is not a guard. Its stated justification was
also wrong: "CER can exceed 1.0 via insertions" describes a measurement,
which ERROR_RATE_MAX governs.

A truncated comparison also still reported success. Closing the empty
case closed cardinality 0, not 1-of-N: both sides derive from the same
baseline.json, so deleting entries shrinks the baseline, the work list
and the measured set together and the gate prints "all 1 corpora within
tolerance" over a release that verified one twelfth of what it should
have — by pure deletion, with no unusual value anywhere. The expected
set is pinned in baseline-meta.json and, for the repo's own baseline, is
now an INVARIANT of regression-gate.sh: a missing, unreadable or
malformed meta is fatal there, an overridden BESTASR_BASELINE only warns.
The compare stage still tolerates an absent anchor with a printed NOTE,
because a stdin filter cannot know whether it was handed a whole sweep —
the invariant belongs where the answer is knowable. That conditional also
upgrades the #48 model-artifact pin, which was skipped SILENTLY when the
same file was missing.

A satisfied anchor now says so. Its only previous evidence was the
absence of the warning, which asks a reader to already know the warning
exists — the argument this issue makes for rendering the tolerance on
passing lines, applied to the guard it just added.

`error_rate: false` was the one boolean position still able to flip a
verdict, and nothing tested it: float(false) is 0.0, a boolean laundered
into a PERFECT SCORE rather than a bad one. Bounding golden at 0.5 had
quietly made the existing boolean test vacuous, which is the general
hazard — tightening one bound can hollow out another guard's only test.

OverflowError escaped the numeric converter; a bare 400-digit integer
parses to an arbitrary-precision int and float() raises neither TypeError
nor ValueError. Non-zero exit, so never a bypass, but it reached the log
as a stack trace where the verdict belongs. Rejected values are capped at
120 characters and corpus lists at 12 names, the anchor is shape-checked
before use, it names the file it actually read rather than the default
path, and the sum-bound message renders at 17 significant digits so a
rejected value cannot print as equal to the threshold it exceeded.

A second verify round found the anchor could still be switched off by
omitting a key, and that three guards had no test able to tell them from
their absence. The gate now REFUSES an unanchored run on the repo's own
baseline (missing, unreadable, or corpora absent/null/not a unique
non-empty string array); an overridden BESTASR_BASELINE warns instead.
The compare stage keeps tolerating an absent anchor with a NOTE, because
a stdin filter cannot know whether it was handed a whole sweep — the
invariant belongs where the answer is knowable, and both halves now have
a test so the permissive half is no longer the whole specification.

Every guard is non-vacuous, measured by deleting each independently:
sum bound 9, completeness 7, boolean 6, isfinite 5, TOLERANCE_MAX 3,
OverflowError 3, ERROR_RATE_MAX 2, echo cap 2, and the gate's own anchor
wiring 1 — a one-token typo in that shell heredoc used to revert the
whole completeness guard with a green suite, because nothing ran
regression-gate.sh.

Those counts come from scripts/mutation-check.sh, committed alongside
them. A count the reader cannot re-run is the same defect as a threshold
the reader cannot see, one level out.

The PR body has been regenerated from this tree. An earlier version of
this commit message claimed the body had already been corrected when only
the CHANGELOG had been; that claim was false when written and is the
reason this message was amended rather than followed by another commit.

474 tests / 88 suites green (+29 over main).

Refs #134
@kiki830621
kiki830621 force-pushed the idd/134-gate-fail-open branch from f9155c9 to 39a9a46 Compare August 2, 2026 07:30
@kiki830621

Copy link
Copy Markdown
Member Author

Verify Report — PR #140, Round 2

Re-verify after the round-1 blocking findings were addressed. This report describes 7a4b2ea, the commit the ensemble was frozen against; the fixes it prompted are in 39a9a46, summarised at the end.

Engine

4 lens Agents (opus) + sequenced Devil's Advocate + Codex (gpt-5.6-sol, xhigh) — 6 independent verifiers, two model families. Diff-freshness gate (#228): PASS, frozen 7a4b2ea == PR head throughout aggregation.

Process notes

  • The Codex leg failed twice on error: The network connection was lost before succeeding on the third attempt. Recorded because the pattern is not random: both regression gate 對真實 regression 回報 exit 0(fail-open):非有限數 + tolerance 從不被渲染 #134 rounds failed on first contact (448-line and 862-line diffs) while the Router warnings never reach the user without --explain #136 and FluidAudio pin 0.15.4 → 0.15.5(#110 子項 A,B/C 的 blocker) #122 rounds succeeded first try on 190 and 85 lines. A future reader should not assume the cross-model leg is as reliable on this PR as elsewhere.
  • The Devil's Advocate wrote its ruling before the Codex result existed, then revised it after receiving the dissent. Both versions are reflected below; the revision is the operative one.
  • Working-tree discipline held. All five Claude verifiers ran mutation and fuzz work in isolated directories against git show HEAD: copies, and three independently recorded the same file hashes. Round 1's contention did not recur.
  • One self-inflicted process defect worth recording: the coordinator's own completion signal watched for file existence, and the instruction to "write a skeleton first" — added after round 1's DA died before writing anything — made four skeletons look like four finished reports. Caught by reading them. Fixing one failure mode produced a quieter one.

Aggregate

FAIL at 7a4b2ea — 1 blocking (adjudicated), plus a record-integrity item that could not be repaired after merge.

Both round-1 blockers were genuinely closed and the code was in better shape than round 1 left it. What failed was narrower and is stated precisely in the adjudication below.


Round-1 findings — disposition

Every R1 finding was checked for silent claiming, and none was claimed fixed without being fixed. Each deferral is either unmentioned or explicitly scoped in the CHANGELOG.

R1 status at 7a4b2ea
F1 (HIGH, blocking) — GOLDEN_MAX = 2.0 left golden inflation open CLOSED. R1's exact reproduction now exits 1. Non-vacuous. Residual measured, not eliminated — see R2-1.
F2 (HIGH, blocking) — truncated comparison reported success PARTIALLY — see the adjudication. The named attack is closed; the requirement is not.
F3 — golden↔provenance coupling Correctly deferred, not claimed
F4 — GOLDEN_MAX / ERROR_RATE_MAX untested ERROR_RATE_MAX covered; GOLDEN_MAX resolved by deletion
F5 — OverflowError CLOSED
F6 — documentation defects CHANGELOG fixed; PR body untouched → escalated, see below
F7, F8, F9, F10, F11, F12, F13, F14 Correctly deferred; F14 escalated to MEDIUM because the shell it names now carries the anchor

Issue #134's three enumerated requirements: FULLY addressed, all independently reproduced. The umbrella requirement — 「gate 對『真實 regression』不得 exit 0」 — is where the adjudication below lands.


The adjudication: is F2 closed?

This is the round's substantive disagreement and it split the ensemble 5–1.

Four Claude lenses and the Devil's Advocate's first pass: CLOSED. The named attack — pure deletion from baseline.json — exits 1 and names all eleven missing corpora, reproduced end-to-end through the extracted assembler by three verifiers independently. The three ways the anchor can vanish by editing committed files are all caught by CI (#require(meta["corpora"] as? [String]) returns nil for a missing key, a null, and a wrong type; Data(contentsOf:) throws for a deleted file — Foundation semantics verified, not assumed).

Codex: PARTIALLY, and blocking. Its framing: the two env vars being overridable is not the problem — anyone with shell control can substitute inputs. The problem is that the supported missing-file / missing-key state is designed to succeed. It listed five non-adversarial paths, of which the fourth is ordinary local triage: copy the baseline to /tmp, cut it to one entry, run the gate with BESTASR_BASELINE pointed at it. No sibling meta exists, so the anchor is absent, and the run exits 0 over a subset.

Ruling, after the Devil's Advocate re-derived the argument: Codex is right. F2 is PARTIALLY closed.

The reasoning that decided it was a layering argument nobody had made:

layer behaviour at 7a4b2ea correct?
compare stage (stdin filter) no anchor → NOTE, continue, exit 0 Yes. A filter cannot know whether its input was a whole sweep. Permissive-with-disclosure is the right contract, and eleven existing tests depend on it.
regression-gate.sh (knows which baseline it is running) no anchor → say nothing, omit the key, exit whatever compare says No. There was no invariant here at all.
CI catches the committed-file variants Real, but a data-consistency test, not a gate test

F2's requirement lives at the middle layer, and that layer did not implement it. So the finding was not closed at the layer where closing it is possible.

The Devil's Advocate's account of why the majority erred is worth preserving verbatim, because it is a failure mode this ensemble had already ruled against once:

Every one of us verified that the named attack fails, and then treated the surviving configurations as residuals. That is precisely the move the R1 Devil's Advocate overturned regression for making on F1 — "the issue was deliberately named after the class rather than the payloads to prevent exactly that scoping." Applying a different standard to F2 one round later is not a judgement call, it is an inconsistency.

Where the ruling did not follow Codex. Codex's first proposed remedy — make the compare stage exit non-zero on a missing anchor — was rejected: it would break the filter's documented contract and eleven tests, and would put the invariant at the layer that cannot justify it. Codex's own second branch (an explicit opt-in, with the release wrapper never enabling it) is the one that was taken. Codex also bundled R1-F7/F9 (corpus type confusion, shape guards) into its blocking list; those are pre-existing, explicitly deferred follow-ups and were kept there.


Findings (merged; the ~20 filed items dedupe to these)

# Severity Finding Source
R2-A HIGH Record integrity. The commit message stated "Corrected in the record: the PR body and CHANGELOG…"; only the CHANGELOG was corrected. The PR body diffed byte-identical to round 1 and still advertised golden | [0, 2.0] — a bound the same commit deleted, with a justification the same commit calls "wrong as well as inert" — plus tolerance | (0, 0.5] against a shipped 0.25, stale counts, and no mention of either architectural change. Functional risk zero. Escalated above round 1's MEDIUM on one argument: the commit-message half cannot be repaired after merge. gh pr edit works forever; git commit --amend does not survive a merge. regression + requirements + DA
R2-B HIGH (adjudicated) The absent-anchor test asserted the degradation as required behaviour#expect(r.exit == 0) under a name saying the opposite. Not merely that a degradation path was left open: the suite made it the specification, so anyone later making the unanchored case fail-closed would break a test. Codex (adjudicated by DA)
R2-C MEDIUM The anchor's wiring had zero coverage, and CI never runs the gate. Demonstrated: rename one token in the assembler (.get("corpora").get("corpora_SET")), touch no JSON file, and the truncation attack passes again with a 100% green suite. By the file's own stated predicate — "a constant that no test can distinguish from its absence is not a guard" — the fix's delivery path failed the test the fix used to justify deleting GOLDEN_MAX. regression + DA + Codex
R2-D MEDIUM Tightening golden to 0.5 silently hollowed out the previous commit's boolean test. float(true) is 1.0, which used to sit inside GOLDEN_MAX = 2.0 so the type guard was what caught it; it now sits outside 0.5, so the bounds catch it first. Measured: verdict-flipping at 7c84d28, 0-of-87 at 7a4b2ea. The commit message's "Every guard is non-vacuous" was false as a universal claim, and the guard it missed was added by the previous commit in the same PR. regression
R2-E MEDIUM Three guards were 0-of-N load-bearing: _MAX_ECHO, the boolean guard (R2-D), and the anchor wiring (R2-C). The first is condemned by name in the same file. regression
R2-F MEDIUM Three tests asserted less than their names claimed…refused by name and …still refused on its own terms assert substrings satisfied by the other code path (and by the passing line), and …says the completeness check ran asserted only exit == 0 against code that emitted no such message. A pattern, not three nits. requirements + regression + security + DA
R2-G MEDIUM The residual golden-inflation window, measured. With tolerance left honest at the committed 0.02, golden can reach 0.48. Sharpest instance: jfk (committed golden 0.0000) inflated to 0.48 admits a 50 % WER on the canonical clean sample, all 12 corpora present, anchor satisfied, exit 0. Not a regression against R1 — R1's own DA recommended bounding the sum at 0.5 — but the residue R1 predicted when it called provenance coupling "better than the constant fix". requirements + logic + security + DA
R2-H LOW R1-F12 reproduced inside the new sum message: {:.4f} renders a rejected value as equal to the threshold it exceeded. Codex supplied a legible construction on the same axis (golden 0.5, tolerance 5e-324: the rounded sum is exactly 0.5 while the exact real sum is not), and logic's 200,000-pair search bounded the escape at half an ULP. The two are complementary, not contradictory, and the honest write-up is that the bound is sound to within half an ULP and the prose should say binary64. Admits nothing: nextafter(0.5) is still rejected. logic + Codex + DA
R2-I LOW The anchor added two traceback shapes to the known malformed-shape class, reachable from a hand-edited field in the file this round made verdict-critical; messages hardcoded a path that is overridable; the missing-corpus join was unbounded. All fail-closed. logic + security + regression
R2-J INFO baseline-meta.json's new corpora_comment states a rule about itself — "Changing the set is a re-seed and must move seeded_at with it" — that nothing enforces. A prose rule added to a data file with no test reading it. DA
R2-K INFO "Unverifiable from the diff" ≠ "unverified". Codex declined the commit's non-vacuity table for lack of a runner or artifact — methodologically correct from its position, since it received a diff and no repository. Two Claude lenses had reproduced all six numbers independently with different harnesses. The objection nonetheless converts into a real improvement: ship the harness. Codex + DA

Negative space — attacks that failed

Recorded so the surviving verdicts carry weight, and because two of these are why the first CLOSED ruling looked defensible.

  • A novel fail-open in the anchor's input shape. 20 shapes of expected_corpora against a baseline truncated 12 → 1, so any exit 0 is a fail-open by construction. Five exit-0 shapes, all five already named. Three traceback, all fail-closed. No novel shape.
  • A differential loosening between commit 2 and commit 3. Two independent grids (1,721 and 534 probes, different token sets, every probe carrying a real 0.99 regression): 0 loosened, 20–24 tightened. Deleting GOLDEN_MAX is a strict tightening.
  • Exploitable float slack above the sum bound. 200,000 randomised straddling pairs against exact Fraction arithmetic: max escape 5.551e-17, exactly half an ULP at 0.5 — the tightest a float comparison can be.
  • Relabelling language/metric to sneak past the schema tests. The one CI-green relabelling makes the gate harder to pass.
  • Padding the baseline with stubs to satisfy the anchor. Satisfies the set comparison, then produces eleven "was never measured" errors.
  • Collapsing the STANDARD_GAPS mitigation by assuming fetch-corpora.sh derives its corpus list from the baseline. Checked: it hardcodes every name with pinned digests. The mitigation survives a premise its author left implicit.
  • set -u / pipefail edges on the new $META argv. Both routes fail closed.

Two verifiers also corrected their own earlier work: one revised a warnings comparison after realising its first reading came from an incremental build, and the Devil's Advocate discarded its own boolean-payload conclusion after Codex found a case its grid's shape had masked — a negative result is only as strong as the space it covered, applied by its author to itself.


What changed in response (39a9a46)

Stated so the report is not mistaken for the current state of the branch.

  • The wrapper invariant. regression-gate.sh now refuses the repo's own baseline when baseline-meta.json is missing, unreadable, or its corpora is absent, null, not a list, empty, non-string or duplicated; an overridden BESTASR_BASELINE warns instead. This also upgrades the pre-existing enhancement: reference model provenance 從 audit anchor 升級為可驗證 pin(#34 verify 殘留) #48 model-artifact pin, which a missing meta file used to skip with no message at all.
  • The test split (R2-B): the compare-stage test keeps exit == 0 and is renamed to what it verifies; a wrapper-layer counterpart asserts refusal across six meta-corruption modes plus the overridden-baseline warning. The fail-open stops being the spec once both exist.
  • The wiring test (R2-C): extracts the assembler heredoc from the real script and asserts the emitted payload carries expected_corpora. The one-token typo now turns the suite red.
  • A satisfied anchor now says so — its only prior evidence was the absence of a warning.
  • Both verdict-flipping boolean payloads (R2-D), kept as separate cases because they are different mechanisms: golden: false makes the threshold maximally strict (a type hole), error_rate: false makes the measurement a perfect score (an accuracy hole).
  • R2-F: the three assertions now name the distinguishing substrings, and a case was added that actually exercises the sum path — the previous headline test was rejected by the per-field ceiling before reaching it.
  • R2-A: commit message amended (and it now records that the earlier claim was false), PR body regenerated.
  • R2-K: scripts/mutation-check.sh is committed. It deletes each guard, re-runs the suite, restores the tree, and fails if anything scores zero.

Every guard is now load-bearing: sum bound 9, completeness 7, boolean 6, isfinite 5, TOLERANCE_MAX 3, OverflowError 3, ERROR_RATE_MAX 2, echo cap 2, anchor wiring 1. 474 tests / 88 suites green.

Deferred to follow-up issues, unchanged: golden↔provenance coupling (carry R2-G's measured window — jfk 0.0000 → 0.48, admitting a 50 % WER), a shape guard after json.load, corpus-identity typing, and the render-fidelity nits.

Re-verify at 39a9a46 before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant