fix: a granted fix-check after a fix-check plans in the loop (GST-1681) (#4096)

* fix: a granted fix-check after a fix-check plans in the loop (GST-1681)

In the loop, a new review_loop_grant naming the last fix-check's commit
(--range-base, the stop it answers) plans one more scoped fix-check, the
next link of the fix-check chain finish counts (GEVA-363, gstack-evals#254).

Replay: GST-1681 (repins lib/review-depth.ts)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

In the loop, a grant recorded before a later logged fix-check already bought a link of the fix-check chain, so gstack-review-plan refuses it (exit 3): a post-review link's grant names the commit its fix-check read, and a stale --range-base names an earlier link. A CLI regression covers both, and a grant recorded after the newest fix-check still plans.

Review-Finding: p1/red-team#1 reader=red-team
Review-Finding: p1/specialist-batched#1 reader=testing
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-06 21:58:23 +00:00
1 parent 0c41ced4fd
commit e325b6d160
10 files changed
+155 -6

No files matched your search

+9 -2
View File
@@ -81,7 +81,9 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
before HEAD; needs --range-base): the stop after a round-2 full pass with a fixed blocking finding or a
gate-class edit plans that fix-check instead; a fix-check never earns another, except that with
--post-review --last fix-check a new grant at a stop head after --range-base (the last fix-check's
commit) plans the next link of the fix-check chain; any other row is refused (exit 3)
commit) plans the next link of the fix-check chain, and in the loop after a fix-check a new grant
naming that fix-check's commit (--range-base) plans the next link; a grant recorded before a later
logged fix-check already bought a link; any other row is refused (exit 3)
--fix-checks <n> fix-checks already run in this invocation: the owner's grant adds one only after the first, in the lean
loop and after a round-2 fix-check in the full loop (GEVA-223)
--post-review plan the Step 9 that /ship Step 16 stage 2.1 inserted for edits after the review bound (finish's
@@ -150,6 +152,7 @@ function main(): number {
const root = git(['rev-parse', '--show-toplevel'])?.trim();
if (!root) { process.stderr.write('gstack-review-plan: run it inside a git checkout\n'); return EXIT.fail; }
process.chdir(root);
let inLoopLink = false;
if (grant) {
const loc = locate({ cwd: root, env: process.env });
const row = ('file' in loc ? readRecord(loc).rows : []).find(r => r.id === grant && r.outcome === 'approved' && `${r.ask} ${r.scope}`.includes('review_loop_grant'));
@@ -157,7 +160,11 @@ function main(): number {
const ancestor = (a: string, d: string) => git(['merge-base', '--is-ancestor', a, d]) !== null;
const chained = has('--post-review') && last === 'fix-check';
const stopHead = row && passHead ? `${row.ask} ${row.scope}`.match(/\b[0-9a-f]{7,40}\b/g)?.map(s => git(['rev-parse', '--verify', '--quiet', `${s}^{commit}`])?.trim()).find(s => s && ancestor(passHead, s) && ancestor(s, 'HEAD') && !(chained && s === passHead)) : undefined;
inLoopLink = !chained && last === 'fix-check' && stopHead === passHead;
if (!stopHead) { process.stderr.write(`gstack-review-plan: --grant ${grant}: no approved row ${grant} naming review_loop_grant and this invocation's stop head (a commit ${chained ? `after --range-base ${rangeBase}, the last fix-check's commit, whose own grant does not carry over` : `at or after --range-base ${rangeBase}`}, at or before HEAD) in this branch's approval record (${'file' in loc ? 'gstack-approvals list' : loc.message}); record the owner's grant with gstack-approvals add --ask '... (review_loop_grant <stop head sha>)' first\n`); return EXIT.refused; }
const logged = inLoopLink ? runScriptSync(path.join(path.dirname(Bun.fileURLToPath(import.meta.url)), 'gstack-review-read'), [], { encoding: 'utf8', timeout: 60_000, maxBuffer: 64 * 1024 * 1024 }) : null;
if (logged && logged.status !== 0) { process.stderr.write(`gstack-review-plan: --grant ${grant}: gstack-review-read exited ${logged.status}, so whether a fix-check already spent this grant is unknown; no plan is safer than a fix-check the grant may not buy\n`); return EXIT.fail; }
if (logged && row && (logged.stdout ?? '').split('\n---CONFIG---')[0]!.split('\n').some(l => { try { const v = JSON.parse(l); return v?.skill === 'review-fix-check' && Date.parse(v.timestamp) > Date.parse(row.recorded_at); } catch { return false; } })) { process.stderr.write(`gstack-review-plan: --grant ${grant}: a fix-check was logged after this grant was recorded, so the grant already bought a link of the fix-check chain; the stop at --range-base ${rangeBase} needs a new grant recorded after the last fix-check (gstack-approvals add --ask '... (review_loop_grant ${passHead!.slice(0, 12)})')\n`); return EXIT.refused; }
}
const collected = collectLive(root, base);
if (collected.errors.length) {
@@ -236,7 +243,7 @@ function main(): number {
const plan = planReview({
...collected, ...exclusionInput, ...riskPaths, declared: declared as RiskClass[], shape: { panel: review.panel, readerModel: review.reader_model, platform: review.platform, head, platformReview: platform?.ok ? platform.review : null }, scope, gateTagged, targeted: { classes: targeted.classes, ran }, numbers: scanNumbers(kept(added)), force, gated: list('--gated'), floor, tiers: auditEffort !== 'host',
criticalFound: has('--critical-found'), adversarialRan: has('--adversarial-ran'), reviewedNumbers: flag('--reviewed-numbers'),
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: (has('--fixed-below') || (postReview && pass > 0)) && last !== 'fix-check', blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate || (postReview && pass > 0), pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines, granted: grant, fixChecks: has('--fix-checks') ? fixChecks : undefined, postReview },
history: { fullPasses: pass, last, fixedBlocking: has('--fixed-blocking'), fixedBelow: (has('--fixed-below') || (postReview && pass > 0)) && last !== 'fix-check', blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate || (postReview && pass > 0), pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines, granted: grant, fixChecks: has('--fix-checks') ? fixChecks : undefined, postReview, chained: inLoopLink },
});
if (postReview && pass > 0) plan.next.reason = `edits after the review bound (Step 16 stage 2.1) plan as a fix to a gate-class path, never a note: ${plan.next.reason}`;
const ranArg = flag('--targeted-ran');
@@ -0,0 +1 @@
- **A Captain-granted fix round after a fix-check is planned, not hand-run.** After round 2's fix-check and one granted fix-check, an owner who granted one more round at the last fix-check's stop head got `NEXT: stop` from `gstack-review-plan`, because in the loop a fix-check never earned another. The lane ran the fix-check by hand on a fix-range token, and finish printed `FIX_CHECK: not counted (fix-range pass; only a later full pass can clear review)` (gstack-evals#254). A new recorded grant naming the last fix-check's commit (`--range-base`) now plans one more scoped fix-check, the next link of the fix-check chain finish already counts; the previous link's grant (one recorded before a later logged fix-check) is refused and a grant at any other commit still stops, so one grant buys one link. (GST-1681)
+5 -1
View File
@@ -118,7 +118,10 @@ incomplete coverage, which blocks until the reviewer is rerun.
`gstack-review-plan --post-review --last fix-check --range-base <that commit>
--grant <id>`, plans one more fix-check, the next link of the
[fix-check chain](ship-policy.md#fix-check-chain) (GST-1676). The last link's
own grant is refused there, so each link still needs the owner's word.
own grant is refused there, so each link still needs the owner's word. In the
loop, a new grant naming the last fix-check's commit (`--range-base`) plans
the next link the same way; a grant recorded before a later logged fix-check
already bought a link and is refused (GST-1681).
<a id="transitions"></a>
### Transitions
@@ -144,6 +147,7 @@ incomplete coverage, which blocks until the reviewer is rerun.
| a round-2 full pass that stops for a fixed blocking finding or a gate-class edit, with the owner's recorded grant (`--grant <approval id>`) | one scoped fix-check (round 3), after Step 11 on gate class |
| a round-2 fix-check that stops the same way, with the owner's recorded grant and `--fix-checks 1` | one scoped fix-check (round 3), after Step 11 on gate class |
| `--post-review` after a fix-check (edits after the review bound), with a new recorded grant at a stop head after the last fix-check's commit | one scoped fix-check, the next link of the fix-check chain, after Step 11 on gate class |
| a fix-check that stops in the loop, with a new recorded grant naming its own commit (`--range-base`, the stop it answers), after any number of fix-checks | one scoped fix-check, the next link of the fix-check chain, after Step 11 on gate class |
A **fix-check** is one reviewer reading the touched hunks, the fix list, the
test results and the callers of every changed symbol, at full effort. It is
+7 -1
View File
@@ -1825,7 +1825,13 @@ the lean panel). At the cap it stops for the owner, whose recorded grant
fix-check, pass `--range-base` at that fix-check's commit: a new grant at a
stop head after it plans the next link of the chain below (GST-1676), and the
last link's own grant is refused. So `finish` never asks for a step the
planner refuses.
planner refuses. In the loop, a fix-check that stops is answered the same way:
a new grant naming that fix-check's commit (`--range-base`, the stop it
answers) plans one more fix-check, the next link of the chain, however many
fix-checks ran before (GST-1681, GEVA-363's third round). The previous link's
grant is refused: it names an earlier stop head, or it was recorded before a
later logged fix-check (a post-review link's grant names the commit its
fix-check read). A grant at any other commit plans nothing: one grant, one link.
<a id="fix-check-chain"></a>**Chained fix-checks (GST-1627).** A second granted
fix-check after the first one (a merge resolution, say) logs its row with
+5 -1
View File
@@ -386,6 +386,8 @@ export interface History {
fixChecks?: number;
/** The Step 9 /ship Step 16 stage 2.1 inserted for edits after the review bound (gstack-review-plan --post-review). */
postReview?: boolean;
/** In the loop, the CLI bound `granted` to the last fix-check's own stop head, its commit (--range-base): a new grant, never the previous link's. */
chained?: boolean;
}
export interface NextStep { step: Step; reason: string }
@@ -401,7 +403,8 @@ export interface NextStep { step: Step; reason: string }
* and the owner's recorded grant (`granted`) turns the stop after a round-2 full pass with a fixed finding, or after a round-2
* fix-check when `fixChecks` says it was the only one (GEVA-223), into its one fix-check; a fix-check never earns another, so
* the grant cannot repeat. Edits after the review bound (`postReview`) that follow a fix-check are a new stop, and a new grant
* there adds one fix-check, the next link of the chain finish counts (GST-1676, docs/ship-policy.md#fix-check-chain).
* there adds one fix-check, the next link of the chain finish counts (GST-1676, docs/ship-policy.md#fix-check-chain). In the
* loop, a grant the CLI bound to the last fix-check's own stop head (`chained`) adds one more the same way (GST-1681).
*/
export function nextStep(h: History, depth?: Depth): NextStep {
const n = h.lean ? leanStep(h, depth) : loopStep(h, depth);
@@ -409,6 +412,7 @@ export function nextStep(h: History, depth?: Depth): NextStep {
if (h.gate && !h.adversarialRan) return { step: 'voices', reason: `gate class: Step 11's ${h.lean ? 'one reader' : 'voices (adversarial, outside, targeted)'} run${h.lean ? 's' : ''} before the owner's exit, then ask again with --adversarial-ran (${n.reason})` };
const afterFixCheck = h.last === 'fix-check' && h.fullPasses === FULL_PASS_CAP - 1 && h.fixChecks === 1;
if (h.postReview && h.last === 'fix-check' && h.granted && !h.blockingOpen) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check for the edits after the last fix-check, a new link of the fix-check chain (docs/ship-policy.md#fix-check-chain); a later post-review edit needs a grant of its own` };
if (h.chained && h.last === 'fix-check' && h.granted && !h.blockingOpen) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check at the last fix-check's stop, a new link of the fix-check chain (docs/ship-policy.md#fix-check-chain); another stop needs a grant of its own` };
if (!h.lean && h.granted && !h.blockingOpen && (afterFixCheck || (h.last === 'full' && h.fullPasses === FULL_PASS_CAP))) return { step: 'fix-check', reason: `the owner's grant ${h.granted} adds one scoped fix-check after round ${FULL_PASS_CAP} (review_loop_exception), ${afterFixCheck ? 'never a second' : 'never a fourth round'}` };
return n;
}
+1 -1
View File
@@ -1 +1 @@
{"sha":"34685744162ba50d743abbf7c6e2e1ee57bf5ae0e6fd82347bca28f50a155b3f","replay":"GST-1676"}
{"sha":"eac38277f567d80e6ec5613e050c05c2b2b837b405f14df25329814b9f3c34f9","replay":"GST-1681"}
+1
View File
@@ -0,0 +1 @@
{"replay":"GST-1681","justification":"GST-1681 (granted fix-check after a fix-check, in the loop): lib/review-depth.ts is main's GST-1676 bytes plus one History field, chained, and one transition in nextStep: with chained set, last fix-check, a recorded grant and no open blocker, the stop becomes one fix-check (the next link of finish's fix-check chain, docs/ship-policy.md#fix-check-chain). Only bin/gstack-review-plan sets chained, outside --post-review, when the grant names exactly --range-base (the last fix-check's commit, the stop it answers). Deterministic no-loss replay, no paid run: the GST-967 grid (MAIN_PLANS) and the GST-972 grid (MAIN_972_PLANS) in test/review-depth.test.ts hash unchanged and the GST-967 transition-table invariant passes, since none of them sets chained; so no plan main builds drops, renames or re-efforts a reader, no stop main prints without a grant changes, and no brief body moves (MAIN_SECTIONS unchanged)."}
+14
View File
@@ -125,6 +125,20 @@ describe('GST-967: the round cap limits fix rounds, not the gate-class voices; a
for (const o of [{ granted: undefined }, { blockingOpen: true }, { postReview: false }, { postReview: undefined }]) expect([o, nextStep({ ...h, ...o }).step]).toEqual([o, 'stop']);
expect(nextStep({ ...h, last: 'full', fullPasses: 1, fixedBelow: true }).step).toBe('full');
});
// Value: protects=in the loop, a grant the CLI bound to the last fix-check's own stop head (`chained`) plans one more fix-check
// after any number of fix-checks (GEVA-363's third round), after Step 11 on gate class; without that binding a fix-check still
// never earns another; fails_when=the planner prints stop for a new grant (GST-1681) or plans one for an unbound grant;
// seam=nextStep; no_claim=the CLI's binding (test/review-plan-cli.test.ts)
test('a new grant bound to the last fix-check\'s stop plans the next chained fix-check in the loop (GST-1681)', () => {
const h = { fullPasses: 1, last: 'fix-check', fixedBlocking: true, granted: 'a-5214be46db1e', fixChecks: 2, chained: true } as const;
expect(nextStep(h)).toMatchObject({ step: 'fix-check', reason: expect.stringContaining('a-5214be46db1e') });
expect(nextStep({ ...h, lean: true }).step).toBe('fix-check');
expect(nextStep({ ...h, fullPasses: FULL_PASS_CAP }).step).toBe('fix-check');
expect(nextStep({ ...h, gate: true }).step).toBe('voices');
expect(nextStep({ ...h, gate: true, adversarialRan: true }).step).toBe('fix-check');
for (const o of [{ chained: false }, { chained: undefined }, { granted: undefined }, { blockingOpen: true }]) expect([o, nextStep({ ...h, ...o }).step]).toEqual([o, 'stop']);
expect(nextStep({ ...h, fixedBlocking: false }).step).toBe('done');
});
test('docs/review-policy.md\'s transitions table and rule 3 say both', () => {
const doc = fs.readFileSync(path.join(ROOT, 'docs', 'review-policy.md'), 'utf8');
const table = doc.slice(doc.indexOf('<a id="transitions"></a>'), doc.indexOf('A **fix-check** is one reviewer'));
+80
View File
@@ -156,6 +156,86 @@ describe('gstack-review-plan', () => {
expect([reused.status, reused.stdout]).toEqual([3, '']);
expect(reused.stderr).toContain(`--grant ${g1}: no approved row ${g1} naming review_loop_grant and this invocation's stop head (a commit after --range-base ${link1}, the last fix-check's commit`);
}, 30_000);
// Value: protects=a Captain's grant for a third fix round past a fix-check is planned, not hand-rendered: GEVA-363 (gstack-evals#254)
// ran pass 1 full, fc1 (round 2), fc2 under a grant at fc1's stop, then the owner granted a third round at fc2's stop head (its
// commit, the new --range-base); the in-loop Step 9 plans that one fix-check; the previous link's grant, a grant at another
// commit, no grant or an open blocker do not; GBRA-336's --post-review --last fix-check --fix-checks 2 --grant invocation plans
// with a grant after --range-base (GST-1676); fails_when=the planner prints stop despite the new grant (GEVA-363: the lane
// hand-ran fc3 on a fix-range token finish could not count) or lets fc2's grant buy fc3; seam=bin/gstack-review-plan, nextStep;
// no_claim=finish's chain count (test/ship-finish-ruled-notes.test.ts)
test('GST-1681: in the loop, a new grant at the last fix-check\'s stop head plans one more fix-check, never for the previous grant', () => {
const r = repo({ 'src/a.ts': 'export const a = 1;\n' });
const env: NodeJS.ProcessEnv = { ...process.env, GSTACK_STATE_ROOT: r.state, GSTACK_APPROVALS_MIRROR: '0' };
delete env.GSTACK_APPROVALS_DIR;
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim();
const commit = (n: number) => { fs.appendFileSync(path.join(r.dir, 'src/a.ts'), `export const v${n} = ${n};\n`); git('commit', '-qam', `c${n}`); return git('rev-parse', 'HEAD'); };
const add = (stop: string, n: string) => {
const a = spawnSync(path.join(path.dirname(BIN), 'gstack-approvals'), ['add', '--ask', `Owner grant: scoped fix-check ${n} (review_loop_grant ${stop.slice(0, 12)})`, '--words', 'do it', '--by', 'GStack Minder', '--scope', 'this /ship invocation', '--json'], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
expect(a.status, a.stderr).toBe(0);
return JSON.parse(a.stdout).added[0] as string;
};
const fc1 = commit(1);
const g1 = add(fc1, 'fc2');
const fc2 = commit(2);
const g2 = add(fc2, 'fc3');
const later = commit(3);
const g3 = add(later, 'at the fix');
const plan = (a: string[]) => spawnSync(BIN, ['--base', 'main', '--gate-tagged', '0', '--pass', '1', '--last', 'fix-check', '--fix-checks', '2', '--range-base', fc2, ...a], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
const third = plan(['--fixed-blocking', '--grant', g2]);
expect(third.status, third.stderr).toBe(0);
expect(third.stdout).toContain(`NEXT: fix-check (the owner's grant ${g2} adds one scoped fix-check at the last fix-check's stop, a new link of the fix-check chain`);
expect(third.stdout).toContain('DISPATCH: fix-check fix-check effort=full');
expect(plan(['--fixed-blocking']).stdout).toContain('NEXT: stop');
expect(plan(['--fixed-blocking', '--grant', g2, '--blocking-open']).stdout).toContain('NEXT: stop');
expect(plan(['--fixed-blocking', '--grant', g3]).stdout).toContain('NEXT: stop');
const reused = plan(['--fixed-blocking', '--grant', g1]);
expect([reused.status, reused.stdout]).toEqual([3, '']);
expect(reused.stderr).toContain(`--grant ${g1}: no approved row ${g1} naming review_loop_grant and this invocation's stop head (a commit at or after --range-base ${fc2}`);
const gbra336 = plan(['--post-review', '--grant', g3]);
expect(gbra336.status, gbra336.stderr).toBe(0);
expect(gbra336.stdout).toContain(`NEXT: fix-check (edits after the review bound (Step 16 stage 2.1) plan as a fix to a gate-class path, never a note: the owner's grant ${g3} adds one scoped fix-check for the edits after the last fix-check`);
expect(plan(['--post-review', '--grant', g2]).status).toBe(3);
}, 30_000);
// Value: protects=one grant buys one link in the loop too: a grant recorded before a later logged fix-check already bought that
// fix-check (a post-review link's grant names the commit its fix-check read; a stale --range-base names an earlier link), so the
// in-loop replan refuses it, while a grant recorded after the newest fix-check plans; fails_when=a spent grant plans another
// paid fix-check that finish cannot count (the GST-1681 /ship review: red-team#1, specialist#1); seam=bin/gstack-review-plan;
// no_claim=finish's chain count (test/ship-finish-ruled-notes.test.ts)
test('GST-1681: in the loop, a grant recorded before a later logged fix-check is spent and refused', () => {
const r = repo({ 'src/a.ts': 'export const a = 1;\n' });
const env: NodeJS.ProcessEnv = { ...process.env, GSTACK_STATE_ROOT: r.state, GSTACK_APPROVALS_MIRROR: '0' };
delete env.GSTACK_APPROVALS_DIR;
const git = (...a: string[]) => spawnSync('git', a, { cwd: r.dir, encoding: 'utf8', timeout: 10_000 }).stdout.trim();
const commit = (n: number) => { fs.appendFileSync(path.join(r.dir, 'src/a.ts'), `export const v${n} = ${n};\n`); git('commit', '-qam', `c${n}`); return git('rev-parse', 'HEAD'); };
const add = (stop: string, n: string) => {
const a = spawnSync(path.join(path.dirname(BIN), 'gstack-approvals'), ['add', '--ask', `Owner grant: scoped fix-check ${n} (review_loop_grant ${stop.slice(0, 12)})`, '--words', 'do it', '--by', 'GStack Minder', '--scope', 'this /ship invocation', '--json'], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
expect(a.status, a.stderr).toBe(0);
return JSON.parse(a.stdout).added[0] as string;
};
const logFixCheck = () => {
const l = spawnSync(path.join(path.dirname(BIN), 'gstack-review-log'), [JSON.stringify({ skill: 'review-fix-check', timestamp: new Date().toISOString(), status: 'clean', via: 'ship', completed: true, converged: true })], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
expect(l.status, l.stderr).toBe(0);
};
const fc1 = commit(1);
logFixCheck();
const g1 = add(fc1, 'fc2');
commit(2);
logFixCheck();
const c = commit(3);
const gc = add(c, 'post-review link at c');
logFixCheck();
commit(4);
const plan = (rangeBase: string, grant: string) => spawnSync(BIN, ['--base', 'main', '--gate-tagged', '0', '--pass', '1', '--last', 'fix-check', '--fix-checks', '3', '--fixed-blocking', '--range-base', rangeBase, '--grant', grant], { cwd: r.dir, encoding: 'utf8', timeout: 60_000, env });
for (const [rangeBase, grant] of [[c, gc], [fc1, g1]] as const) {
const spent = plan(rangeBase, grant);
expect([spent.status, spent.stdout]).toEqual([3, '']);
expect(spent.stderr).toContain(`--grant ${grant}: a fix-check was logged after this grant was recorded, so the grant already bought a link of the fix-check chain`);
}
const fresh = add(c, 'fc5');
const next = plan(c, fresh);
expect(next.status, next.stderr).toBe(0);
expect(next.stdout).toContain(`NEXT: fix-check (the owner's grant ${fresh} adds one scoped fix-check at the last fix-check's stop`);
}, 30_000);
test('--fix-check-prompt renders the fix-check reviewer prompt from the parent\'s files and refuses a missing one', () => {
const r = repo({ 'docs/howto.md': '# x\n' });
const w = (n: string, t: string) => { const f = path.join(r.state, n); fs.writeFileSync(f, t); return f; };
+32
View File
@@ -350,6 +350,38 @@ describe('finish counts a post-review fix-check chain whose grants name each lin
});
});
/**
* GST-1681 (GEVA-363, gstack-evals#254): pass 1, then fc1 (round 2) at the stop the voices read, then the owner's grant at that
* stop head buys fc2, and a second grant at fc2's commit (its stop head) buys a third round, fc3. Each in-loop grant names the
* stop it answers, the link's own range_base.
*/
describe('finish counts a Captain-granted third fix round as a chained fix-check (GST-1681)', () => {
// Value: protects=the GEVA-363 shape counts once fc3 is a planned fix-check: fc2 and fc3 each carry their own grant, the chain stops
// at fc1's tree (the reviewed tree), and the ungranted in-cap fc1 row is not a link; fails_when=finish refuses fc3 (GEVA-363
// printed FIX_CHECK: not counted) or asks fc1 for a grant; seam=finish's chain walk and per-link grant assignment
test('GEVA-363: fc1 in the cap, then fc2 and fc3 under their own grants, and finish counts fc2..fc3', () => {
const f = shipFixture({ 'src/x.ts': 'export const x = 1;\n' }, { 'src/x.ts': 'export const x = 2;\n', 'test/x.test.ts': TEST_V1 });
ships.push(f);
const p1 = headOf(f);
f.write({ 'src/x.ts': 'export const x = 3;\n' }); f.commit('round 1 fixes');
const fc1 = headOf(f);
logLink(f, p1);
const reviewed = f.run(path.join(BIN, 'gstack-wtree'), []).stdout.trim();
const reviewedBase = f.git('merge-base', 'origin/main', 'HEAD').stdout.trim();
const g2 = grantLink(f, fc1, 'fc2');
f.write({ 'src/x.ts': 'export const x = 4;\n' }); f.commit('fix the voices\' CRITICAL');
const fc2 = headOf(f);
logLink(f, fc1);
const g3 = grantLink(f, fc2, 'fc3');
f.write({ 'test/x.test.ts': TEST_V2 }); f.commit('fix fc2\'s CRITICAL');
const fc3 = headOf(f);
logLink(f, fc2);
const r = finishAfterAudit(f, reviewed, reviewedBase);
expect(lineOf(r.stdout, 'FIX_CHECK'), r.stdout).toBe(`FIX_CHECK: reviewed src/x.ts,test/x.test.ts (fix-check chain of 2 from ${fc1.slice(0, 12)}: at ${fc2.slice(0, 12)} grant ${g2}; at ${fc3.slice(0, 12)} grant ${g3})`);
expect([r.status, lineOf(r.stdout, 'ROUTE')], r.stdout).toEqual([0, expect.stringMatching(/ behavior=0 .* fix_checked=2$/)]);
});
});
describe('ruledNotes reads only approved owner rulings (GST-1537)', () => {
const HEAD = '0123456789abcdef0123456789abcdef01234567';
const row = (finding: string, o: Partial<{ ask: string; scope: string; by: string; outcome: string }> = {}) => {