From 1b23f8188c54f2e90cff23c178106b3b3a940562 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 31 Oct 2026 17:50:15 +0000 Subject: [PATCH] feat(ship): judge may dismiss a tagged closing summary on record (GST-435 Part 2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-review-dispatch | 8 ++- changelog.d/gst-435-summary-candidate.md | 11 ++++ docs/review-policy.md | 17 +++++- lib/review-ledger.ts | 76 ++++++++++++++++++++---- test/review-dispatch.test.ts | 48 ++++++++++++++- 5 files changed, 142 insertions(+), 18 deletions(-) create mode 100644 changelog.d/gst-435-summary-candidate.md diff --git a/bin/gstack-review-dispatch b/bin/gstack-review-dispatch index d7dd38f51..9b6952704 100755 --- a/bin/gstack-review-dispatch +++ b/bin/gstack-review-dispatch @@ -48,8 +48,9 @@ const HELP = `gstack-review-dispatch — verbatim reviewer briefs and loss-free until the brief is re-sent and answered with a review, which voids it. --unavailable '' (the outside voice only; refused when the output holds findings) records its failure text as missing coverage: COLLECT_UNAVAILABLE, and verify prints UNAVAILABLE and unavailable=N, never a review judge --ledger --merged --judged - judged lines {"id","severity","reason"?,"duplicate_of"?}: every merged id kept at its severity or above (a - duplicate folds into one at least as severe); added and promoted findings need a reason. JUDGMENT: ok …, or + judged lines {"id","severity","reason"?,"duplicate_of"?,"dismissed"?:{"quote","reason"}}: every merged id kept at its + severity or above (a duplicate folds into one at least as severe); added and promoted findings need a reason; only a + summary_candidate line may be dismissed, quoted byte for byte with a reason (verify prints DISMISSED: lines). JUDGMENT: ok …, or JUDGMENT_REJECTED: dropped=… downgraded=… (exit 1) verify --ledger [--targeted-out ] BRIEF, OUTPUT and JUDGE rows from the ledger alone, then VERIFY: clean (exit 0), VERIFY: dirty (exit 1) or @@ -290,13 +291,14 @@ function main(): number { const judged = flag('--judged'); if (!merged || !judged) { process.stderr.write('gstack-review-dispatch judge: needs --merged and --judged \n'); return EXIT.usage; } const r = recordJudgment(ledger, readJsonl(merged), readJsonl(judged), judged, merged); - process.stdout.write(r.ok ? `JUDGMENT: ok added=${r.added.length} promoted=${r.promoted.length} folded=${r.folded.length}\n` + process.stdout.write(r.ok ? `JUDGMENT: ok added=${r.added.length} promoted=${r.promoted.length} folded=${r.folded.length}${r.dismissed.length ? ` dismissed=${r.dismissed.length}` : ''}\n` : `JUDGMENT_REJECTED: dropped=${JSON.stringify(r.dropped)} downgraded=${JSON.stringify(r.downgraded)} unexplained=${JSON.stringify(r.unexplained)} invalid=${JSON.stringify(r.invalid)} (lead judgment may add or promote a finding, never drop or downgrade one)\n`); return r.ok ? EXIT.ok : EXIT.fail; } if (cmd === 'verify') { const v = verify(ledger); for (const r of v.rows) process.stdout.write(`${r.kind} ${r.reader} ${r.status} ${r.detail}\n`); + for (const d of v.dismissals) process.stdout.write(`DISMISSED: ${d.id} ${d.label} ${JSON.stringify(d.quote)}\n`); if (!v.briefs) { process.stdout.write('VERIFY: no brief records (this run\'s ledger kept no prompts; dispatched-vs-rendered cannot be checked)\n'); return EXIT.refused; } const record = targetedRecord(ledger, v.rows); if (record) { diff --git a/changelog.d/gst-435-summary-candidate.md b/changelog.d/gst-435-summary-candidate.md new file mode 100644 index 000000000..e87474edb --- /dev/null +++ b/changelog.d/gst-435-summary-candidate.md @@ -0,0 +1,11 @@ +**/ship's lead judgment can now dismiss a reader's own closing summary, on record.** HARN-1 paid for extra review passes because lines like "I found three INFORMATIONAL issues and nothing CRITICAL." collected as CRITICAL findings that `judge` would not let go. Collect still keeps every label, but it now tags a line `summary_candidate` when one of its clauses matches a closed list of closing-summary templates and nothing else on the line could be a finding. The judge may dismiss only a tagged line, quoting it byte for byte with a reason. `verify` re-derives each dismissal from the reader's unchanged output and prints a `DISMISSED:` line for it. + +### Known +- **A blocker sentence with no label, file:line or "block" word after a summary is tagged.** "Neither finding is CRITICAL. The token leak must be fixed." can be dismissed, and only the judge's recorded, quoted reason covers it. "The token leak blocks the merge." after the same summary is not tagged. +- HARN-15's "None of the fixes blocks landing; I found four INFORMATIONAL defects and no CRITICAL." is not tagged, because of its "block" word, so it stays a phantom. + +### Tests +- `test/review-dispatch.test.ts`: tagged and untagged lines, including the review rounds' lost-blocker shapes; a dismissal that is untagged, paraphrased, unreasoned or folded into is a drop; verify prints the dismissal and turns dirty when the output file changes. + +### Docs +- `docs/review-policy.md` (collect, judge, verify) describes the tag, the dismissal rule and the residual risk. diff --git a/docs/review-policy.md b/docs/review-policy.md index d526247bf..cafe16f60 100644 --- a/docs/review-policy.md +++ b/docs/review-policy.md @@ -470,7 +470,10 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends: wherever they stand, headings and table rows included; a top-level list item under a label-only heading takes the stronger of the two). Every label word counts, a reader's own closing summary included ("I found nothing CRITICAL", "Neither finding is CRITICAL"): it - collects at its label and stays at that label in the judged set (GST-435). A `Severity:` or `Blocks:` line + collects at its label (GST-435). Collect also tags such a line `summary_candidate` when one of its clauses + matches a closed list of closing-summary templates and nothing else on it could be a finding. That means no + other label, no file:line, no "block" word, no `Blocks: yes`, no JSON and no joined field line. A tagged line still enters + the judged set at its label; only `judge` may dismiss it (step 4). A `Severity:` or `Blocks:` line continues the finding above it until a list item, heading, bold paragraph opener or a second line for the same field starts another: `Severity` raises its label, `Blocks: yes` makes it CRITICAL and `Blocks: no` never lowers it; an inline @@ -487,6 +490,13 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends: finding, lowers its severity, folds it into a less severe duplicate, or adds or promotes one without a reason (`JUDGMENT_REJECTED`). A merged file missing any id the ledger collected is a drop. Each addition and promotion is logged with its reason. + A judged line may carry `dismissed: {quote, reason}` only for a merged line that + `summaryCandidate` tags from its own raw text, quoted byte for byte, with a reason. + Any other dismissal is a drop, and so is a duplicate folded into a dismissed line. Each + dismissal is logged with its label, quote and reason. Known residual risk: a blocker + sentence with no label, file:line or "block" word after a summary ("Neither finding is + CRITICAL. The token leak must be fixed.") is tagged, so only the judge's recorded, + quoted reason stands between it and a dismissal. 5. `verify --ledger ` replays all of it from the run's ledger: rendered against dispatched hashes per render (each render is one dispatch of its reader, so a two-pass ledger checks pass-1 sends against pass-1 briefs; a later render prints as @@ -501,7 +511,10 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends: byte-exact before anything was collected is void and counted (`voided=N` on the verdict line); one that produced an output stays `MISMATCH`. Then output hashes against the merged lines they produced, and the judgment rerun against the - recorded judged set, covering every collected id. A ledger from a run that kept no + recorded judged set, covering every collected id. Each dismissal is re-derived from the + reader's unchanged output file as a tagged finding (`dismissal_not_rederived` otherwise); + the JUDGE row prints `dismissed=N` and verify prints one `DISMISSED: