mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 23:37:53 +01:00
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:
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`.
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
Reference in new issue
Block a user