fix(ship): pair ledger blockers by reviewer and per ledger; extensionless loci; pr-body names no targeted ledger (GST-951 round-2 findings, pending grant)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-02 07:54:44 +00:00
1 parent 49421ee874
commit 15823cc466
10 files changed
+89 -38

No files matched your search

@@ -1,2 +1,2 @@
- **The /ship receipt counts every blocker its readers found** (GST-951). `gstack-review-scope summary` read only the review-log rows, but /ship's readers report through the dispatch ledgers (`gstack-review-dispatch collect` and `judge`). So a blocker that only the native adversarial pass raised, a CRITICAL logged fixed without classify's blocking stamp, and findings logged before they were classified all left `blocking_found` at 0. That happened on #3841, #3838 and #3837. The summary now also reads each ledger's collected and judged CRITICAL findings, pairs each one with at most one row finding at the same path:line so a blocker held in both places counts once and one classified below the floor does not, and counts an unclassified CRITICAL row as blocking. `unrecorded=N` names blockers no row gives a disposition, and `ledgers_read=N` says how many it read.
- **The /ship receipt counts every blocker its readers found** (GST-951). `gstack-review-scope summary` read only the review-log rows, but /ship's readers report through the dispatch ledgers (`gstack-review-dispatch collect` and `judge`). So a blocker that only the native adversarial pass raised, a CRITICAL logged fixed without classify's blocking stamp, and findings logged before they were classified all left `blocking_found` at 0. That happened on #3841, #3838 and #3837. The summary now also reads each ledger's collected and judged CRITICAL findings, pairs each one with at most one row finding from the same reviewer at the same path:line so a blocker held in both places counts once and one classified below the floor does not, and counts an unclassified CRITICAL row as blocking. `unrecorded=N` names blockers no row gives a disposition, and `ledgers_read=N` says how many it read.
- **The summary refuses rather than undercount.** It now needs `--run <run dir>` (it finds every `review-ledger.jsonl` under it), `--ledger <f>`, or `--no-ledger` when no reader ran through `gstack-review-dispatch`. A missing choice, a run directory with no ledger, or an unreadable ledger, merged file or judged set, or a judged set changed since `judge`, is `REVIEW_SCOPE_LEDGER` with nothing written. /ship on Capy (`ship/DRIVER.md`) passes `--run`; the Claude Code flow passes `--no-ledger`.
+1 -1
View File
@@ -395,7 +395,7 @@ puts in the receipt:
| Field | Meaning |
|---|---|
| `review_loop.passes` | full review passes this ship ran (1 or 2; up to 3 under `review_scope: all`) |
| `review_loop.blocking_found` | every blocker found (GST-951): the rows' blocking findings (classified, or an unclassified CRITICAL), plus each ledger CRITICAL (as collected or judged) that no row finding at the same path:line accounts for (each row finding pairs with one), so an adversarial, outside-voice or not-yet-classified blocker counts and one whose newest classification is below the floor does not. An unreadable ledger, merged file or judged set, or a judged set changed since `judge`, is `REVIEW_SCOPE_LEDGER`, never a count |
| `review_loop.blocking_found` | every blocker found (GST-951): the rows' blocking findings (classified, or an unclassified CRITICAL), plus each ledger CRITICAL (as collected or judged) that no row finding from the same reviewer at the same path:line accounts for (each row finding pairs with one per ledger), so an adversarial, outside-voice or not-yet-classified blocker counts and one whose newest classification is below the floor does not. An unreadable ledger, merged file or judged set, or a judged set changed since `judge`, is `REVIEW_SCOPE_LEDGER`, never a count |
| `review_loop.unrecorded` | blockers found that no row gives a disposition (fixed, deferred, noted, skipped, a gate answer); absent when 0 |
| `review_loop.ledgers_read` | dispatch ledgers the count read; absent under `--no-ledger` |
| `review_loop.deferred` | blocking findings in unchanged code filed as issues (a Step 11 late defer, logged as a `review-late-defer` row so it never displaces the newest `review` pass, counts here and is not a pass) |
+3 -2
View File
@@ -240,7 +240,8 @@ export function recordJudgment(ledger: string, merged: MergedLine[], judged: Jud
* The blocking findings a run's ledger holds (GST-951): every collected line whose label ranks CRITICAL, raised to its judged
* severity when the newest judge accepted a judged set that is unchanged since; lead judgment's additions count and a folded
* duplicate does not. Every reader counts the same way, whatever it is and whether or not Step 9.4 classified its findings.
* Each carries its locus (a JSON finding's path:line, else the first file:line its text names) so the summary can find its row.
* Each carries its locus (a JSON finding's path:line, else the first path:line its text names, an extensionless bin/ path
* included) so the summary can find its row.
* A ledger, merged file or judged set it cannot read, or a judged set changed since judge, throws: the count is never a guess.
*/
export interface LedgerBlocker { id: string; locus: string | null }
@@ -249,7 +250,7 @@ function lineLocus(raw: string): string | null {
const o = JSON.parse(raw);
if (o && typeof o.path === 'string' && Number.isInteger(o.line)) return `${o.path.replace(/^\.\//, '')}:${o.line}`;
} catch { /* a prose finding */ }
return LOCUS.exec(raw)?.[0]?.replace(/^\.\//, '') ?? null;
return /(?:[\w.-]+\/)+[\w.-]+:\d+|[\w.-]+\.\w+:\d+/.exec(raw)?.[0]?.replace(/^\.\//, '') ?? null;
}
export function ledgerBlockers(ledger: string): LedgerBlocker[] {
const fail = (why: string): never => { throw new Error(`${ledger}: ${why}`); };
+22 -8
View File
@@ -357,6 +357,12 @@ function rowDispatches(row: Record<string, any>): Array<{ role: string; bytes: n
const DISPOSITIONS = ['fixed', 'auto-fixed', 'deferred', 'noted', 'skipped', 'kept', 'restored', 'open'];
/** A row finding that blocks: classified blocking, or an unclassified CRITICAL defect (logged before Step 9.4's classify). */
const rowBlocks = (f: Record<string, any>) => f.blocking === true || (f.blocking === undefined && f.advisory !== true && rank(f.severity) === 4);
/** The reviewer behind a row's raised_by or a ledger id: every specialist is one; the lead's additions and unknowns name none. */
function raiserOf(raiser: unknown): string | null {
const role = typeof raiser === 'string' ? raiser.split(/[/#]/)[0]!.trim() : '';
if (!role || role === 'unknown' || role === 'lead' || role === 'core') return null;
return ['qa', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', 'targeted'].includes(role) ? role : 'specialist';
}
/** A row finding's path:line, from its path and line or its fingerprint (`path:line:category`). */
function rowLocus(f: Record<string, any>): string | null {
if (typeof f.path === 'string' && Number.isInteger(f.line)) return `${f.path.replace(/^\.\//, '')}:${f.line}`;
@@ -366,18 +372,21 @@ function rowLocus(f: Record<string, any>): string | null {
/**
* The loop summary. blocking_found counts every blocker found (GST-951). Each CRITICAL the dispatch ledgers hold
* (`ledgerBlockers`, one list per ledger) is paired with at most one row finding at the same path:line that may be it (no
* severity yet, CRITICAL as logged or before a mapper lowered it, or blocking). A paired finding blocks unless its newest
* (`ledgerBlockers`, one list per ledger) is paired with at most one row finding per ledger at the same path:line, raised
* by the same reviewer (or naming none; a fix-check re-reads every reviewer's fixes), that may be it (no severity yet, CRITICAL as logged or before a mapper lowered
* it, or blocking), so one finding re-raised by a later pass's ledger pairs again. A paired finding blocks unless its newest
* classification put it below the floor (blocking: false), and its row's action is its disposition. An unpaired ledger
* CRITICAL blocks and is unrecorded. A row finding blocks by its newest classification, else as an unclassified CRITICAL.
* A ledger CRITICAL with no locus, or a second one at a paired line, counts on its own: the count may run high, never low.
* A ledger CRITICAL with no locus, or a second one at a paired line, counts on its own. A different defect the same reviewer
* raises at the exact line of a finding a row classified below the floor pairs with that finding: path:line is the only
* identity rows and ledgers share until rows carry the ledger id.
*/
export function summarizeReviewLoop(rows: readonly Record<string, any>[], since: string, ledgers: readonly (readonly LedgerBlocker[])[] = []): ReviewLoopSummary {
const sinceMs = Date.parse(since);
const inRun = (r: Record<string, any>) => r && r.via === 'ship' && Number.isFinite(rowTime(r)) && (!Number.isFinite(sinceMs) || rowTime(r) >= sinceMs);
const loopRows = rows.filter(r => inRun(r) && (r.skill === 'review' || r.skill === LATE_DEFER_SKILL || r.skill === FIX_CHECK_SKILL));
const passes = loopRows.filter(r => r.skill === 'review');
type Seen = { locus: string | null; classified: boolean; blocks: boolean; candidate: boolean; disposed: boolean };
type Seen = { locus: string | null; raiser: string | null; classified: boolean; blocks: boolean; candidate: boolean; disposed: boolean };
const seen = new Map<string, Seen>();
const deferred = new Set<string>();
const noted = new Set<string>();
@@ -392,6 +401,7 @@ export function summarizeReviewLoop(rows: readonly Record<string, any>[], since:
const classified = typeof f.blocking === 'boolean';
seen.set(id, {
locus: rowLocus(f) ?? prior?.locus ?? null,
raiser: prior?.raiser ?? raiserOf(f.raised_by),
classified: classified || !!prior?.classified,
blocks: classified ? f.blocking : prior?.classified ? prior.blocks : !!prior?.blocks || rowBlocks(f),
candidate: !!prior?.candidate || f.severity === undefined || rank(f.severity) === 4 || rank(f.raw_severity) === 4 || f.blocking === true,
@@ -404,11 +414,15 @@ export function summarizeReviewLoop(rows: readonly Record<string, any>[], since:
}
const paired = new Set<string>();
let ledgerOnly = 0;
for (const b of ledgers.flat()) {
const id = b.locus ? [...seen].find(([k, s]) => s.candidate && s.locus === b.locus && !paired.has(k))?.[0] : undefined;
if (id) paired.add(id); else ledgerOnly++;
const pairedIds = new Set<string>();
for (const [n, list] of ledgers.entries()) {
for (const b of list) {
const by = b.id.startsWith('fix-check') ? null : raiserOf(b.id);
const id = b.locus ? [...seen].find(([k, s]) => s.candidate && s.locus === b.locus && (!s.raiser || !by || s.raiser === by) && !paired.has(`${n}\0${k}`))?.[0] : undefined;
if (id) { paired.add(`${n}\0${id}`); pairedIds.add(id); } else ledgerOnly++;
}
}
const blockers = [...seen].filter(([k, s]) => s.blocks || (paired.has(k) && !s.classified));
const blockers = [...seen].filter(([k, s]) => s.blocks || (pairedIds.has(k) && !s.classified));
const unrecorded = ledgerOnly + blockers.filter(([, s]) => !s.disposed).length;
const out: ReviewLoopSummary = { passes: passes.length, blocking_found: blockers.length + ledgerOnly, deferred: deferred.size, noted: noted.size, fixed: fixed.size, scope };
if (unrecorded) out.unrecorded = unrecorded;
+3 -3
View File
@@ -14,7 +14,7 @@ gates and the report. `STOP:` (exit 3) means nothing after it ran; `NEXT:` names
the next act. Read a section only when a phase or the list at the end sends you, never
up front. A STOP or decision this file does not cover: read `ship/SKILL.md` at that step, finish it
there, come back. Unattended, take a question's recommended option (log it); gate
dispositions and approvals stay pending and end the run. Keep a private invocation record (`--record`): write its six headings at Phase 1 and fill each as its phase ends: **Checks**
dispositions and approvals stay pending and end the run. Keep a private invocation record (`--record`): write its six headings at Phase 1, fill each as its phase ends: **Checks**
(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Exploratory QA:`, `Scope Check:`), **Decisions** (any
phase: each answered or auto-taken question, verbatim), **Reviews** (Phases 3-5: the plan line,
start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7:
@@ -28,7 +28,7 @@ file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` r
small model at low effort; their findings go to the PR body.
**Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's
section read or a fix-check, plus a keep-alive per 200 s waited. Fixes redo 3-4.
section read or a fix-check, plus a keep-alive per 200 s waited.
**Budget: observe.** `gstack-ship-budget start --mode observe` records, never admits: each program ends
with `gstack-ship-budget note --step <last step> --reading R --credits C --record <rec>`, R and C read in that program
@@ -119,7 +119,7 @@ STALE or MISSING reruns once), `PREGATE:`; print them, the last `Gate edits:` an
## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22
- Step 18: the open-PR lookup; a title file starting `v<NEW_VERSION> `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H.
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <run dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; a draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
- Step 20: the metrics row via `gstack-review-log`.
- Step 22: `gstack-ship-budget close --reading R --credits C` (report `BUDGET_FINAL:`, `BUDGET_TIMELINE:` (tokens, credits), any `METER:` line), then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
+3 -3
View File
@@ -12,7 +12,7 @@ gates and the report. `STOP:` (exit 3) means nothing after it ran; `NEXT:` names
the next act. Read a section only when a phase or the list at the end sends you, never
up front. A STOP or decision this file does not cover: read `ship/SKILL.md` at that step, finish it
there, come back. Unattended, take a question's recommended option (log it); gate
dispositions and approvals stay pending and end the run. Keep a private invocation record (`--record`): write its six headings at Phase 1 and fill each as its phase ends: **Checks**
dispositions and approvals stay pending and end the run. Keep a private invocation record (`--record`): write its six headings at Phase 1, fill each as its phase ends: **Checks**
(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Exploratory QA:`, `Scope Check:`), **Decisions** (any
phase: each answered or auto-taken question, verbatim), **Reviews** (Phases 3-5: the plan line,
start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7:
@@ -26,7 +26,7 @@ file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` r
small model at low effort; their findings go to the PR body.
**Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's
section read or a fix-check, plus a keep-alive per 200 s waited. Fixes redo 3-4.
section read or a fix-check, plus a keep-alive per 200 s waited.
**Budget: observe.** `gstack-ship-budget start --mode observe` records, never admits: each program ends
with `gstack-ship-budget note --step <last step> --reading R --credits C --record <rec>`, R and C read in that program
@@ -117,7 +117,7 @@ STALE or MISSING reruns once), `PREGATE:`; print them, the last `Gate edits:` an
## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22
- Step 18: the open-PR lookup; a title file starting `v<NEW_VERSION> `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H.
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <run dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; a draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
- Step 20: the metrics row via `gstack-review-log`.
- Step 22: `gstack-ship-budget close --reading R --credits C` (report `BUDGET_FINAL:`, `BUDGET_TIMELINE:` (tokens, credits), any `METER:` line), then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
+3 -4
View File
@@ -233,10 +233,9 @@ When the owner accepted a stop after a fix-check edit, add
and put its printed `Review-loop exception:` line in the body; never a prose line under `REVIEW_LOOP:`.
The summary reads this branch's review log for the invocation's passes (its
`REVIEW_LOOP:` line is the one the Pre-Landing Review section carries). When Step 11
rendered the targeted reader, replace `--no-ledger` with `--ledger <its render --out>/review-ledger.jsonl`
(a run whose passes all ran through `gstack-review-dispatch` passes `--run <run dir>`), so
every reader's CRITICAL findings count. The `&&`
`REVIEW_LOOP:` line is the one the Pre-Landing Review section carries). Here every
finding reaches a row through Step 9.4; a run whose readers were collected by
`gstack-review-dispatch` (ship/DRIVER.md) passes `--run <run dir>` instead. The `&&`
matters: a summary that refuses (no pass matched `REVIEW_SINCE`) must not let `write`
record a stale or empty loop file.
+3 -4
View File
@@ -216,10 +216,9 @@ When the owner accepted a stop after a fix-check edit, add
and put its printed `Review-loop exception:` line in the body; never a prose line under `REVIEW_LOOP:`.
The summary reads this branch's review log for the invocation's passes (its
`REVIEW_LOOP:` line is the one the Pre-Landing Review section carries). When Step 11
rendered the targeted reader, replace `--no-ledger` with `--ledger <its render --out>/review-ledger.jsonl`
(a run whose passes all ran through `gstack-review-dispatch` passes `--run <run dir>`), so
every reader's CRITICAL findings count. The `&&`
`REVIEW_LOOP:` line is the one the Pre-Landing Review section carries). Here every
finding reaches a row through Step 9.4; a run whose readers were collected by
`gstack-review-dispatch` (ship/DRIVER.md) passes `--run <run dir>` instead. The `&&`
matters: a summary that refuses (no pass matched `REVIEW_SINCE`) must not let `write`
record a stale or empty loop file.
+48 -10
View File
@@ -56,21 +56,39 @@ const CLEAN = 'NO FINDINGS';
const OUTSIDE_CLEAN = 'No issues found in the diff.\nRecommendation: ship because no finding reaches the floor';
describe('GST-951: the receipt counts every blocker the readers found', () => {
// #3841 (reconstruction): the native adversarial pass's FIXABLE (CRITICAL) finding was fixed, but no row recorded it; the
// receipt's blocking_found=1 fixed=1 was entered by hand. Its two other findings were noted in pass 2.
test('#3841: an adversarial-only blocker counts', () => {
// #3841 (reconstruction; GSTA-203 PR 8 trail rows 9 and 14): Step 11's native adversarial FIXABLE (CRITICAL) finding was
// fixed in round 2, but no row recorded it (pass 2's row held no findings), so summary printed blocking_found=0 fixed=0 and
// the lane hand-set blocking_found=1 fixed=1. Its two other findings are the noted pair.
const ADVERSARIAL_3841 = [
'1. lib/pregate-clear.ts:140 The receipt stores the raw run URL, not the attempt it judged. FIXABLE',
'2. lib/pregate.ts:88 The refusal text does not name the attempt. INVESTIGATE',
'3. docs/pregate.md:12 The docs do not say a rerun replaces the attempt. INVESTIGATE',
'Recommendation: fix 1 because a later attempt would be judged by the first',
].join('\n');
test('#3841 as /ship logs it: the queued adversarial blocker in pass 2\'s row counts as 1 found and 1 fixed', () => {
const repo = branchWith([
review(0, 1, []),
review(40, 2, [{ fingerprint: 'lib/pregate-clear.ts:140:security', action: 'fixed', raised_by: 'adversarial-native' },
note('lib/pregate.ts:88:docs', 'adversarial-native'), note('docs/pregate.md:12:docs', 'adversarial-native')]),
]);
const run = path.join(repo.root, 'run');
pass(path.join(run, 'p1'), [['specialist/testing', CLEAN], ['red-team', CLEAN]]);
pass(path.join(run, 'p1-voices'), [['outside-voice', OUTSIDE_CLEAN], ['adversarial-native', ADVERSARIAL_3841]]);
pass(path.join(run, 'p2'), [['specialist/testing', CLEAN], ['red-team', CLEAN]]);
expect(loop(summary(repo, '--no-ledger'))).toMatchObject({ passes: 2, blocking_found: 0, noted: 2, fixed: 1 });
const s = loop(summary(repo, '--run', run));
expect(s).toMatchObject({ passes: 2, blocking_found: 1, noted: 2, fixed: 1, ledgers_read: 3, rounds: 2 });
expect(s.unrecorded).toBeUndefined();
});
test('#3841 as the lane logged it: an adversarial-only blocker counts, and its missing fix record shows as unrecorded', () => {
const repo = branchWith([
review(0, 1, []),
review(40, 2, [note('lib/pregate.ts:88:docs', 'adversarial-native'), note('docs/pregate.md:12:docs', 'adversarial-native')]),
]);
const run = path.join(repo.root, 'run');
pass(path.join(run, 'p1'), [['specialist/testing', CLEAN], ['red-team', CLEAN]]);
pass(path.join(run, 'p1-voices'), [['outside-voice', OUTSIDE_CLEAN], ['adversarial-native', [
'1. lib/pregate-clear.ts:140 The receipt stores the raw run URL, not the attempt it judged. FIXABLE',
'2. lib/pregate.ts:88 The refusal text does not name the attempt. INVESTIGATE',
'3. docs/pregate.md:12 The docs do not say a rerun replaces the attempt. INVESTIGATE',
'Recommendation: fix 1 because a later attempt would be judged by the first',
].join('\n')]]);
pass(path.join(run, 'p1-voices'), [['outside-voice', OUTSIDE_CLEAN], ['adversarial-native', ADVERSARIAL_3841]]);
expect(loop(summary(repo, '--no-ledger'))).toMatchObject({ passes: 2, blocking_found: 0, noted: 2, fixed: 0 });
const s = loop(summary(repo, '--run', run));
expect(s).toMatchObject({ passes: 2, blocking_found: 1, noted: 2, fixed: 0, unrecorded: 1, ledgers_read: 2, rounds: 2 });
@@ -148,7 +166,7 @@ describe('GST-951: the receipt counts every blocker the readers found', () => {
review(0, 1, [
row('src/auth.ts:42:testing', { path: 'src/auth.ts', line: 42, severity: 'CRITICAL', kind: 'test-gap', blocking: false, action: 'noted', raised_by: 'testing' }),
row('src/m.ts:5:state-matrix', { path: 'src/m.ts', line: 5, severity: 'INFORMATIONAL', raw_severity: 'CRITICAL', kind: 'test-gap', blocking: false, action: 'noted', raised_by: 'state-machines' }),
row('src/f.ts:3:logic', { action: 'fixed', raised_by: 'red-team' }),
row('src/f.ts:3:logic', { action: 'fixed', raised_by: 'outside-voice' }),
row('src/n.ts:8:logic', { severity: 'CRITICAL', raised_by: 'red-team' }),
row('src/g.ts:7:logic', { action: 'fixed', raised_by: 'core' }),
]),
@@ -164,6 +182,26 @@ describe('GST-951: the receipt counts every blocker the readers found', () => {
expect(s).toMatchObject({ passes: 2, blocking_found: 3, fixed: 2, unrecorded: 1, ledgers_read: 1 });
});
test('round 2 cases: another reviewer at a below-floor line, an extensionless path, one finding in two ledgers, no locus', () => {
const repo = branchWith([
review(0, 1, [
{ fingerprint: 'src/auth.ts:42:testing', path: 'src/auth.ts', line: 42, severity: 'CRITICAL', blocking: false, action: 'noted', raised_by: 'testing' },
{ fingerprint: 'bin/gstack-review-scope:262:logic', path: 'bin/gstack-review-scope', line: 262, severity: 'CRITICAL', blocking: true, action: 'fixed', raised_by: 'red-team' },
{ fingerprint: 'x.ts:5:logic', path: 'x.ts', line: 5, severity: 'CRITICAL', blocking: true, action: 'fixed', raised_by: 'red-team' },
]),
fixCheck(30, []),
]);
const run = path.join(repo.root, 'run');
pass(path.join(run, 'p1'), [
['specialist/testing', '[CRITICAL] src/auth.ts:42 the denied path has no test'],
['outside-voice', '1. src/auth.ts:42 an unauthenticated caller reaches the handler. Blocks: yes\nRecommendation: fix 1 because it is open'],
['red-team', '[CRITICAL] bin/gstack-review-scope:262 the refusal is skipped\n[CRITICAL] x.ts:5 the guard is inverted'],
]);
pass(path.join(run, 'fix-check'), [['fix-check', '[CRITICAL] x.ts:5 the guard is still inverted\n[CRITICAL] the retry loop never ends']]);
const s = loop(summary(repo, '--run', run));
expect(s).toMatchObject({ blocking_found: 4, fixed: 2, noted: 1, unrecorded: 2, ledgers_read: 2 });
});
test('judgment decides: a folded duplicate counts once, a lead addition counts, a rejected judgment keeps the collected labels', () => {
const repo = branchWith([review(0, 1, [])]);
const run = path.join(repo.root, 'run');
@@ -1,4 +1,4 @@
{
"bytes": 263,
"why": "GST-951: summary names its ledgers (--run in DRIVER, --ledger or --no-ledger in pr-body)"
"bytes": 177,
"why": "GST-951: summary names its ledgers (--run in DRIVER, --no-ledger in pr-body)"
}