diff --git a/bin/gstack-review-dispatch b/bin/gstack-review-dispatch index 33cf23f28..89a4b3dfe 100755 --- a/bin/gstack-review-dispatch +++ b/bin/gstack-review-dispatch @@ -66,7 +66,8 @@ function render(): number { if (role === 'targeted') { const base = resolveBaseBranch(process.cwd(), flag('--base')); const mb = [`origin/${base}`, base].map(ref => spawnSync('git', ['merge-base', ref, 'HEAD'], { encoding: 'utf8', timeout: 30_000 })).find(r => r.status === 0)?.stdout.trim(); - if (!diffBase || diffBase !== mb) throw new BriefError(`the targeted reader reads the whole diff on every pass: --diff-base must be the merge base with ${base} (${mb ?? 'unresolved'}), got ${diffBase ?? 'none'}`); + const full = diffBase ? spawnSync('git', ['rev-parse', '--verify', '--quiet', `${diffBase}^{commit}`], { encoding: 'utf8', timeout: 30_000 }).stdout?.trim() : undefined; + if (!full || full !== mb) throw new BriefError(`the targeted reader reads the whole diff on every pass: --diff-base must be the merge base with ${base} (${mb ?? 'unresolved'}), got ${diffBase ?? 'none'}`); } const checklists = flag('--checklists')?.split(',').map(s => s.trim()).filter(Boolean).map(c => (c.includes('/') ? c : path.join(ROOT, 'review/specialists', `${c}.md`))); for (const c of checklists ?? []) if (!fs.existsSync(c)) throw new BriefError(`checklist not found: ${c}`); diff --git a/docs/review-policy.md b/docs/review-policy.md index 7165acb73..71f5a70e7 100644 --- a/docs/review-policy.md +++ b/docs/review-policy.md @@ -230,7 +230,7 @@ and lockfiles stay runtime. To adopt it, commit an entry like this on the base b **Gate paths for the machinery itself.** An edit to `lib/review-depth.ts`, `lib/review-collect.ts`, `bin/gstack-review-plan`, `lib/review-scope.ts`, -`lib/ship-queue*.ts`, `lib/regen.ts`, `.gstack/generated.json`, +`lib/review-targeted.ts`, `review/specialists/targeted.md`, `lib/ship-queue*.ts`, `lib/regen.ts`, `.gstack/generated.json`, `ship/sections/review-army.md.tmpl`, `ship/sections/adversarial.md.tmpl` or `scripts/resolvers/review-army.ts` is gate class: changing how much review runs gets the most review. `rules=` (`DEPTH_RULES`) hashes the deciding constants and patterns, diff --git a/ship/sections/adversarial.md b/ship/sections/adversarial.md index 103e4abcf..0bc8600b3 100644 --- a/ship/sections/adversarial.md +++ b/ship/sections/adversarial.md @@ -5,7 +5,7 @@ `DISPATCH: outside-voice`. Without the native line (light depth at runtime 0; above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`; with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader -(`gstack-review-dispatch render --role targeted --classes `) like the voices; `verify --targeted-out` feeds the receipt's `--review-targeted`. Under a token cap, +(`gstack-review-dispatch render --role targeted --classes --base --diff-base `, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out` feeds the receipt's `--review-targeted`. Under a token cap, reserve the planned voices as one batch (`gstack-ship-budget check --next dispatch:`) and add `--reservation ` to the `--start adversarial-review` call below. diff --git a/ship/sections/adversarial.md.tmpl b/ship/sections/adversarial.md.tmpl index 56c143433..ea99ddfdd 100644 --- a/ship/sections/adversarial.md.tmpl +++ b/ship/sections/adversarial.md.tmpl @@ -3,7 +3,7 @@ `DISPATCH: outside-voice`. Without the native line (light depth at runtime 0; above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`; with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader -(`gstack-review-dispatch render --role targeted --classes `) like the voices; `verify --targeted-out` feeds the receipt's `--review-targeted`. Under a token cap, +(`gstack-review-dispatch render --role targeted --classes --base --diff-base `, the merge base on every pass, never RANGE_BASE) like the voices; `verify --targeted-out` feeds the receipt's `--review-targeted`. Under a token cap, reserve the planned voices as one batch (`gstack-ship-budget check --next dispatch:`) and add `--reservation ` to the `--start adversarial-review` call below. diff --git a/test/review-packets-removed.test.ts b/test/review-packets-removed.test.ts index 6619d89d6..ab7ff8240 100644 --- a/test/review-packets-removed.test.ts +++ b/test/review-packets-removed.test.ts @@ -60,7 +60,7 @@ const NO_LOSS_REPLAYS = { type Replay = keyof typeof NO_LOSS_REPLAYS; const REVIEWER_BRIEFS: Record = { 'ship/sections/review-army.md': { sha: 'b7ca291a1cc63750bfb9710a4c12fd971523b9598da2a32ef80fb258eb0eceab', replay: 'L' }, - 'ship/sections/adversarial.md': { sha: 'dd3e4a4393e18e848ec55e2705be56de773894b2be6d5ec0485479b1f07f16db', replay: 'GST-257' }, + 'ship/sections/adversarial.md': { sha: 'd776ee5bc6e0c13930f8f4de05bad2f1ed9e52bc9fc5df80ae3ba7680455b2bb', replay: 'GST-257' }, 'review/sections/review-army.md': { sha: '78329b64640260cd8831ca81a9189f3f8dcade9c406431631d3f2cd798cd3967', replay: '#3638' }, 'review/sections/adversarial.md': { sha: 'e6962fa25b78f362654f2954f79683e524d2ebd452d1494f8a2b099888e8e255', replay: 'G1' }, 'review/checklist.md': { sha: '607788afeb2c83f2dff1dccd58d212616ec72393d28cda2fa18690c1a83df699', replay: '#3638' }, diff --git a/test/review-targeted.test.ts b/test/review-targeted.test.ts index f86751c4e..44f825623 100644 --- a/test/review-targeted.test.ts +++ b/test/review-targeted.test.ts @@ -263,6 +263,7 @@ describe('the targeted role end to end on a fixture repository', () => { expect(brief).toContain(`DIFF_BASE=${base} && git diff "$DIFF_BASE"`); expect(brief).toContain('Read the targeted brief above ('); expect(runBin(repo, 'gstack-review-dispatch', ['render', '--role', 'targeted', '--classes', 'cost', '--diff-base', base, '--out', path.join(repo.root, 'bad')]).err).toContain('BRIEF_RENDER_FAILED: slot classes must list'); + expect(runBin(repo, 'gstack-review-dispatch', ['render', '--role', 'targeted', '--classes', 'money', '--base', 'main', '--diff-base', base.slice(0, 12), '--out', path.join(repo.root, 'short')]).status).toBe(0); expect(runBin(repo, 'gstack-review-dispatch', ['render', '--role', 'targeted', '--classes', 'money', '--diff-base', head, '--out', path.join(repo.root, 'range')]).err).toContain(`the targeted reader reads the whole diff on every pass: --diff-base must be the merge base with main (${base})`); expect(runBin(repo, 'gstack-review-dispatch', ['sent', '--ledger', ledger, '--role', 'targeted', '--sent', file]).out).toContain('BRIEF_SENT: reader=targeted'); const output = path.join(repo.root, 'targeted.out'); diff --git a/test/size-ledger/ship/gst-257-targeted-review.json b/test/size-ledger/ship/gst-257-targeted-review.json index 03ceae988..d6dec8e4c 100644 --- a/test/size-ledger/ship/gst-257-targeted-review.json +++ b/test/size-ledger/ship/gst-257-targeted-review.json @@ -1,4 +1,4 @@ { - "bytes": 215, - "why": "GST-257: Step 11 dispatches the targeted reader on money, gate, auth and secret diffs" + "bytes": 302, + "why": "GST-257: Step 11 dispatches the targeted reader on money, gate, auth and secret diffs, on the merge base every pass" }