Files
msd-core/gsd-core/references/verifier-evidence-gate.md
Tom Boucher 6beaa66b25 enhance(#3304): gate re-verification blockers on deterministic evidence (#4085)
* test(#3304): add failing-first suite for the convergence evidence gate

Content-assertion suite for the Step 7 re-verification evidence gate
(agents/gsd-verifier.md / gsd-core/references/verifier-evidence-gate.md).
Committed before the implementation to prove RED via gsd-test.

* enhance(#3304): gate re-verification blockers on deterministic evidence

Step 7's anti-pattern scan re-runs at full, unbounded scope on every
re-verification pass, independent of the must-haves established in Step 2.
A blocker it finds — other than the self-evidencing debt-marker check —
previously reverted a completed gap-closure round and started another
--gaps cycle on nothing more than the verifier's own new judgment call,
with no bound on how many times that could repeat.

A Step 7 blocker now blocks unconditionally in re-verification mode only
if it is a carried-forward gap (present in the prior VERIFICATION.md's
gaps: list) or the flagged file was git-modified since the prior pass
(a regression; fails closed toward blocking when history is unresolvable).
Otherwise it predates the gap-closure round unflagged and needs
deterministic evidence — a named test run red, or another concrete
reproducible artifact — to stay blocking. Unevidenced, it downgrades to
a new advisory: frontmatter list and report section instead of setting
status: gaps_found, and never reverts a completed must-have.

Maintainer approval was narrowed to this evidence condition only,
explicitly rejecting the broader "advisory whenever untraceable to a
requirement/decision/prior-gap" proposal — implemented and pinned by
tests/verifier-evidence-gate.test.cjs and documented as rejected in
gsd-core/references/verifier-evidence-gate.md so it can't silently
re-expand.

Closes #3304

* fix(#3304): correct window-truncation and indentation bugs in evidence-gate tests

gsd-test's GREEN checkpoint caught 3 real bugs in the test file itself
(not the production prose): a {0,600} match window was shorter than the
724-char paragraph it was scanning (the "exclude from Step 9 Rule 1"
phrase starts at offset 662), and two regexes assumed no indentation
after a markdown list-continuation line break. All three phrases are
confirmed unique across agents/gsd-verifier.md, so the windowed
submatches are replaced with direct whole-string assertions instead of
just widening the window.

Also acknowledges the deliberate byte growth in agents/gsd-verifier.md
that the differential-attribution check (ADR-2719) correctly flagged.

Emitted-Drift-Ack-Growth: gsd-verifier.md — adds the #3304 re-verification evidence gate (Step 7 rule, Advisory bucket, advisory: frontmatter, report section); 1488 bytes, still within the LARGE-tier 48 KiB cap (48751/49152).

* docs(#3304): backfill changeset PR number

---------

Co-authored-by: sim <sim@local>
2026-08-30 16:51:52 -04:00

8.1 KiB

Convergence Evidence Gate (#3304)

Bounds Step 7's anti-pattern scan so an approved gap-closure contract can actually close. Applies only when is_re_verification = true (Step 0) — a first pass has no prior contract to be out-of-contract from, so this gate is a pure no-op there.

The problem this closes

Steps 4-7c re-verify at full, unbounded scope on every re-verification pass — that is documented, intended design, not the bug. The bug is narrower: Step 7's Categorize: line lets the verifier's own free-form judgment label anything it believes "prevents goal" a 🛑 Blocker, and Step 9 Rule 1 promotes any 🛑 Blocker straight into status: gaps_found — with no distinction between a blocker tied to what the gap-closure round was actually supposed to fix and a blocker that is simply a new opinion formed on this pass. Reported real-world instance: a re-verification cycle promoted four "architectural and security observations" to blockers, none backed by a failing test, none traceable to a requirement/decision/prior gap, reverting a completed, all-green gap-closure round and recommending another --gaps cycle — with no bound on how many times that could repeat.

Truths, artifacts, and key links (Steps 3-6) can never produce this failure mode: Step 0 re-verification mode reuses the must-haves extracted in Step 2 verbatim ("Skip to Step 3") rather than re-establishing them, so whatever a truth/artifact/link is was fixed before this re-verification round started. Only Step 7's blanket per-file scan is unbounded by that must-haves contract — which is exactly the mechanism the issue's diagnosis names. This gate therefore touches Step 7 only.

Definitions

Self-evidencing blocker (unaffected by this gate). The debt-marker gate (TBD/FIXME/XXX with no issue #123/PR #123/#123/DEF-* reference on the same line) is the only Step 7 category with zero judgment component — a regex match plus the absence of a follow-up reference, nothing inferred. Its own textual presence in the file is the deterministic evidence. It keeps blocking unconditionally, exactly as before. Do not extend this carve-out to any other Step 7 category (stub classification, hollow props, empty implementations, console-log-only): every one of those already requires judgment per Step 7's own "Stub classification" paragraph ("a grep match is a STUB only when the value flows to rendering... and no other code path populates it with real data") — that judgment is exactly what this gate exists to check.

New-scope finding. Any Step 7 🛑 Blocker other than a self-evidencing one (above) is a new-scope finding unless either of the following holds, in which case it is in-contract and blocks unconditionally, evidence or not:

  1. Carried-forward gap — it matches an item in the previous VERIFICATION.md's gaps: list, using the same 80%-token-overlap matching algorithm Step 3b already uses for override matching (normalize to lowercase, strip punctuation, collapse whitespace, tokenize, intersect).

  2. Regression — the flagged file was modified since the previous VERIFICATION.md's verified: timestamp. Check file-level, not line-level — an LLM agent re-deriving precise line provenance mid-pass is unreliable; file-level modification is a single, robust command:

    git log --since="$PREV_VERIFIED_TS" --oneline -- "$file"
    

    A non-empty result means the file changed since the prior pass — the gap-closure round could plausibly have introduced this finding, so it's self-evidencing as a regression and blocks. Fail closed: if git history is unavailable, ambiguous, or the timestamp can't be parsed, treat the file as modified (blocks). The imprecision this trades away (a big file with one unrelated hunk touched treats every pattern in it as "new") only ever makes more things block, never fewer — consistent with <adversarial_stance>.

A finding that is neither a carried-forward gap nor on a file modified since the prior pass predates the gap-closure round entirely and was never flagged as a gap then — this is the literal "some findings predated the gap implementation and had previously been explicitly treated as non-blocking" case from the issue.

Deterministic evidence — required for a new-scope finding to stay blocking. One of:

  • A named test that FAILS when actually run (red). Run exactly one test, the same discipline Step 7b already uses for behavioral spot-checks — never the full suite. Record the exact command and the failing output.
  • Another concrete, reproducible artifact — a command + output that demonstrates the defect (a crash, a probe failure, a reproducible bad response). An assertion, opinion, or architectural preference with no test and no reproducible command output is not evidence, however well-reasoned.

The gate

  • New-scope finding with deterministic evidence → 🛑 Blocker, unchanged. This includes evidenced security findings — they are preserved and still block.
  • New-scope finding without deterministic evidence → downgrade out of the blocker set. Record it in the advisory: frontmatter list (parallel to the existing Step 9b deferred: list) with its reasoning intact. It does not count toward Step 9 Rule 1's gaps_found trigger and does not revert a completed must-have or, on its own, justify another /gsd:plan-phase --gaps cycle.

This changes nothing else: a carried-forward gap or a regression still blocks with or without a pre-existing requirement to point at, and every non-Step-7 trigger (FAILED truth, MISSING/STUB artifact, NOT_WIRED link) is untouched, since those can never be new-scope in the first place.

What this deliberately does NOT implement

The issue as filed proposed a broader rule: a finding is advisory whenever it is untraceable to a requirement/decision/prior-gap (conditions A and B), regardless of evidence. The maintainer approved condition C only — evidence, not contract-traceability, is the bar. A finding with no pre-existing requirement to point at but with a real failing test still blocks. Do not implement A/B: that would demote a genuine, reproducible defect to advisory purely for being newly discovered, which is exactly the deferral this project's no-defer rule forbids. This gate narrows when a blocker needs proof, not what counts as in scope.

Advisory frontmatter

advisory: # Only if new-scope findings lack deterministic evidence (Step 7)
  - finding: "Short description of the new-scope concern"
    category: architectural | security | other
    reason: "Why this was raised; what would resolve it"
    evidence_status: "none provided" # or cite what was attempted but inconclusive

Report section

### Advisory (New Scope, Unevidenced)

New-scope findings from Step 7 with no deterministic evidence — reported,
not blocking, do not revert a completed must-have.

| # | Finding | Category | Why Advisory |
|---|---------|----------|--------------|
| 1 | {finding} | {category} | new-scope, no deterministic evidence |

Include this section (even if empty, stating "None") whenever is_re_verification = true ran — an omitted section reads as "not checked," not "checked and clean."

Worked example (from the issue's reported incident)

Prior pass: gaps_found, 4 items — all closed by approved gap-closure plans, re-verification begins.

  • Finding: "the retry loop's backoff strategy is architecturally fragile under sustained load." Not in the prior gaps: list. The flagged file was last modified 3 weeks before this verification pass (before the gap-closure plans even started) — not a regression. No test run, no reproducible command demonstrating a failure. → advisory, does not block, does not revert the 4 closed gaps.
  • Finding: TBD: handle the timeout case left in a file the gap-closure plan edited this pass. → self-evidencing debt marker, unaffected by this gate, blocks exactly as it always has.
  • Finding: a previously-closed gap's file now fails the SAME named test that originally proved it broken. → carried-forward gap, blocks.