diff --git a/.agents/skills/multi-review/protocol/multi-review.md b/.agents/skills/multi-review/protocol/multi-review.md index 0e74964..49c893d 100644 --- a/.agents/skills/multi-review/protocol/multi-review.md +++ b/.agents/skills/multi-review/protocol/multi-review.md @@ -289,7 +289,10 @@ unrelated to those rows. Its findings still merge normally way a provider's findings are), so a `defect` still reaches the human gate even from an otherwise-incomplete turn. -Each round records a durable coverage line alongside the round's quarantine records: +**Round 1 records a durable coverage line** alongside the round's quarantine records — round 1, +not every round: like the symbol-check pass below, this one is derived and dispatched once per +review, because the pairs it compares mostly live in unedited text that a diff-scoped round-N copy +does not carry. > [crossref-coverage: not applicable] > [crossref-coverage: / rows verdicted] @@ -358,7 +361,8 @@ An exit-1 turn is **reported, never quarantined**, for the same reason as above, merge normally (`multi-review-star.sh merge --pass ".symcheck"`, namespaced `symcheck-rd-`). -Each round records a durable coverage line alongside the round's quarantine records: +**Round 1 records a durable coverage line** alongside the round's quarantine records — round 1, +not every round: this pass is derived and dispatched once per review, not once per round. > [symcheck-coverage: not applicable] > [symcheck-coverage: / rows verdicted] @@ -368,6 +372,26 @@ Each round records a durable coverage line alongside the round's quarantine reco `Symbol-check pass: / rows verdicted`, or `Symbol-check pass: / rows verdicted — INCOMPLETE`. +**Every round records what the pass therefore did not see** (#99), written by the primary at the +END of its own turn — after the edits that address that round's findings, before it touches the +marker. Because the pass runs in round 1 only, its count covers the document as the secondaries +first read it, not the ready-to-paste code the author adds afterwards while fixing findings, which +is the least-reviewed code in the change by construction. Round 1 records one too: its own fixes +are unchecked for exactly the same reason, and a review that converges at round 1 is the commonest +shape of the gap. Each round records: + + > [symcheck-added: ] + +`` is the doc's current `rows` count minus round 1's `` (`0` when that line said `not +applicable`), floored at `0`. The gate renders it under the coverage line — `— round 1 only: more block(s) now than were checked, so at least are +unchecked`, or `— round 1 only: no net change in block count since (a one-for-one swap would not +show here)`, and `— round 1 only: NO RECORD of whether blocks were added since` when no round +recorded one at all. A document where the pass never applied renders none of this. + +`` is a **count, not an identity diff**: rows are positional (`B`) and carry line ranges that +shift on any edit above them, so a round that replaces one block with another nets zero. That blind +spot is why the zero wording names it rather than claiming nothing changed. + **Known limits of the introduces set**, in both directions. It harvests the backticked tokens from `- Create:`/`- Produces:` lines, and a block ends only at a blank line — so a prose-heavy entry contributes grammar fragments, and an entry that follows the block with no intervening blank is diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3f3fbd9..373656b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "multi-review", "description": "Human-gated star review: Claude (the neutral primary) fans a spec/plan/PR out to N independent secondary reviewers (in-harness fable by default — disable with MULTI_REVIEW_FABLE=off — plus cross-vendor codex/gemini), adjudicates their findings, and converges — always ending at a human approval gate.", - "version": "1.35.0", + "version": "1.36.0", "author": { "name": "Agrology" }, diff --git a/commands/multi-review.md b/commands/multi-review.md index 0727021..8d965c6 100644 --- a/commands/multi-review.md +++ b/commands/multi-review.md @@ -895,6 +895,7 @@ re-resolve later (a mutable env var could otherwise swap providers mid-review un `> [symcheck-coverage: / rows verdicted]`. Never optional: an unrecorded exit-1 turn is byte-identical at the gate to a review where this pass was never wired up. - **Exit 2** → a usage/infra error on YOUR side. Fix the invocation; nothing is recorded. + **Plan-lint coverage, in this same step — EVERY round, not round 1 only.** The fan-out's lint ran this round (it blocks the fan-out until it exits 0 or 3), so record which. If it exited 3, append `> [planlint-coverage: not applicable]` under ``'s `## Review` heading, alongside @@ -1039,6 +1040,42 @@ re-resolve later (a mutable env var could otherwise swap providers mid-review un while any resolved record lacks one, in every round: a record names an earlier-round finding by rule, so its own round cannot be read from the id, and it is your claim regardless. + **Record what the symbol-check pass did not see — EVERY round, and here, not in the fan-out + (#99).** That pass is derived and dispatched in ROUND 1 ONLY, so it covers the document as the + secondaries first read it. Every ready-to-paste block you have just added while addressing this + round's findings is unchecked against the repository, and that is the least-reviewed code in the + change by construction. Left unrecorded, round 1's clean `/` is all the gate renders, and a + review that added unchecked code reads exactly like one that added none. + + **This belongs at the END of your turn, not in the fan-out.** The fan-out runs BEFORE you edit + the body, so a count taken there predates this round's own fixes — and on the final round those + are precisely the blocks nobody will ever check. Take it after your last edit and before you + touch the marker, every round including round 1: round 1's fixes are unchecked too, and a review + that converges at round 1 would otherwise record nothing at all. + + ${CLAUDE_PLUGIN_ROOT}/scripts/multi-review-symcheck.sh rows "" | wc -l + + Let `` be the total from round 1's `symcheck-coverage` line — `` is `0` when that line said + `not applicable`, which is the doc that shipped no blocks and has since gained some. Let `` be + the count above minus ``, floored at `0`. An exit 3 from `rows` means the current count is + `0`, not that the step is skipped. Append, under ``'s `## Review` heading: + + > [symcheck-added: ] + + Record it when `` is `0` too — "I checked and nothing was added" and "nobody looked" are + different facts, and the gate renders them differently. **Exactly one record per round**: the + record carries no round of its own, so the gate reads a count below the marker's round as a + round that never looked, and renders the number STALE rather than as an all-clear. + + **Skip this entirely on a PR-flavor doc.** You never edit a PR diff, so no block can be added to + it, and `rows` is not applicable to a scratch by construction. The gate is dormant there too; + recording a ritual `0` every round would be noise that looks like a check. + + **`` is a COUNT, not an identity diff, and the gate says so.** Rows are positional (`B`) + and carry line ranges that shift on any edit above them, so a round that REPLACES one block with + another nets zero here and reports no change. Closing that needs a stable per-block identity — + option 2 in #99 — and neither this instruction nor the gate wording may imply it exists. + 5. Decide: **converge**, or re-enter `awaiting-secondaries` for another round. First run `${CLAUDE_PLUGIN_ROOT}/scripts/multi-review-star.sh round-stats ""`. It prints diff --git a/docs/multi-review.md b/docs/multi-review.md index 0e74964..49c893d 100644 --- a/docs/multi-review.md +++ b/docs/multi-review.md @@ -289,7 +289,10 @@ unrelated to those rows. Its findings still merge normally way a provider's findings are), so a `defect` still reaches the human gate even from an otherwise-incomplete turn. -Each round records a durable coverage line alongside the round's quarantine records: +**Round 1 records a durable coverage line** alongside the round's quarantine records — round 1, +not every round: like the symbol-check pass below, this one is derived and dispatched once per +review, because the pairs it compares mostly live in unedited text that a diff-scoped round-N copy +does not carry. > [crossref-coverage: not applicable] > [crossref-coverage: / rows verdicted] @@ -358,7 +361,8 @@ An exit-1 turn is **reported, never quarantined**, for the same reason as above, merge normally (`multi-review-star.sh merge --pass ".symcheck"`, namespaced `symcheck-rd-`). -Each round records a durable coverage line alongside the round's quarantine records: +**Round 1 records a durable coverage line** alongside the round's quarantine records — round 1, +not every round: this pass is derived and dispatched once per review, not once per round. > [symcheck-coverage: not applicable] > [symcheck-coverage: / rows verdicted] @@ -368,6 +372,26 @@ Each round records a durable coverage line alongside the round's quarantine reco `Symbol-check pass: / rows verdicted`, or `Symbol-check pass: / rows verdicted — INCOMPLETE`. +**Every round records what the pass therefore did not see** (#99), written by the primary at the +END of its own turn — after the edits that address that round's findings, before it touches the +marker. Because the pass runs in round 1 only, its count covers the document as the secondaries +first read it, not the ready-to-paste code the author adds afterwards while fixing findings, which +is the least-reviewed code in the change by construction. Round 1 records one too: its own fixes +are unchecked for exactly the same reason, and a review that converges at round 1 is the commonest +shape of the gap. Each round records: + + > [symcheck-added: ] + +`` is the doc's current `rows` count minus round 1's `` (`0` when that line said `not +applicable`), floored at `0`. The gate renders it under the coverage line — `— round 1 only: more block(s) now than were checked, so at least are +unchecked`, or `— round 1 only: no net change in block count since (a one-for-one swap would not +show here)`, and `— round 1 only: NO RECORD of whether blocks were added since` when no round +recorded one at all. A document where the pass never applied renders none of this. + +`` is a **count, not an identity diff**: rows are positional (`B`) and carry line ranges that +shift on any edit above them, so a round that replaces one block with another nets zero. That blind +spot is why the zero wording names it rather than claiming nothing changed. + **Known limits of the introduces set**, in both directions. It harvests the backticked tokens from `- Create:`/`- Produces:` lines, and a block ends only at a blank line — so a prose-heavy entry contributes grammar fragments, and an entry that follows the block with no intervening blank is diff --git a/scripts/multi-review-mutation-check.sh b/scripts/multi-review-mutation-check.sh index 4305a4f..037998b 100755 --- a/scripts/multi-review-mutation-check.sh +++ b/scripts/multi-review-mutation-check.sh @@ -2139,6 +2139,50 @@ mutations() { ' round N ≥ 2, skip this sub-step entirely** — steps 4, 5, 7 and 8'"'"'s symcheck clauses then have' \ ' steps 4, 5, 7 and 8'"'"'s symcheck clauses then have' + # (#99) The pass is ROUND 1 ONLY (the two entries above pin that, deliberately). What was missing + # is any record that later rounds went unchecked: round 1's clean `/` was all the gate saw, + # so a review that added unchecked code rendered identically to one that added none. Three + # separate guards — the instruction to record it, how `` is derived, and the honesty about + # what a COUNT cannot see — each with its own named assertion. + mutate 'command/symcheck-added-recorded' 'commands/multi-review.md' replace \ + 'no round records blocks added after round 1' 'multi-review-packaging.test.sh' \ + ' > [symcheck-added: ]' \ + ' > [symcheck-noted]' + mutate 'command/symcheck-added-derivation' 'commands/multi-review.md' replace \ + 'nothing says how is derived' 'multi-review-packaging.test.sh' \ + ' the count above minus ``, floored at `0`. An exit 3 from `rows` means the current count is' \ + ' the count above minus ``. An exit 3 from `rows` means the current count is' + + # (fable-rd2-r2) The gate keys STALE off "fewer records than rounds", so the instruction must + # promise exactly one per round — without it a primary recording on its own cadence trips a + # warning it was never told how to avoid. + mutate 'command/symcheck-added-one-per-round' 'commands/multi-review.md' replace \ + 'nothing states the one-per-round rule' 'multi-review-packaging.test.sh' \ + ' different facts, and the gate renders them differently. **Exactly one record per round**: the' \ + ' different facts, and the gate renders them differently. The' + + # (fable-rd2-r1) ...and a PR-flavor primary must be told to skip, matching the gate's dormancy. + mutate 'command/symcheck-added-skip-on-pr' 'commands/multi-review.md' replace \ + 'told to record a count that cannot mean anything' 'multi-review-packaging.test.sh' \ + ' **Skip this entirely on a PR-flavor doc.** You never edit a PR diff, so no block can be added to' \ + ' You never edit a PR diff, so no block can be added to' + + # (fable-rd1-r1/r4) The paragraph must stay IN THE PRIMARY TURN and say EVERY round. Recorded in + # the fan-out it runs before the body edits, so the count predates this round's own fixes — and + # on the final round those are exactly the blocks nobody checks. The packaging window is scoped + # to the Primary turn section, so weakening this phrase is what the assertion sees. + mutate 'command/symcheck-added-in-primary-turn' 'commands/multi-review.md' replace \ + 'nothing pins the record to the primary turn' 'multi-review-packaging.test.sh' \ + ' **Record what the symbol-check pass did not see — EVERY round, and here, not in the fan-out' \ + ' **Record what the symbol-check pass did not see' + + # The blind spot must stay stated. A reader who believes `` detects any change will read a + # zero as "nothing happened", when a one-for-one block swap produces exactly that zero. + mutate 'command/symcheck-added-count-not-identity' 'commands/multi-review.md' replace \ + ' is presented as if it detected any change' 'multi-review-packaging.test.sh' \ + ' **`` is a COUNT, not an identity diff, and the gate says so.** Rows are positional (`B`)' \ + ' **`` has a blind spot.** Rows are positional (`B`)' + mutate 'command/symcheck-not-a-secondary' 'commands/multi-review.md' replace \ 'nothing says the symcheck pass is not a secondary' 'multi-review-packaging.test.sh' \ ' argument, so **the symcheck pass is not a secondary**: like the crossref pass it is excluded' \ @@ -2581,6 +2625,45 @@ mutations() { " [[ \"\$nfoot\" -eq 0 ]] || die \"merge: '\${doc}' carries \${nfoot} already-merged round(s) but '\${doc}.manifest' is missing — refusing to merge onto rounds it cannot verify. Restore the manifest, or rebuild it from the doc's '' footers (one 'finding =' line per entry, every round, in document order, PLUS one 'quarantine =' line for each entry in a footer's 'quarantined:' list — verify binds these by hash, so the key needs no round suffix) and re-run\" 1" \ " [[ \"\$nfoot\" -eq 0 ]] || die \"merge: '\${doc}' carries \${nfoot} already-merged round(s) but '\${doc}.manifest' is missing — refusing to merge onto rounds it cannot verify. Restore the manifest, or rebuild it from the doc's '' footers (one 'finding =' line per entry, every round, in document order) and re-run\" 1" + # (#99) ...and the gate must RENDER it. FIVE outcomes, five entries: a positive count, a zero that + # does not overclaim, a NO RECORD when nothing was written, a STALE when some round forgot, and + # silence on a PR scratch. This comment has now been wrong about the count twice — "three + # branches" (fable-rd1-r3 found the zero branch uncovered) and then four (fable-rd2-r1/r2 added + # the flavor and staleness branches). Count the `echo`s before trusting it again. + mutate 'star/gate-symcheck-added-rendered' 'scripts/multi-review-star.sh' replace \ + 'round-1-only gap is silent' 'multi-review-star.test.sh' \ + ' echo " — round 1 only: ${sadd} more block(s) now than were checked, so at least ${sadd} are unchecked"' \ + ' :' + mutate 'star/gate-symcheck-added-no-record' 'scripts/multi-review-star.sh' replace \ + 'an unrecorded later round is silent' 'multi-review-star.test.sh' \ + ' echo " — round 1 only: NO RECORD of whether blocks were added since"' \ + ' :' + # (fable-rd1-r3) The ZERO branch needed its own entry. The rendering has four outcomes, not the + # three the first cut of this comment claimed, and the one it left uncovered is the anti-overclaim + # wording the whole design leans on: a COUNT cannot see a one-for-one swap, so a bare "no change" + # here would assert more than the number knows. + mutate 'star/gate-symcheck-added-zero' 'scripts/multi-review-star.sh' replace \ + 'zero added-count rendered nothing or overclaimed' 'multi-review-star.test.sh' \ + ' echo " — round 1 only: no net change in block count since (a one-for-one swap would not show here)"' \ + ' :' + + # (fable-rd2-r1) A PR scratch must stay silent, and the COVERAGE ENCLOSURE DOES NOT DO THAT — the + # first cut of this entry said it did, which was simply false: `rows` exits 3 on a scratch, the + # round records `not applicable`, and a non-empty `sx` reaches the block. Only the flavor branch + # keeps it out. The primary never edits a PR diff, so the warning could never be true there. + mutate 'star/gate-symcheck-added-dormant-on-pr' 'scripts/multi-review-star.sh' replace \ + 'was warned about unchecked blocks it cannot have' 'multi-review-star.test.sh' \ + ' if [[ "$(_doc_flavor "$doc")" != pr ]]; then' \ + ' if true; then' + + # (fable-rd2-r2) A count that is not current is worse than none — it reads as an all-clear. The + # record carries no round, and the reader is `tail -1`, so without this a round that forgot to + # record renders an earlier round's number as this round's fact: #99's own gap, one round down. + mutate 'star/gate-symcheck-added-stale' 'scripts/multi-review-star.sh' replace \ + 'rendered as current on a round-3 review' 'multi-review-star.test.sh' \ + ' elif [[ "$srd" =~ ^[0-9]+$ ]] && (( sn < srd )); then' \ + ' elif false; then' + # gate-summary renders the claim BEFORE the human approves the publish. Nothing can mechanically # verify a prose finding was fixed, so the gate is the only thing standing behind it — a silent # gate summary means the human approves a claim they were never shown. diff --git a/scripts/multi-review-packaging.test.sh b/scripts/multi-review-packaging.test.sh index 5c74fac..594cbaf 100755 --- a/scripts/multi-review-packaging.test.sh +++ b/scripts/multi-review-packaging.test.sh @@ -1167,6 +1167,44 @@ else fi +# --- symcheck's round-1-only gap must be RECORDED, and recorded in the primary turn (#99) --- +# The window is the assertion. In the fan-out this paragraph would run BEFORE the body edits, so +# the count would predate the round's own fixes — and on the final round those are exactly the +# blocks nobody checks (fable-rd1-r1). It would also sit under step 8's "skip entirely on round +# N >= 2" header, which the paragraph itself contradicts (fable-rd1-r4). +pt="$(grep -n '^#### Primary turn' "$CMD" | head -1 | cut -d: -f1)" +pte="$(awk -v s="$pt" 'NR>s && /^### /{print NR; exit}' "$CMD")" +if [[ -z "$pt" || -z "$pte" ]]; then + bad "command: the Primary turn section could not be located, so #99's record cannot be scoped" +else + ptblk="$(sed -n "${pt},$((pte-1))p" "$CMD")" + grep -qF 'symcheck-added: ]' <<<"$ptblk" \ + && ok "command: the primary records the count of blocks the symcheck pass never saw (#99)" \ + || bad "command: no round records blocks added after round 1 — the gate cannot show the gap" + grep -qF 'EVERY round, and here, not in the fan-out' <<<"$ptblk" \ + && ok "command: the record is pinned to every round and to the primary turn (fable-rd1-r1)" \ + || bad "command: nothing pins the record to the primary turn, so a fan-out count predates the round's own fixes" + grep -qF 'floored at `0`' <<<"$ptblk" \ + && ok "command: 's derivation names and the floor unambiguously (fable-rd1-r2)" \ + || bad "command: nothing says how is derived, so the count is left to invention" + # The wording must not overclaim: a COUNT cannot see a one-for-one swap, and a reader who thinks + # it can will trust a zero that means nothing. + grep -qF 'COUNT, not an identity diff' <<<"$ptblk" \ + && ok "command: the added-count's blind spot (a swapped block) is stated (#99)" \ + || bad "command: is presented as if it detected any change, which it does not" + # The gate reads "fewer records than rounds" as a round that never looked, so the instruction has + # to promise exactly one per round — otherwise a conscientious primary that records twice in one + # round, or once every other round, trips a warning it was never told how to avoid. + grep -qF 'Exactly one record per round' <<<"$ptblk" \ + && ok "command: exactly one record per round is stated, which is what STALE keys off (fable-rd2-r2)" \ + || bad "command: nothing states the one-per-round rule the gate's staleness check depends on" + # And a PR scratch must be told to skip, or the primary records a ritual zero every round that + # looks like a check and can never mean anything. + grep -qF 'Skip this entirely on a PR-flavor doc' <<<"$ptblk" \ + && ok "command: the record is skipped on a PR scratch, matching the gate (fable-rd2-r1)" \ + || bad "command: a PR-flavor primary is told to record a count that cannot mean anything" +fi + # --- the symcheck pass must be derived, dispatched, merged, checked and announced --- # Patterns are the SYMCHECK-prefixed forms, never the bare `/ rows verdicted]` the plan # sketched: that one now has two matches in this file (crossref writes the same shape), so it is diff --git a/scripts/multi-review-star.sh b/scripts/multi-review-star.sh index d6da13b..da5ae3b 100755 --- a/scripts/multi-review-star.sh +++ b/scripts/multi-review-star.sh @@ -214,6 +214,31 @@ _symcheck_coverage() { # -> "not applicable" | "N/M rows verdicted" | tail -1 } +# _symcheck_added — the durable count of ready-to-paste blocks the doc has gained since the +# symbol-check pass ran (#99). The pass is round 1 only, so a block added while fixing a finding is +# never checked; the primary records this each later round and the gate renders it beside the +# coverage line. Read like its siblings, last one wins. +# +# It is a COUNT, not an identity diff: rows are positional (`B`) and carry line ranges that +# shift on any edit above them, so a round that swaps one block for another nets zero here. Closing +# that needs a stable per-block identity — option 2 in #99 — and the rendering below is worded so +# the gate does not imply this already has one. +_symcheck_added() { # -> "" + review_section "$1" | strip_fences /dev/stdin \ + | grep -oE '^> \[symcheck-added: [0-9]+\]' \ + | sed -E 's/^> \[symcheck-added: ([0-9]+)\]$/\1/' \ + | tail -1 +} + +# _symcheck_added_count -> how many rounds recorded one. The record carries no round of its +# own and the reader above is `tail -1`, so a later round that FORGOT to record would render an +# earlier round's count as current — #99's own gap, moved one round down (fable-rd2-r2). Exactly one +# record per round is mandated, so a count below the marker's round is a derivable "nobody looked". +_symcheck_added_count() { # -> "" + review_section "$1" | strip_fences /dev/stdin \ + | grep -cE '^> \[symcheck-added: [0-9]+\]' +} + # _planlint_coverage — the plan lint's durable line (spec B5), read the way the two pass # readers above read theirs. Records that the lint RAN this round; passing is what the blocking # fan-out step guarantees. @@ -1786,6 +1811,38 @@ cmd_gate_summary() { echo "Symbol-check pass: ${sx} — INCOMPLETE" fi fi + # (#99) The pass runs in ROUND 1 ONLY, so the count above covers the document as the + # secondaries first read it — not the blocks the author added afterwards while fixing their + # findings, which are the least-reviewed code in the change by construction. Say which. + # Rendered for the not-applicable case too: a round-1 doc with no blocks that GAINS one is + # exactly this gap, and the coverage line alone would read as "nothing to check". + # + # NOT conditioned on the round (fable-rd1-r1). The primary records this at the END of its own + # turn, so EVERY round has one to record — round 1 included, whose fixes are unchecked for + # exactly the same reason. Gating the NO RECORD on round > 1 hid the commonest case of the gap: + # a review that converges at round 1 and never records anything rendered as if all was checked. + # + # DORMANT ON A PR SCRATCH (fable-rd2-r1). `rows` is not applicable to one by construction — no + # `**Files:**` sections — so the round records `not applicable`, which is NON-EMPTY and reaches + # here: the coverage enclosure above does NOT keep PR scratches out, only this does. The primary + # never edits a PR diff, so no block can be added to it and the warning could never be true. + # The plan-lint block below branches on the same flavor, for the same kind of reason (#118). + if [[ "$(_doc_flavor "$doc")" != pr ]]; then + local sadd sn srd + sadd="$(_symcheck_added "$doc")" + sn="$(_symcheck_added_count "$doc")" + srd="$("${STAR_DIR}/multi-review-core.sh" marker "$doc" 2>/dev/null | awk '{print $2}')" + if [[ -z "$sadd" ]]; then + echo " — round 1 only: NO RECORD of whether blocks were added since" + elif [[ "$srd" =~ ^[0-9]+$ ]] && (( sn < srd )); then + # A count that is not current is worse than none: it reads as an all-clear. + echo " — round 1 only: the block count is STALE — ${sn} of ${srd} round(s) recorded one" + elif [[ "$sadd" == "0" ]]; then + echo " — round 1 only: no net change in block count since (a one-for-one swap would not show here)" + else + echo " — round 1 only: ${sadd} more block(s) now than were checked, so at least ${sadd} are unchecked" + fi + fi echo fi diff --git a/scripts/multi-review-star.test.sh b/scripts/multi-review-star.test.sh index a4d1d38..69d1f81 100644 --- a/scripts/multi-review-star.test.sh +++ b/scripts/multi-review-star.test.sh @@ -3006,6 +3006,91 @@ grep -qF 'symcheck-rd1-r1' <<<"$out" \ && ok "gate-summary: the pass's findings are still present" \ || bad "gate-summary: the exclusion suppressed the findings along with the raiser — criterion 11" +# --- symcheck runs round 1 only, so later rounds' blocks are UNCHECKED (#99) --- +# The pass is derived and dispatched in round 1 only, so a ready-to-paste block the author adds +# while fixing a finding is never checked against the repo — the least-reviewed code in the change +# by construction. The gate must say so rather than render round 1's clean count alone, which is +# indistinguishable from a review where nothing was added. +D="$(mkstar sym-added.md \ + '> [symcheck-coverage: 4/4 rows verdicted]' \ + '> [symcheck-added: 2]')" +out="$(bash "$SUT" gate-summary "$D" claude-opus-5 2>/dev/null)" +grep -qF 'more block(s) now than were checked' <<<"$out" \ + && ok "gate-summary: blocks added after round 1 are reported as unchecked (#99)" \ + || bad "gate-summary: round-1-only gap is silent — 4/4 reads as fully checked: $out" + +# Zero is recorded and rendered too, and the wording must NOT overclaim: this is a COUNT, not an +# identity diff, so a round that swapped one block for another nets zero and would show here as +# no change. Option 2 in #99 (a stable per-block identity) is what would close that; the gate must +# not imply it already has. +D="$(mkstar sym-added0.md \ + '> [symcheck-coverage: 4/4 rows verdicted]' \ + '> [symcheck-added: 0]')" +out="$(bash "$SUT" gate-summary "$D" claude-opus-5 2>/dev/null)" +grep -qF 'no net change in block count' <<<"$out" \ + && ok "gate-summary: a zero added-count renders, and says a swap would not show (#99)" \ + || bad "gate-summary: zero added-count rendered nothing or overclaimed: $out" + +# A multi-round review that recorded NOTHING is the failure this feature exists to make visible — +# byte-identical at the gate to a single-round review without it, unless the gate derives it. +SA2="${WORK}/sym-added-norecord.md" +{ echo "# Doc"; echo '' + echo ""; echo; echo "## Review"; echo + printf '%s\n' '> [symcheck-coverage: 4/4 rows verdicted]'; } > "$SA2" +out="$(bash "$SUT" gate-summary "$SA2" claude-opus-5 2>/dev/null)" +grep -qF 'NO RECORD of whether blocks were added' <<<"$out" \ + && ok "gate-summary: a round-2 review with no added-count is reported NO RECORD (#99)" \ + || bad "gate-summary: an unrecorded later round is silent: $out" + +# ...including a review that converged at ROUND 1 (fable-rd1-r1). The pass runs at round 1's +# fan-out, so the primary's own fixes are unchecked in every round — round 1 included. Treating a +# one-round review as "nothing to report" would hide the most common case of the gap entirely. +SA1="${WORK}/sym-added-rd1.md" +{ echo "# Doc"; echo '' + echo ""; echo; echo "## Review"; echo + printf '%s\n' '> [symcheck-coverage: 4/4 rows verdicted]'; } > "$SA1" +out="$(bash "$SUT" gate-summary "$SA1" claude-opus-5 2>/dev/null)" +grep -qF 'NO RECORD of whether blocks were added' <<<"$out" \ + && ok "gate-summary: a round-1 review with no added-count is NO RECORD too (fable-rd1-r1)" \ + || bad "gate-summary: a single-round review hid the gap — its own fixes are unchecked: $out" + +# A PR scratch renders NONE of this (fable-rd2-r1). `symcheck.sh rows` exits 3 on a PR scratch by +# construction — pr.sh writes no `**Files:**` sections — so the round records +# `[symcheck-coverage: not applicable]`, which is NON-EMPTY and therefore reaches the block below. +# The coverage enclosure alone does NOT keep PR scratches out; only the flavor does. The primary +# never edits a PR diff, so no block can be added to it and the warning could never be true. +SA0="${WORK}/sym-added-pr.md" +{ echo "# PR review: something"; echo '' + echo ""; echo + echo '- **PR:** https://github.com/o/r/pull/1'; echo; echo "## Review"; echo + printf '%s\n' '> [symcheck-coverage: not applicable]'; } > "$SA0" +out="$(bash "$SUT" gate-summary "$SA0" claude-opus-5 2>/dev/null)" +grep -qF 'round 1 only:' <<<"$out" \ + && bad "gate-summary: a PR scratch was warned about unchecked blocks it cannot have: $out" \ + || ok "gate-summary: the added-count line is dormant on a PR scratch (fable-rd2-r1)" + +# A record carries no round and the reader is `tail -1`, so a later round that FORGOT to record +# would render an earlier round's count as current — the #99 gap moved one round down. Exactly one +# record per round is mandated, so fewer records than the marker's round is a derivable gap. +SA3="${WORK}/sym-added-stale.md" +{ echo "# Doc"; echo '' + echo ""; echo; echo "## Review"; echo + printf '%s\n' '> [symcheck-coverage: 4/4 rows verdicted]' '> [symcheck-added: 0]'; } > "$SA3" +out="$(bash "$SUT" gate-summary "$SA3" claude-opus-5 2>/dev/null)" +grep -qF 'STALE' <<<"$out" \ + && ok "gate-summary: fewer records than rounds renders STALE, not the old count (fable-rd2-r2)" \ + || bad "gate-summary: a round-1 record rendered as current on a round-3 review: $out" + +# ...and a review whose every round DID record renders the value, not STALE. +SA4="${WORK}/sym-added-current.md" +{ echo "# Doc"; echo '' + echo ""; echo; echo "## Review"; echo + printf '%s\n' '> [symcheck-coverage: 4/4 rows verdicted]' '> [symcheck-added: 0]' '> [symcheck-added: 3]'; } > "$SA4" +out="$(bash "$SUT" gate-summary "$SA4" claude-opus-5 2>/dev/null)" +grep -qF '3 more block(s)' <<<"$out" && ! grep -qF 'STALE' <<<"$out" \ + && ok "gate-summary: one record per round renders the latest count (fable-rd2-r2)" \ + || bad "gate-summary: a complete record set was reported stale or lost its count: $out" + # --- round-stats is the OTHER STAR_PASSES consumer, and the one #90 shipped without --- # gate-summary and round-stats read the same string but in different code; in the #90 build the # round-stats consumer was missed entirely and the pass got its own provider column plus a vote in