Found by relying on it: I added an enforced requirement with a fresh // VERIFIES: binding, ran scripts/trace.sh check, and got TRACE: PASS on evidence from a gate verdict taken at a different commit — one that predated the binding.
The defect
scripts/lib/trace.py:406-433 selects the evidence log. Its own comment states the rule:
A verdict is attributable to (tree, HEAD, TOOLCHAIN).
It then checks two of the three: result == "INVALID" (tree/HEAD moved during the run) and a toolchain mismatch. It never compares the verdict's recorded commit against HEAD.
Sabotage-verified, with a control that the surrounding machinery does fire:
control : verdict commit 0e42ea2f == HEAD 0e42ea2f -> TRACE: PASS rc=0
sabotage : verdict commit := deadbeefdeadbeef… -> TRACE: PASS rc=0 <-- unchecked
control 2: verdict toolchain := "rustc 0.0.0 (not-real)" -> NOTE "…a verdict does not survive a
toolchain change; run a full gate"
Scope — narrower than it first looks, and worth stating precisely
| path |
behaviour |
inside scripts/gate.sh |
exports GATE_LOG="$LOG" (gate.sh:203), so it reads the live log — attributable by construction. Not affected. |
| CI's traceability job |
has no target/, so: NOTE: no gate log in target/ — run-status of enforced bindings unverified. Honest, but it means CI never verifies run-status. Not a regression; pre-existing and declared. |
standalone scripts/trace.sh check — what a developer runs |
falls back to target/gate-verdict.json and accepts it regardless of commit |
So this is a local hole. The failure it permits: add a // VERIFIES: binding, do not run the gate, and the checker confirms the cited test "ran and passed" from a verdict taken before the binding existed. Sharper variant — rename a test and bind to the old name: the stale log still records the old name passing, so the binding validates against a test that no longer exists.
Why it matters despite being local
The standalone path is the one used while authoring a binding, which is exactly when the evidence claim is being made for the first time. It is also the same class the file already guards against for toolchain — feedback_gate_verdict_bound_to_toolchain records that a distro upgrade re-derives a verdict "with no act by anyone". A commit change is an act by someone, which arguably makes it the easier of the two to check.
Proposed fix
Mirror the existing toolchain branch: compare v["commit"] against git rev-parse HEAD and, on mismatch, return the same (None, reason) degradation — "the last gate verdict was taken at , and HEAD is ; run a full gate" — so it becomes the familiar NOTE plus unverified, not a silent pass and not a hard failure.
Deliberately not proposing a hard failure: the toolchain case degrades rather than fails, and matching that keeps one behaviour for "the evidence is not attributable". A stricter choice would also make every ordinary edit-then-check cycle noisy.
The sabotage above is the self-test: a bogus commit must produce the NOTE, and the restored verdict must pass.
What I did about it meanwhile
Ran a full gate on the exact commit (GATE: PASS 0e42ea2f… clean) so #1412's new enforced binding rests on attributable evidence rather than on this gap.
Related: #1151 (the INVALID verdict check), #1240 (evidence-self-test), #1412.
🤖 Generated with Claude Code
https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6
Found by relying on it: I added an
enforcedrequirement with a fresh// VERIFIES:binding, ranscripts/trace.sh check, and gotTRACE: PASSon evidence from a gate verdict taken at a different commit — one that predated the binding.The defect
scripts/lib/trace.py:406-433selects the evidence log. Its own comment states the rule:It then checks two of the three:
result == "INVALID"(tree/HEAD moved during the run) and a toolchain mismatch. It never compares the verdict's recordedcommitagainst HEAD.Sabotage-verified, with a control that the surrounding machinery does fire:
Scope — narrower than it first looks, and worth stating precisely
scripts/gate.shGATE_LOG="$LOG"(gate.sh:203), so it reads the live log — attributable by construction. Not affected.target/, so:NOTE: no gate log in target/ — run-status of enforced bindings unverified. Honest, but it means CI never verifies run-status. Not a regression; pre-existing and declared.scripts/trace.sh check— what a developer runstarget/gate-verdict.jsonand accepts it regardless of commitSo this is a local hole. The failure it permits: add a
// VERIFIES:binding, do not run the gate, and the checker confirms the cited test "ran and passed" from a verdict taken before the binding existed. Sharper variant — rename a test and bind to the old name: the stale log still records the old name passing, so the binding validates against a test that no longer exists.Why it matters despite being local
The standalone path is the one used while authoring a binding, which is exactly when the evidence claim is being made for the first time. It is also the same class the file already guards against for toolchain —
feedback_gate_verdict_bound_to_toolchainrecords that a distro upgrade re-derives a verdict "with no act by anyone". A commit change is an act by someone, which arguably makes it the easier of the two to check.Proposed fix
Mirror the existing toolchain branch: compare
v["commit"]againstgit rev-parse HEADand, on mismatch, return the same(None, reason)degradation — "the last gate verdict was taken at , and HEAD is ; run a full gate" — so it becomes the familiar NOTE plus unverified, not a silent pass and not a hard failure.Deliberately not proposing a hard failure: the toolchain case degrades rather than fails, and matching that keeps one behaviour for "the evidence is not attributable". A stricter choice would also make every ordinary edit-then-check cycle noisy.
The sabotage above is the self-test: a bogus commit must produce the NOTE, and the restored verdict must pass.
What I did about it meanwhile
Ran a full gate on the exact commit (
GATE: PASS 0e42ea2f… clean) so #1412's newenforcedbinding rests on attributable evidence rather than on this gap.Related: #1151 (the INVALID verdict check), #1240 (
evidence-self-test), #1412.🤖 Generated with Claude Code
https://claude.ai/code/session_0188ATCj6DZ9aRVQ2vSirua6