mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
Merge origin/main (v1.91.358.0, #3671) into gsta-167-calls-2; stage the targeted reader with the voices
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
commit
35b1221de6
29 files changed
+832
-59
No files matched your search
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.358.0] - 2026-10-31
|
||||
|
||||
**Diffs that touch money, gate, auth or secret code now get one more reviewer, a targeted reader with a short brief for that kind of code, and the /ship receipt records whether it ran (GST-257, part 1 of 2).** The merge queue reports the result and refuses nothing yet; enforcement comes in a later release.
|
||||
|
||||
### Added
|
||||
- **`lib/review-targeted.ts` names the classes a diff touches.** It reads added and deleted lines, so removing a spend cap, a check or a redaction counts. Money is a cost, spend, budget, ledger, billing, price, fee or invoice path, an identifier ending in `_usd` or `Usd`, any `spend_cap` spelling, or a call to a paid model provider. Gate is a hunk the gate scan tagged. Auth is `SCOPE_AUTH` on an auth, session, jwt, permission or role path. Secret is a secrets, credentials, keychain or token path, or a line that reads a key-named environment variable or a credential store. Matching is token-bounded, paths review depth counts as docs or tests never count (agent prose such as skill sections and docs/review-policy.md counts as code there, so it counts here too), and an input the classifier cannot read gives all four classes.
|
||||
- **`gstack-review-plan` prints `TARGETED: classes=… rules=… brief=… targeted_patch_id=…`** and, when a class hit, `DISPATCH: targeted targeted effort=full` at every depth, light included. `--targeted-ran <targeted_patch_id>` skips the reader only when the whole committed diff, tests included, is the one it already read.
|
||||
- **`review/specialists/targeted.md` is the reader's brief:** one block per class, the blocking floor in one sentence, and the checklist's Forced-CRITICAL rule on uncapped spend in the money block. The reader is read-only, and `NO FINDINGS` is a result.
|
||||
- **`gstack-review-dispatch render --role targeted --classes …`** renders the brief with only the blocks that hit; `verify --targeted-out <file>` writes the record (done only when the reader's output was collected and the committed diff it was rendered on is unchanged) `gstack-ship-receipt write --review-targeted @<file>` stores as `review_targeted`, bound to the receipt's `product_patch_id`. A carry keeps it while the product patch is unchanged.
|
||||
- **`gstack-ship-queue ready` and `train build` print `REVIEW_TARGETED: required=… receipt=… verdict=ok|missing`.** Nothing is refused on it in this release.
|
||||
|
||||
### Precision
|
||||
- Over the last 100 non-release commits on main (8663c3d2..8be7f213), money hit 21, gate 18, auth 3 and secret 8. The gate class first used the gate paths, which hit 43 of 100, so it was narrowed to the gate scan's tagged hunks; those paths already get full review depth.
|
||||
|
||||
### Tests
|
||||
- `test/review-targeted.test.ts` covers each class with deleted lines, token boundaries, the docs and tests exclusions, failing closed, the dispatch at light, standard and full depth, `--targeted-ran`, the path from `render` through the receipt, the review-loop summary and the queue line. Its dispatch-superset check runs `gstack-review-plan` on 18 fixture diffs and requires every reviewer main dispatched at 8be7f21, unchanged, plus only the targeted reader. `test/review-packets-removed.test.ts` repins `lib/review-depth.ts` and `ship/sections/adversarial.md` and pins the new brief to that check.
|
||||
|
||||
## [1.91.357.0] - 2026-10-31
|
||||
|
||||
**pregate `literals` no longer flags a version-history table that the branch extended.** A test that keeps one row per past version of a constant (`[45, "<digest>"]`, then `[46, "<digest>"]`) used to fail `literals` on every bump, because the old row still holds the old value. Truther turned the check down to a warning over it, which also hid real stale tests.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.357.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.358.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* gstack-review-dispatch sent --ledger <f> --role <role> [--name <n>] --sent <file> the prompt actually passed; mismatch fails
|
||||
* gstack-review-dispatch collect --ledger <f> --role <role> [--name <n>] --output <file> --merged <file>
|
||||
* gstack-review-dispatch judge --ledger <f> --merged <file> --judged <file> no drop, no downgrade
|
||||
* gstack-review-dispatch verify --ledger <f> replay every hash and the judgment
|
||||
* gstack-review-dispatch verify --ledger <f> [--targeted-out <file>] replay every hash and the judgment
|
||||
* gstack-review-dispatch pass --base <b> --out <dir> --pass <id> [-- plan flags] the pass's staged manifest
|
||||
* gstack-review-dispatch show|stage|chunk|mark --manifest <f> ... one manifest step
|
||||
* gstack-review-dispatch program [--ledger <f>] the checked-in Capy pass program
|
||||
@@ -20,10 +20,12 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
|
||||
import { BRIEF_ROLES, BriefError, briefFile, promptHash, renderBrief, sectionTexts, type BriefRole, type Slots } from '../lib/review-brief';
|
||||
import { collect, readJsonl, readerKey, recordJudgment, recordSent, appendEvent, verify, type JudgedLine, type MergedLine } from '../lib/review-ledger';
|
||||
import { collect, readJsonl, readLedger, readerKey, recordJudgment, recordSent, appendEvent, verify, type JudgedLine, type MergedLine, type VerifyRow } from '../lib/review-ledger';
|
||||
import { buildManifest, chunkCount, chunkOf, manifestPath, markReader, readManifest, renderStage, writeManifest, STAGES, type Extra, type Manifest, type PassReader, type ReaderState, type Stage } from '../lib/review-pass';
|
||||
import type { Dispatch } from '../lib/review-depth';
|
||||
import { runScriptSync } from '../lib/script-invocation';
|
||||
import { TARGETED_RULES, parseClassList, targetedBriefSha, targetedPatchId } from '../lib/review-targeted';
|
||||
import { resolveBaseBranch } from '../lib/ship-policy';
|
||||
|
||||
const HELP = `gstack-review-dispatch — verbatim reviewer briefs and loss-free collection for /ship
|
||||
|
||||
@@ -33,6 +35,8 @@ const HELP = `gstack-review-dispatch — verbatim reviewer briefs and loss-free
|
||||
--learnings-file <f> --checklist <path> (default <root>/review/specialists/<name>.md; red-team.md for red-team)
|
||||
--checklists <a,b> (specialist-batched: the plan's checklists= names, or paths) --findings-file <f> --specialists <n> (red-team)
|
||||
--fix-list <f> --hunks <f> --tests <f> [--callers <f>] (fix-check)
|
||||
--classes <money,gate,auth,secret> (targeted: the plan's TARGETED classes; the brief is review/specialists/targeted.md;
|
||||
--diff-base must be the merge base with --base, default origin/HEAD's branch, on every pass)
|
||||
writes <out>/<role>[-<name>].prompt.md, records it in the ledger (default <out>/review-ledger.jsonl) and prints
|
||||
BRIEF: reader=<role/name> sha256=<h> bytes=<n> file=<path>. Pass that file's content as the reader's prompt, unchanged.
|
||||
sent --ledger <f> --role <role> [--name <n>] --sent <file>
|
||||
@@ -43,9 +47,10 @@ const HELP = `gstack-review-dispatch — verbatim reviewer briefs and loss-free
|
||||
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
|
||||
JUDGMENT_REJECTED: dropped=… downgraded=… (exit 1)
|
||||
verify --ledger <f>
|
||||
verify --ledger <f> [--targeted-out <file>]
|
||||
BRIEF, OUTPUT and JUDGE rows from the ledger alone, then VERIFY: clean (exit 0), VERIFY: dirty (exit 1) or
|
||||
VERIFY: no brief records (exit 3: this run's ledger kept no prompts)
|
||||
VERIFY: no brief records (exit 3: this run's ledger kept no prompts). A ledger with a targeted brief also prints
|
||||
TARGETED_RECORD: and --targeted-out writes it as the JSON gstack-ship-receipt write --review-targeted reads
|
||||
pass --base <ref> --out <dir> --pass <id> [--ledger <f>] [--diff-base <sha>] [--learnings-file <f>] [--extras <json>]
|
||||
[--checklist-root <dir>] [-- <gstack-review-plan flags>]
|
||||
runs gstack-review-plan and writes <out>/pass-manifest.json: stage first (audit readers marked first in --extras),
|
||||
@@ -80,8 +85,15 @@ function render(): number {
|
||||
if (!role || !BRIEF_ROLES.includes(role) || !out) { process.stderr.write(`gstack-review-dispatch render: needs --role <${BRIEF_ROLES.join('|')}> and --out <dir>\n`); return EXIT.usage; }
|
||||
const name = flag('--name');
|
||||
const diffBase = flag('--diff-base');
|
||||
const checklist = flag('--checklist') ?? (role === 'red-team' ? path.join(ROOT, 'review/specialists/red-team.md') : role === 'specialist' && name ? path.join(ROOT, 'review/specialists', `${name}.md`) : undefined);
|
||||
if (checklist && (role === 'specialist' || role === 'red-team') && !fs.existsSync(checklist)) throw new BriefError(`checklist not found: ${checklist}`);
|
||||
const checklist = flag('--checklist') ?? (role === 'red-team' ? path.join(ROOT, 'review/specialists/red-team.md') : role === 'targeted' ? path.join(ROOT, 'review/specialists/targeted.md') : role === 'specialist' && name ? path.join(ROOT, 'review/specialists', `${name}.md`) : undefined);
|
||||
if (checklist && (role === 'specialist' || role === 'red-team' || role === 'targeted') && !fs.existsSync(checklist)) throw new BriefError(`checklist not found: ${checklist}`);
|
||||
const brief = role === 'targeted' ? readFile(checklist, 'targeted brief') : undefined;
|
||||
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();
|
||||
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}`);
|
||||
let diff: string | undefined;
|
||||
@@ -94,6 +106,7 @@ function render(): number {
|
||||
name, diff_base: diffBase, exclude: flag('--exclude'), stack: flag('--stack'), test_fw: flag('--test-fw'),
|
||||
learnings: readFile(flag('--learnings-file'), 'learnings'), checklist, checklists: checklists?.join(','),
|
||||
findings: readFile(flag('--findings-file'), 'findings'), specialists: flag('--specialists'), diff,
|
||||
classes: flag('--classes'), brief,
|
||||
fix_list: readFile(flag('--fix-list'), 'fix list'), hunks: readFile(flag('--hunks'), 'hunks'), tests: readFile(flag('--tests'), 'tests'), callers: readFile(flag('--callers'), 'callers'),
|
||||
};
|
||||
const text = renderBrief(role, slots, sectionTexts(ROOT));
|
||||
@@ -102,7 +115,8 @@ function render(): number {
|
||||
fs.writeFileSync(file, text, { mode: 0o600 });
|
||||
const ledger = flag('--ledger') ?? path.join(out, 'review-ledger.jsonl');
|
||||
const sha = promptHash(text);
|
||||
appendEvent(ledger, { event: 'rendered', reader: readerKey(role, name), role, name: name ?? null, file, sha256: sha, bytes: Buffer.byteLength(text) });
|
||||
const targeted = brief === undefined ? {} : { classes: parseClassList(flag('--classes')!), rules: TARGETED_RULES, brief_sha: targetedBriefSha(checklist), diff_base: diffBase ?? null, patch_id: diffBase ? targetedPatchId(process.cwd(), diffBase) : 'none' };
|
||||
appendEvent(ledger, { event: 'rendered', reader: readerKey(role, name), role, name: name ?? null, file, sha256: sha, bytes: Buffer.byteLength(text), ...targeted });
|
||||
process.stdout.write(`BRIEF: reader=${readerKey(role, name)} sha256=${sha} bytes=${Buffer.byteLength(text)} file=${file}\n`);
|
||||
return EXIT.ok;
|
||||
}
|
||||
@@ -133,7 +147,7 @@ function pass(): number {
|
||||
if (text.status !== 0) { process.stdout.write(text.stdout ?? ''); process.stderr.write(text.stderr ?? ''); return EXIT.fail; }
|
||||
const j = plan(true);
|
||||
if (j.status !== 0) { process.stderr.write(j.stderr ?? ''); return EXIT.fail; }
|
||||
const p = JSON.parse(j.stdout) as { next: { step: string }; dispatches: Dispatch[] };
|
||||
const p = JSON.parse(j.stdout) as { next: { step: string }; dispatches: Dispatch[]; targeted?: { classes: string[] } };
|
||||
const planLines = text.stdout.trimEnd().split('\n');
|
||||
const step = p.next.step;
|
||||
if (step !== 'full' && step !== 'scoped') { process.stdout.write([...planLines, `PASS: none next=${step} (no reviewer pass; follow NEXT)`].join('\n') + '\n'); return EXIT.ok; }
|
||||
@@ -143,7 +157,8 @@ function pass(): number {
|
||||
const extrasFile = flag('--extras');
|
||||
const extras = extrasFile ? JSON.parse(readFile(extrasFile, 'extras')!) as Extra[] : [];
|
||||
const m = buildManifest({ pass: id, out, ledger: flag('--ledger') ?? path.join(out, 'review-ledger.jsonl'), base, diffBase, root, checklistRoot: flag('--checklist-root') ?? ROOT,
|
||||
dispatches: p.dispatches, next: step, planLines, learnings: readFile(flag('--learnings-file'), 'learnings') ?? null, extras });
|
||||
dispatches: p.dispatches, next: step, planLines, learnings: readFile(flag('--learnings-file'), 'learnings') ?? null, extras,
|
||||
targetedClasses: p.targeted?.classes, mergeBase: git(['merge-base', base, 'HEAD'])?.trim() });
|
||||
fs.mkdirSync(m.out, { recursive: true, mode: 0o700 });
|
||||
const t = sectionTexts(ROOT);
|
||||
renderStage(m, 'first', t, branchDiff);
|
||||
@@ -213,6 +228,23 @@ function program(): number {
|
||||
return EXIT.ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* The receipt's review_targeted record from the newest targeted dispatch: done only when its brief was sent verbatim, its output
|
||||
* collected, and the committed diff it was rendered on is still the one at verify (targetedPatchId; uncommitted never proves it).
|
||||
*/
|
||||
function targetedRecord(ledger: string, rows: VerifyRow[]): { classes: string[]; rules: string; brief_sha: string; status: 'done' | 'failed'; findings: number; raised_by: string } | null {
|
||||
const events = readLedger(ledger);
|
||||
const render = events.filter(e => e.event === 'rendered' && e.reader === 'targeted').at(-1);
|
||||
if (!render) return null;
|
||||
const label = rows.filter(r => r.kind === 'BRIEF' && r.reader.split('@')[0] === 'targeted').at(-1)?.reader;
|
||||
const brief = rows.filter(r => r.kind === 'BRIEF' && r.reader === label).at(-1);
|
||||
const output = rows.filter(r => r.kind === 'OUTPUT' && r.reader === label).at(-1);
|
||||
const read = typeof render.patch_id === 'string' && !['uncommitted', 'none'].includes(render.patch_id) && typeof render.diff_base === 'string' && targetedPatchId(process.cwd(), render.diff_base) === render.patch_id;
|
||||
const done = brief?.status === 'MATCH' && output?.status === 'MATCH' && read;
|
||||
const ids = events.filter(e => e.event === 'output' && e.reader === 'targeted').at(-1)?.ids;
|
||||
return { classes: (render.classes as string[]) ?? [], rules: String(render.rules), brief_sha: String(render.brief_sha), status: done ? 'done' : 'failed', findings: done && Array.isArray(ids) ? ids.length : 0, raised_by: 'targeted' };
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
const cmd = args[0];
|
||||
if (!cmd || cmd === '--help' || cmd === '-h') { process.stdout.write(HELP + '\n'); return cmd ? EXIT.ok : EXIT.usage; }
|
||||
@@ -253,6 +285,11 @@ function main(): number {
|
||||
const v = verify(ledger);
|
||||
for (const r of v.rows) process.stdout.write(`${r.kind} ${r.reader} ${r.status} ${r.detail}\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) {
|
||||
process.stdout.write(`TARGETED_RECORD: classes=${record.classes.join(',')} status=${record.status} findings=${record.findings} brief=${record.brief_sha.slice(0, 12)} rules=${record.rules}\n`);
|
||||
if (flag('--targeted-out')) fs.writeFileSync(flag('--targeted-out')!, JSON.stringify(record) + '\n');
|
||||
}
|
||||
process.stdout.write(`VERIFY: ${v.clean ? 'clean' : 'dirty'} briefs=${v.briefs} outputs=${v.rows.filter(r => r.kind === 'OUTPUT').length} voided=${v.voided}\n`);
|
||||
return v.clean ? EXIT.ok : EXIT.fail;
|
||||
}
|
||||
|
||||
+16
-6
@@ -4,14 +4,16 @@
|
||||
*
|
||||
* gstack-review-plan --base <ref> [--pass <full passes run>] [--last none|full|fix-check] [--fixed-blocking] [--fixed-below]
|
||||
* [--blocking-open] [--touched-gate] [--critical-found] [--adversarial-ran] [--gate-tagged <n>] [--gated a,b] [--force a,b|all]
|
||||
* [--reviewed-numbers <fingerprint>] [--depth-floor standard|full] [--range-base <sha>] [--pass1-depth light|standard|full] [--no-tiers] [--json]
|
||||
* [--reviewed-numbers <fingerprint>] [--depth-floor standard|full] [--range-base <sha>] [--pass1-depth light|standard|full] [--no-tiers]
|
||||
* [--targeted-ran <targeted_patch_id>] [--json]
|
||||
* gstack-review-plan --fix-check-prompt --fix-list <file> --hunks <file> --tests <file> [--callers <file>]
|
||||
*
|
||||
* Reads the diff against the merge base with <ref> (committed, tracked uncommitted
|
||||
* edits and untracked files, through lib/review-collect.ts collectLive), runs
|
||||
* gstack-diff-scope and, unless --gate-tagged is given, gstack-gate-diff; prints
|
||||
* REVIEW_CLASS:, REVIEW_DEPTH: (depth, runtime lines, the rules hash, reasons and
|
||||
* result codes), GATE_SCAN:, FIX_RANGE: (with --range-base), REVIEW_PLAN:, one DEPTH_ERROR: per input that failed
|
||||
* result codes), GATE_SCAN:, TARGETED: (the money, gate, auth and secret classes, lib/review-targeted.ts),
|
||||
* FIX_RANGE: (with --range-base), REVIEW_PLAN:, one DEPTH_ERROR: per input that failed
|
||||
* the depth closed, one DISPATCH: plus DIFF_CMD: (and DIFF_EXCLUDE: none) per
|
||||
* reviewer and one SKIP: per reviewer left out. A plan that fails closed to full
|
||||
* exits 0; a diff git cannot list exits 1. Logic: lib/review-depth.ts.
|
||||
@@ -23,6 +25,7 @@ import { EXIT, exitTableHelp } from '../lib/headless-artifacts';
|
||||
import { runScriptSync } from '../lib/script-invocation';
|
||||
import { collectCommit, collectLive } from '../lib/review-collect';
|
||||
import { classifyDiff, diffCommand, planReview, reviewDepthLine, reviewPlanLine, scanNumbers, SCOPED_RANGE_LINES, type Depth, type History } from '../lib/review-depth';
|
||||
import { classList, targetedBriefSha, targetedClasses, targetedPatchId } from '../lib/review-targeted';
|
||||
import { renderFixCheckPrompt } from '../scripts/resolvers/fix-check';
|
||||
|
||||
const HELP = `gstack-review-plan — the review depth of the next /ship step (docs/review-policy.md#review-depth)
|
||||
@@ -49,9 +52,12 @@ const HELP = `gstack-review-plan — the review depth of the next /ship step (do
|
||||
--no-tiers specialists, audits and watchers at the host's default effort too (as gstack-config review_effort_tiers off does)
|
||||
--json the plan as JSON (depth, runtime_lines, rules, errors with input and repair, gate_scan, per-dispatch exclude)
|
||||
--pass1-depth <d> pass 1's REVIEW_DEPTH depth: full, or absent, keeps pass 2 full
|
||||
--targeted-ran <id> the TARGETED line's targeted_patch_id (the whole committed diff, tests included) when the targeted
|
||||
reader last read it: the same patch skips it; any other id, none, or uncommitted edits dispatch it again
|
||||
--fix-check-prompt print the fix-check reviewer's prompt from --fix-list, --hunks, --tests and --callers files
|
||||
|
||||
Depth: light (runtime lines <= 50), standard (above 50), full (gate class or any DEPTH_ERROR). NEXT: full, scoped
|
||||
Depth: light (runtime lines <= 50), standard (above 50), full (gate class or any DEPTH_ERROR). TARGETED: the money,
|
||||
gate, auth and secret classes the diff touches; any class dispatches the targeted reader at every depth. NEXT: full, scoped
|
||||
(a full pass on the range: fix-check, Red Team, native adversarial), fix-check, stop or done. A full plan
|
||||
from an error exits 0; exit 1 means no plan could be built (git could not list the diff).
|
||||
${exitTableHelp()}`;
|
||||
@@ -131,18 +137,22 @@ function main(): number {
|
||||
: { base: rangeBase, runtime_lines: range.runtimeLines, ...(range.class === 'gate' ? { touched_gate: true as const } : {}) };
|
||||
}
|
||||
}
|
||||
const targeted = targetedClasses({ ...collected, scope, gateTagged });
|
||||
const patch = collected.mergeBase ? targetedPatchId(process.cwd(), collected.mergeBase) : 'none';
|
||||
const brief = targetedBriefSha();
|
||||
const plan = planReview({
|
||||
...collected, scope, gateTagged, numbers: scanNumbers(added), force, gated: list('--gated'), floor, tiers: auditEffort !== 'host',
|
||||
...collected, scope, gateTagged, targeted: { classes: targeted.classes, ran: !['uncommitted', 'none'].includes(patch) && flag('--targeted-ran') === patch }, numbers: scanNumbers(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') && last !== 'fix-check', blockingOpen: has('--blocking-open'), touchedGate: has('--touched-gate') || !!fixRange?.touched_gate, pass1Depth, rangeRuntimeLines: fixRange?.runtime_lines },
|
||||
});
|
||||
const gateScan = { tagged: gateTagged, head };
|
||||
if (has('--json')) {
|
||||
const { runtimeLines, stepDepth, depthReasons, ...rest } = plan;
|
||||
process.stdout.write(JSON.stringify({ ...rest, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed }) + '\n');
|
||||
process.stdout.write(JSON.stringify({ ...rest, targeted: { ...targeted, brief, targeted_patch_id: patch }, step_depth: stepDepth, runtime_lines: runtimeLines, depth_reasons: depthReasons, depth_floor: floor ?? null, gate_scan: gateScan, fix_range: fixRange ?? null, registry: { sha: registry.sha, outputs: registry.outputs }, per_path: perPath, diff_lines: diffLines, pass, audit_effort: auditEffort, scope, gate_tagged: gateTagged, paths, deleted, stamp_only: stampOnly, consumed: collected.consumed }) + '\n');
|
||||
return EXIT.ok;
|
||||
}
|
||||
const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`];
|
||||
const out = [`REVIEW_CLASS: ${plan.class} (${plan.reasons.slice(0, 3).join('; ') || 'no signals'})`, reviewDepthLine(plan), `GATE_SCAN: ${gateTagged === null ? 'unavailable' : `tagged=${gateTagged}`} head=${head}`,
|
||||
`TARGETED: classes=${classList(targeted.classes)} rules=${targeted.rules} brief=${brief} targeted_patch_id=${patch}${targeted.reasons.length ? ` reasons=${targeted.reasons.join('; ')}` : ''}`];
|
||||
if (fixRange) out.push(`FIX_RANGE: ${fixRange.runtime_lines === null ? `unread (${fixRange.error})` : `runtime_lines=${fixRange.runtime_lines}`} base=${fixRange.base}`);
|
||||
out.push(reviewPlanLine(plan, pass + (plan.next.step === 'full' || plan.next.step === 'scoped' ? 1 : 0)), `NEXT: ${plan.next.step} (${plan.next.reason})`);
|
||||
for (const e of plan.errors) out.push(`DEPTH_ERROR: ${e.code} input=${e.input} detail=${e.detail}; fix: ${e.repair}${e.result ? ` (${e.result})` : ''}`);
|
||||
|
||||
+12
-3
@@ -44,7 +44,7 @@ import {
|
||||
receiptIssuesFrom, receiptSummaryLine, renderCensus, renderReceiptBlock, validateReceipt, type CensusPr,
|
||||
} from '../lib/ship-receipt';
|
||||
import { checkDocsCaps, docsCapLine, docsExceptionLine, parseReceiptDocs } from '../lib/ship-receipt-docs';
|
||||
import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, receiptDepthAtHead, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
|
||||
import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, parseReviewTargeted, receiptDepthAtHead, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
|
||||
import { checkReceiptProse, proseLine } from '../lib/ship-receipt-prose';
|
||||
import { resolveStateRoot } from '../lib/state-root';
|
||||
import { isRegistryUnavailable, loadRegistryFromBase } from '../lib/regen';
|
||||
@@ -89,6 +89,9 @@ write flags: --pr --head --base --version --gated-tree --predecessor --gate <jso
|
||||
"Review-loop exception: …" line verbatim; else REVIEW_LOOP_EXCEPTION_INVALID). Every receipt records gstack_version and
|
||||
review_depth: the depth HEAD needs, recomputed from the committed range (--head must be HEAD; a dirty checkout or an
|
||||
unreadable input records full).
|
||||
--review-targeted <json|@file> ({classes, rules, brief_sha, status, findings, raised_by}: gstack-review-dispatch verify
|
||||
--targeted-out); recorded as review_targeted bound to the receipt's product_patch_id. Without it the receipt has no
|
||||
review_targeted, which the queue reports as legacy.
|
||||
--approval-run <autoplan run dir> (repeatable; resolves plan-sha citations) (W2: a class declared in origin/<base>'s
|
||||
.gstack/approval-classes.json with hits needs a citation that resolves and binds to its effects: APPROVAL_CITATION_MISSING)
|
||||
--out <file> --body <pr-body.md>. Defaults: head=HEAD, base=origin/HEAD's branch, version=the
|
||||
@@ -207,6 +210,12 @@ function cmdWrite(rest: string[], json: boolean): number {
|
||||
if (bodyFile && !bodyText.split(/\r?\n/).some(l => l.trim() === line)) return refuse(`${bodyFile} does not carry the exception line verbatim: ${JSON.stringify(line)}`);
|
||||
}
|
||||
const reviewDepth = receiptDepthAtHead(repoRoot, base, path.dirname(Bun.fileURLToPath(import.meta.url)), headSha ?? head);
|
||||
const product = reviewedProduct(repoRoot, base, head, pregateFile);
|
||||
let targeted: ReturnType<typeof parseReviewTargeted> | undefined;
|
||||
if (flag(rest, '--review-targeted') !== undefined) {
|
||||
try { targeted = parseReviewTargeted(jsonArg(flag(rest, '--review-targeted')), product.product_patch_id ?? 'none'); } catch (e: any) { targeted = { ok: false, message: `--review-targeted: ${e.message}` }; }
|
||||
}
|
||||
if (targeted && !targeted.ok) { process.stderr.write(resultLine('gstack-ship-receipt write', 'RECEIPT_INVALID', `${targeted.message}; nothing written`) + '\n'); return EXIT.fail; }
|
||||
const approval = approvalForReceipt({ repoRoot, base, head, citationText: [bodyText, ...flags(rest, '--approval')].join('\n'), runDirs: flags(rest, '--approval-run') });
|
||||
if (approval.kind === 'invalid') {
|
||||
for (const line of approval.lines) process.stderr.write(resultLine('gstack-ship-receipt write', 'APPROVAL_CLASSES_INVALID', line) + '\n');
|
||||
@@ -236,8 +245,8 @@ function cmdWrite(rest: string[], json: boolean): number {
|
||||
session_kind: sessionKind, artifacts_consumed: flag(rest, '--artifacts-consumed'), run: flag(rest, '--run'), thread: flag(rest, '--thread'),
|
||||
policy: flag(rest, '--policy') ?? tree?.policy, queue_mode: flag(rest, '--queue-mode'), preregistration: flag(rest, '--preregistration'), history: flag(rest, '--history'),
|
||||
...pregate, issues: issuesFile ? receiptIssuesFrom(issuesFile) : undefined, approval: approval.kind === 'ok' ? approval.approval : undefined,
|
||||
review_loop: reviewLoop, ...(docs?.ok ? { docs: docs.docs } : {}), ...reviewedProduct(repoRoot, base, head, pregateFile),
|
||||
gstack_version: runningGstackVersion(), review_depth: reviewDepth, review_loop_exception: loopException?.ok ? loopException.exception : undefined,
|
||||
review_loop: reviewLoop, ...(docs?.ok ? { docs: docs.docs } : {}), ...product,
|
||||
gstack_version: runningGstackVersion(), review_depth: reviewDepth, review_loop_exception: loopException?.ok ? loopException.exception : undefined, review_targeted: targeted?.ok ? targeted.targeted : undefined,
|
||||
};
|
||||
} catch (e: any) { process.stderr.write(`gstack-ship-receipt write: ${e.message}\n`); return EXIT.usage; }
|
||||
if (input.pr !== undefined && !Number.isInteger(input.pr)) { process.stderr.write('gstack-ship-receipt write: --pr must be an integer\n'); return EXIT.usage; }
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
|
||||
### Fixed
|
||||
- **The pass program takes its input as the Capy skill writes it.** `extras` may be JSON in the input, not only a file path, and `cwd` names the checkout its commands run in. A reader that reports done a moment before its summary arrives is collected on a later poll instead of stopping the pass. Replay C2-34 (GSTA-193) lost 7 of its 15 calls to these three issues.
|
||||
- **The targeted reader runs in the pass.** When the plan dispatches it for money, gate, auth or secret code, it is staged with Step 11's voices on the merge base. Its brief matches `gstack-review-dispatch render --role targeted` byte for byte, and its ledger row carries the classes, rules, brief and patch the receipt's targeted record reads.
|
||||
+56
-2
@@ -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,
|
||||
@@ -285,6 +285,59 @@ the specialists, audits and watchers at the host default too
|
||||
part 4a). A binary image no code names is no longer a `binary_code_path` error
|
||||
that forces full depth; one that code names still is.
|
||||
|
||||
## Targeted pass on money, gate, auth and secret code
|
||||
|
||||
Some code is costly to get wrong: code that spends money, code that decides
|
||||
whether a check passes, code that decides who gets in, and code that handles
|
||||
secrets. When a diff touches one of these, `gstack-review-plan` dispatches one
|
||||
more reader, `targeted`, at every depth, light included. It reads the whole
|
||||
diff, fixtures and tests included, with the blocks of
|
||||
`review/specialists/targeted.md` for the classes that hit, and it runs in Step
|
||||
11 beside the native adversarial pass and the outside voice (GST-257).
|
||||
|
||||
`lib/review-targeted.ts` decides the classes from added and deleted lines, so
|
||||
removing a cap, a check or a redaction counts. Docs and tests paths never count,
|
||||
and matching is token-bounded: `costume` never reads as cost.
|
||||
|
||||
| Class | Signal |
|
||||
|---|---|
|
||||
| money | a path segment naming cost, spend, budget, ledger, billing, price, fee or invoice; or a changed line with an identifier ending in `_usd` or `Usd`, any `spend_cap` spelling, or a call to a paid provider `lib/pricing.ts` prices |
|
||||
| gate | a hunk the gate scan tagged: a relaxed threshold, a skipped test, a loosened check |
|
||||
| auth | `SCOPE_AUTH` from `gstack-diff-scope`, on a code path with an auth, session, jwt, permission or role token in its name |
|
||||
| secret | a secrets, credentials, keychain or token path, or a changed line that reads a key-named environment variable (`OPENAI_API_KEY`, `GH_TOKEN`) or a credential store |
|
||||
|
||||
An input the classifier cannot read (the diff, `gstack-diff-scope`, the gate
|
||||
scan) fails closed to all four classes. `TARGETED_RULES` hashes the patterns.
|
||||
The classifier and the brief are gate paths, so an edit that weakens them gets
|
||||
full review.
|
||||
|
||||
The plan prints `TARGETED: classes=<money,gate,auth,secret|none> rules=<hash>
|
||||
brief=<sha256 of the brief> targeted_patch_id=<id>` and, when a class hit,
|
||||
`DISPATCH: targeted targeted effort=full`. The targeted reader always reads the whole diff: `render --role targeted`
|
||||
refuses a `--diff-base` other than the merge base, so on pass 2 and later it
|
||||
gets the merge base, not `RANGE_BASE`. After a fix, pass
|
||||
`--targeted-ran <targeted_patch_id>` from the line the reader ran on: the same
|
||||
committed patch skips the reader, and a changed one, uncommitted edits or
|
||||
`none` dispatch it again. `targeted_patch_id` is the patch-id of the whole
|
||||
committed diff, tests included, so a test-only fix that adds a gate hunk
|
||||
dispatches it again; it is not the receipt's `product_patch_id`.
|
||||
|
||||
The brief states the blocking floor in one sentence, and its money block cites
|
||||
the checklist's Forced-CRITICAL rule on uncapped spend, so a money finding can
|
||||
block without new floor words. The reader is read-only, and `NO FINDINGS` is a
|
||||
result. `gstack-review-dispatch render --role targeted --classes <classes>`
|
||||
renders its brief; `verify --targeted-out <file>` writes the record that
|
||||
`gstack-ship-receipt write --review-targeted @<file>` puts in the receipt.
|
||||
`gstack-ship-queue ready` and `train build` print
|
||||
`REVIEW_TARGETED: required=<classes|none> receipt=<classes|none|legacy>
|
||||
verdict=ok|missing` and refuse nothing yet; enforcement comes in a later
|
||||
release.
|
||||
|
||||
Over the last 100 non-release commits on main (8663c3d2..8be7f213), money hit
|
||||
21, gate 18, auth 3 and secret 8. The gate class first used the gate paths,
|
||||
which hit 43 of 100; those paths already get full depth, so the gate class was
|
||||
narrowed to the gate scan's tagged hunks.
|
||||
|
||||
## What the receipt records
|
||||
|
||||
`gstack-review-scope summary --since <REVIEW_SINCE>` reads this invocation's
|
||||
@@ -300,8 +353,9 @@ puts in the receipt:
|
||||
| `review_loop.fixed` | findings fixed in this PR |
|
||||
| `review_loop.scope` | the `review_scope` the loop ran under |
|
||||
| `review_loop.fix_checks` | fix-checks this ship ran (never passes) |
|
||||
| `review_loop.dispatches` | reviewer dispatches by role: `specialist`, `red-team`, `adversarial-native`, `outside-voice`, `fix-check` |
|
||||
| `review_loop.dispatches` | reviewer dispatches by role: `specialist`, `red-team`, `adversarial-native`, `outside-voice`, `fix-check`, `targeted` |
|
||||
| `review_loop.est_input_tokens` | prompt plus diff bytes the dispatches read, divided by 4: a relative estimate for comparing runs, not a bill |
|
||||
| `review_targeted` | the [targeted pass](#targeted-pass-on-money-gate-auth-and-secret-code): the `classes` it covered, `rules`, `brief_sha`, the receipt's own `product_patch_id`, `status` (`done` only with its output collected and the committed diff it was rendered on still the one at verify, else `failed`; absent when write got no record, which the queue reports as `legacy`), `findings` and `raised_by`; a carry keeps it while the product patch is unchanged |
|
||||
|
||||
Each review row may carry `dispatches: [{role, effort, prompt_bytes,
|
||||
diff_bytes, status}]`, and each finding `raised_by` (the reviewer that raised it,
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface ShipReceipt {
|
||||
review_depth?: { depth: 'light' | 'standard' | 'full'; class: 'docs' | 'tests' | 'code' | 'gate'; runtime_lines: number; rules: string; base: string; registry: string | null; gate_scan: { tagged: number | null; head: string }; reasons: string[] };
|
||||
/** The owner's acceptance of a stop after a fix-check edit under frozen scope (#3559); the PR body repeats it word for word. */
|
||||
review_loop_exception?: { accepted_by: string; date: string; via: string; reason: string; fix_check: { head: string; findings: number; files?: string[] } };
|
||||
/** The targeted pass on money, gate, auth and secret code (lib/review-targeted.ts), bound to the receipt's product_patch_id; `done` only with its output collected. */
|
||||
review_targeted?: { classes: string[]; rules: string; brief_sha: string; product_patch_id: string; status: 'done' | 'failed' | 'not-run'; findings: number; raised_by: string };
|
||||
}
|
||||
|
||||
export const RESULT_STATUSES: readonly ResultStatus[] = ['complete', 'gate_pending', 'amend_pending', 'incomplete', 'interrupted', 'refused'];
|
||||
@@ -234,6 +236,7 @@ export const ARTIFACT_SCHEMAS = {
|
||||
carry: { type: 'object', required: ['from', 'reason', 'product_patch_id'], properties: { from: str, reason: str, product_patch_id: str, changed_paths: strList, merge_commits: { type: 'integer', minimum: 0 }, range_diff: str, covers: strList, reruns: strList, trees: strList, ancestry: { enum: ['linear', 'rewritten'] } } },
|
||||
head_tree: str, product_patch_id: str, gstack_version: str,
|
||||
review_depth: { type: 'object', required: ['depth', 'class', 'runtime_lines', 'rules', 'base', 'gate_scan'], properties: { depth: { enum: ['light', 'standard', 'full'] }, class: { enum: ['docs', 'tests', 'code', 'gate'] }, runtime_lines: { type: 'integer', minimum: 0 }, rules: str, base: str, registry: nullableStr, gate_scan: { type: 'object', required: ['tagged', 'head'], properties: { tagged: { type: ['integer', 'null'], minimum: 0 }, head: str } }, reasons: strList } },
|
||||
review_targeted: { type: 'object', required: ['classes', 'rules', 'brief_sha', 'product_patch_id', 'status', 'findings', 'raised_by'], properties: { classes: { type: 'array', items: { enum: ['money', 'gate', 'auth', 'secret'] } }, rules: str, brief_sha: str, product_patch_id: str, status: { enum: ['done', 'failed', 'not-run'] }, findings: { type: 'integer', minimum: 0 }, raised_by: str } },
|
||||
review_loop_exception: { type: 'object', required: ['accepted_by', 'date', 'via', 'reason', 'fix_check'], properties: { accepted_by: str, date: str, via: str, reason: str, fix_check: { type: 'object', required: ['head', 'findings'], properties: { head: str, findings: { type: 'integer', minimum: 0 }, files: strList } } } },
|
||||
issues: { type: 'object', properties: { fixes: { type: 'array', items: { type: 'integer' } }, refs: { type: 'array', items: { type: 'integer' } }, flakes: { type: 'array', items: { type: 'object', required: ['issue', 'run'], properties: { issue: { type: 'integer' }, run: str } } }, related: { type: 'array', items: { type: 'object', required: ['pr', 'author', 'closes'], properties: { pr: { type: 'integer' }, repo: str, author: str, closes: { type: 'array', items: { type: 'integer' } }, supersedes: { type: 'boolean' }, credit: { type: 'boolean' } } } } } },
|
||||
},
|
||||
|
||||
+11
-1
@@ -12,13 +12,15 @@
|
||||
* adversarial-native diff_base
|
||||
* outside-voice diff_base, diff (the branch diff, which the bin reads from git)
|
||||
* fix-check fix_list, hunks, tests, callers (scripts/resolvers/fix-check.ts)
|
||||
* targeted classes, brief (review/specialists/targeted.md's text), checklist (its path), diff_base, stack, test_fw, learnings
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { renderFixCheckPrompt } from '../scripts/resolvers/fix-check';
|
||||
import { parseClassList, targetedBrief } from './review-targeted';
|
||||
|
||||
export const BRIEF_ROLES = ['specialist', 'specialist-batched', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check'] as const;
|
||||
export const BRIEF_ROLES = ['specialist', 'specialist-batched', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', 'targeted'] as const;
|
||||
export type BriefRole = typeof BRIEF_ROLES[number];
|
||||
export type Slots = Record<string, string | undefined>;
|
||||
|
||||
@@ -119,6 +121,14 @@ function renderRole(role: BriefRole, slots: Slots, t: SectionTexts): string {
|
||||
const body = fill(t.outside, [['DIFF_BASE=$(git merge-base origin/<base> HEAD)', `DIFF_BASE=${diffBase}`]], role);
|
||||
return [body, '', `## Branch diff (git diff ${diffBase})`, FENCE + 'diff', (slots.diff ?? '').trim(), FENCE].join('\n');
|
||||
}
|
||||
case 'targeted': {
|
||||
const classes = parseClassList(need(slots, 'classes'));
|
||||
if (!classes?.length) throw new BriefError(`slot classes must list money, gate, auth or secret, got ${JSON.stringify(slots.classes)}`);
|
||||
let brief: string;
|
||||
try { brief = targetedBrief(need(slots, 'brief'), classes); } catch (e) { throw new BriefError((e as Error).message); }
|
||||
const body = fill(t.specialist, [['Read the checklist at {checklist path}', `Read the targeted brief above (${need(slots, 'checklist')}: ${classes.join(', ')})`]], role);
|
||||
return `${brief}\n\n${common(body, '')}`;
|
||||
}
|
||||
case 'fix-check':
|
||||
return renderFixCheckPrompt({ fixList: need(slots, 'fix_list'), touchedHunks: need(slots, 'hunks'), testResults: need(slots, 'tests'), callers: slots.callers ?? '' });
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface Collected extends ClassInput {
|
||||
stampOnly: string[];
|
||||
/** Added lines per path, for scanNumbers. */
|
||||
added: Record<string, string[]>;
|
||||
/** Deleted lines per path, for the targeted classes (lib/review-targeted.ts). */
|
||||
removed: Record<string, string[]>;
|
||||
diffLines: number;
|
||||
mergeBase: string | null;
|
||||
untracked: string[];
|
||||
@@ -52,11 +54,11 @@ export function loadRegistryAt(root: string, sha: string): RegistryInfo {
|
||||
}
|
||||
|
||||
/** Numstat, deletions and the -U0 patch for `range` (`[mergeBase, head]` or `[mergeBase]` for the working tree). */
|
||||
function readDiff(root: string, range: string[], errors: DepthError[]): Pick<Collected, 'perPath' | 'deleted' | 'stampOnly' | 'added'> {
|
||||
function readDiff(root: string, range: string[], errors: DepthError[]): Pick<Collected, 'perPath' | 'deleted' | 'stampOnly' | 'added' | 'removed'> {
|
||||
const numstat = git(root, ['diff', '--numstat', '-z', '--no-renames', ...range]);
|
||||
const status = git(root, ['diff', '--name-status', '-z', '--no-renames', '--diff-filter=D', ...range]);
|
||||
const patch = git(root, ['diff', '-U0', '--no-color', '--no-ext-diff', '--no-renames', ...range]);
|
||||
const empty = { perPath: [], deleted: [], stampOnly: [], added: {} };
|
||||
const empty = { perPath: [], deleted: [], stampOnly: [], added: {}, removed: {} };
|
||||
for (const [r, what] of [[numstat, '--numstat'], [status, '--name-status'], [patch, '-U0']] as const) {
|
||||
if (!r.ok) { errors.push({ code: 'numstat_failed', detail: oneLine(r.err || 'git diff failed'), input: `git diff ${what} ${range.join(' ')}`, repair: 'check that both commits exist locally (git fetch origin <base>) and rerun' }); return empty; }
|
||||
}
|
||||
@@ -69,6 +71,7 @@ function readDiff(root: string, range: string[], errors: DepthError[]): Pick<Col
|
||||
const statusParts = status.out.split('\0');
|
||||
const deleted = statusParts.flatMap((s, k) => (s === 'D' && statusParts[k + 1] ? [statusParts[k + 1]!] : [])).filter(p => !scratch(p));
|
||||
const added: Record<string, string[]> = {};
|
||||
const removed: Record<string, string[]> = {};
|
||||
const changed: Record<string, string[]> = {};
|
||||
let cur = '';
|
||||
let minus = '';
|
||||
@@ -83,10 +86,11 @@ function readDiff(root: string, range: string[], errors: DepthError[]): Pick<Col
|
||||
}
|
||||
if (!cur || scratch(cur) || line.startsWith('@@')) continue;
|
||||
if (line[0] === '+') (added[cur] ??= []).push(line.slice(1));
|
||||
if (line[0] === '-') (removed[cur] ??= []).push(line.slice(1));
|
||||
if (line[0] === '+' || line[0] === '-') (changed[cur] ??= []).push(line);
|
||||
}
|
||||
const stampOnly = perPath.map(e => e.path).filter(p => p.endsWith('package.json') && !!changed[p]?.length && changed[p]!.every(l => /^[+-]\s*"version"\s*:\s*"[^"]*"\s*,?\s*$/.test(l)));
|
||||
return { perPath, deleted, stampOnly, added };
|
||||
return { perPath, deleted, stampOnly, added, removed };
|
||||
}
|
||||
|
||||
/** More data candidates than this keep every data file code: the consumer scan is not run. */
|
||||
@@ -154,7 +158,7 @@ export function collectCommit(root: string, baseSha: string, headSha: string): C
|
||||
const mb = git(root, ['merge-base', baseSha, headSha]);
|
||||
if (!mb.ok) {
|
||||
errors.push({ code: 'numstat_failed', detail: oneLine(mb.err || 'no merge base'), input: `git merge-base ${baseSha} ${headSha}`, repair: 'check that both commits exist locally (git fetch origin <base>) and rerun' });
|
||||
return result({ perPath: [], deleted: [], stampOnly: [], added: {} }, { root: [], registry, errors, mergeBase: null, untracked: [], consumed: 'all' });
|
||||
return result({ perPath: [], deleted: [], stampOnly: [], added: {}, removed: {} }, { root: [], registry, errors, mergeBase: null, untracked: [], consumed: 'all' });
|
||||
}
|
||||
const mergeBase = mb.out.trim();
|
||||
const diff = readDiff(root, [mergeBase, headSha], errors);
|
||||
@@ -172,7 +176,7 @@ export function collectLive(root: string, base: string): Collected {
|
||||
const mb = git(root, ['merge-base', ref, 'HEAD']);
|
||||
if (!mb.ok) {
|
||||
errors.push({ code: 'numstat_failed', detail: oneLine(mb.err || `no merge base with ${ref}`), input: `git merge-base ${ref} HEAD`, repair: `fetch the base first (git fetch origin ${base})` });
|
||||
return result({ perPath: [], deleted: [], stampOnly: [], added: {} }, { root, registry, errors, mergeBase: null, untracked: [], consumed: 'all' });
|
||||
return result({ perPath: [], deleted: [], stampOnly: [], added: {}, removed: {} }, { root, registry, errors, mergeBase: null, untracked: [], consumed: 'all' });
|
||||
}
|
||||
const mergeBase = mb.out.trim();
|
||||
const diff = readDiff(root, [mergeBase], errors);
|
||||
|
||||
+9
-3
@@ -18,7 +18,7 @@ export type DiffClass = 'docs' | 'tests' | 'code' | 'gate';
|
||||
export type PathClass = 'docs' | 'tests' | 'code';
|
||||
export type Depth = 'light' | 'standard' | 'full';
|
||||
export type Effort = 'light' | 'medium' | 'full' | 'host';
|
||||
export type Role = 'specialist' | 'red-team' | 'adversarial-native' | 'outside-voice' | 'fix-check';
|
||||
export type Role = 'specialist' | 'red-team' | 'adversarial-native' | 'outside-voice' | 'fix-check' | 'targeted';
|
||||
export type Step = 'full' | 'scoped' | 'fix-check' | 'done' | 'stop';
|
||||
|
||||
export const SPECIALISTS = ['testing', 'maintainability', 'security', 'performance', 'data-migration', 'api-contract', 'design', 'simplification', 'state-machines'] as const;
|
||||
@@ -38,7 +38,7 @@ export const BATCH_SCOPED: readonly Specialist[] = ['data-migration', 'api-contr
|
||||
* them; the Red Team, the native adversarial pass and the fix-check at full (the lane's reasoning); the outside voice at
|
||||
* its own provider's. Light's batched specialist carries the Red Team prompt, so it stays full.
|
||||
*/
|
||||
export const ROLE_EFFORT = { specialist: 'medium', 'red-team': 'full', 'adversarial-native': 'full', 'fix-check': 'full', 'outside-voice': 'host' } as const satisfies Record<Role, Effort>;
|
||||
export const ROLE_EFFORT = { specialist: 'medium', 'red-team': 'full', 'adversarial-native': 'full', 'fix-check': 'full', 'outside-voice': 'host', targeted: 'full' } as const satisfies Record<Role, Effort>;
|
||||
/** Why light with runtime above 0 keeps the native adversarial pass as its own dispatch. */
|
||||
export const LIGHT_ADVERSARIAL_REASON = 'backtest inconclusive (GSTA-147 D5)';
|
||||
const CLASS_RANK: Record<DiffClass, number> = { docs: 0, tests: 1, code: 2, gate: 3 };
|
||||
@@ -53,7 +53,7 @@ const RELEASE_METADATA = [/^CHANGELOG\.md$/, /^VERSION$/, /^agents-digest\//];
|
||||
/** Gate paths, the review-depth machinery included: an edit to how much review runs gets the most review. */
|
||||
const GATE_PATH = [
|
||||
/(^|\/)\.github\/workflows\//, /(^|\/)(secrets?|credentials?|keychain|token[s]?)([._/-]|$)/i, /state-root/, /(^|\/)(pregate|gate|ship-receipt|ship-policy)[^/]*\.(ts|sh|json)$/, /(^|\/)bin\/gstack-(pregate|gate|ship-receipt|verify-gate|ci-gate)$/,
|
||||
/^lib\/review-(depth|collect|scope)\.ts$/, /^bin\/gstack-review-plan$/, /^lib\/ship-queue[^/]*\.ts$/, /^lib\/regen\.ts$/, /^\.gstack\/generated\.json$/, /^ship\/sections\/(review-army|adversarial)\.md\.tmpl$/, /^scripts\/resolvers\/review-army\.ts$/,
|
||||
/^lib\/review-(depth|collect|scope|targeted)\.ts$/, /^review\/specialists\/targeted\.md$/, /^bin\/gstack-review-plan$/, /^lib\/ship-queue[^/]*\.ts$/, /^lib\/regen\.ts$/, /^\.gstack\/generated\.json$/, /^ship\/sections\/(review-army|adversarial)\.md\.tmpl$/, /^scripts\/resolvers\/review-army\.ts$/,
|
||||
];
|
||||
|
||||
/** Data files: JSON, CSV, TSV and raster images. YAML, TOML and SVG are never data here: deploy configs, API specs and executable SVG stay code. */
|
||||
@@ -291,6 +291,8 @@ export interface PlanInput extends ClassInput {
|
||||
floor?: Depth;
|
||||
/** false under --no-tiers or review_effort_tiers off: specialists run at the host's default effort. */
|
||||
tiers?: boolean;
|
||||
/** The targeted classes (lib/review-targeted.ts) and whether the targeted reader already read this product patch. */
|
||||
targeted?: { classes: readonly string[]; ran?: boolean };
|
||||
}
|
||||
/** One reviewer. `exclude` holds git pathspec argv items the reviewer's diff leaves out; `checklists` names what the batched specialist reads. */
|
||||
export interface Dispatch { role: Role; name: string; effort: Effort; exclude: string[]; checklists?: string[]; reason?: string }
|
||||
@@ -432,6 +434,10 @@ export function planReview(i: PlanInput): ReviewPlan {
|
||||
add('fix-check', 'fix-check', 'full');
|
||||
if (numbersChanged) add('outside-voice', 'outside-voice (numbers changed)', 'host');
|
||||
}
|
||||
if (i.targeted?.classes.length && ['full', 'scoped', 'fix-check'].includes(next.step)) {
|
||||
if (i.targeted.ran) skipped.push('targeted (already read this product patch)');
|
||||
else add('targeted', 'targeted', ROLE_EFFORT.targeted);
|
||||
}
|
||||
return { ...depth, stepDepth, depthReasons, reasons: c.reasons, next, numbers: i.numbers, dispatches, skipped };
|
||||
}
|
||||
|
||||
|
||||
+16
-3
@@ -16,6 +16,7 @@ import * as path from 'node:path';
|
||||
import { BriefError, briefFile, promptHash, renderBrief, type BriefRole, type SectionTexts, type Slots } from './review-brief';
|
||||
import { appendEvent, readerKey } from './review-ledger';
|
||||
import { diffCommand, type Dispatch } from './review-depth';
|
||||
import { TARGETED_RULES, parseClassList, targetedBriefSha, targetedPatchId } from './review-targeted';
|
||||
|
||||
export const STAGES = ['first', 'specialists', 'red-team', 'voices'] as const;
|
||||
export type Stage = typeof STAGES[number];
|
||||
@@ -25,6 +26,8 @@ export interface PassModel { model_id: string; reasoning_mode?: string }
|
||||
export interface PassReader {
|
||||
reader: string; stage: Stage; title: string; state: ReaderState;
|
||||
role?: BriefRole; name?: string; effort?: string; exclude?: string; checklists?: string[];
|
||||
/** The targeted reader: the plan's TARGETED classes and the merge base it reads on every pass. */
|
||||
classes?: string[]; diff_base?: string;
|
||||
/** An audit or QA reader: its prompt is a file the parent wrote from its section; its output is returned, never collected. */
|
||||
prompt_file?: string; model?: PassModel;
|
||||
file?: string; sha256?: string; bytes?: number; chunks?: number; findings_sha256?: string;
|
||||
@@ -63,12 +66,13 @@ export function detectTestFw(root: string): string {
|
||||
}
|
||||
|
||||
const safe = (reader: string) => reader.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const STAGE_OF: Record<string, Stage> = { specialist: 'specialists', 'red-team': 'red-team', 'adversarial-native': 'voices', 'outside-voice': 'voices' };
|
||||
const STAGE_OF: Record<string, Stage> = { specialist: 'specialists', 'red-team': 'red-team', 'adversarial-native': 'voices', 'outside-voice': 'voices', targeted: 'voices' };
|
||||
|
||||
/** The manifest for one pass from the plan's dispatches. A fix-check is not part of a pass, so it is refused. */
|
||||
export function buildManifest(o: {
|
||||
pass: string; out: string; ledger: string; base: string; diffBase: string; root: string; checklistRoot: string;
|
||||
dispatches: Dispatch[]; next: string; planLines: string[]; learnings?: string | null; extras?: Extra[];
|
||||
targetedClasses?: readonly string[]; mergeBase?: string;
|
||||
}): Manifest {
|
||||
const readers: PassReader[] = [];
|
||||
for (const d of o.dispatches) {
|
||||
@@ -78,7 +82,8 @@ export function buildManifest(o: {
|
||||
const name = role === 'specialist' ? d.name : undefined;
|
||||
const reader = readerKey(role, name);
|
||||
const exclude = d.exclude.length ? diffCommand(d).slice('git diff <diff-base> '.length) : undefined;
|
||||
readers.push({ reader, stage, title: `ship ${o.pass} ${reader}`, state: 'planned', role, name, effort: d.effort, exclude, checklists: d.checklists });
|
||||
readers.push({ reader, stage, title: `ship ${o.pass} ${reader}`, state: 'planned', role, name, effort: d.effort, exclude, checklists: d.checklists,
|
||||
...(role === 'targeted' ? { classes: [...(o.targetedClasses ?? [])], diff_base: o.mergeBase ?? o.diffBase } : {}) });
|
||||
}
|
||||
for (const e of o.extras ?? []) {
|
||||
if (!/^[a-z0-9-]+$/.test(e.name)) throw new BriefError(`an audit reader's name is lowercase letters, digits and dashes: ${e.name}`);
|
||||
@@ -142,12 +147,20 @@ export function renderStage(m: Manifest, stage: Stage, t: SectionTexts, diffFor:
|
||||
Object.assign(slots, { checklist: path.join(m.checklist_root, 'review/specialists/red-team.md'), findings, specialists: String(specialists) });
|
||||
}
|
||||
if (role === 'outside-voice') slots.diff = diffFor(m.diff_base);
|
||||
let targeted = {};
|
||||
if (role === 'targeted') {
|
||||
const checklist = path.join(m.checklist_root, 'review/specialists/targeted.md');
|
||||
let brief: string;
|
||||
try { brief = fs.readFileSync(checklist, 'utf8'); } catch { throw new BriefError(`cannot read the targeted brief ${checklist}`); }
|
||||
Object.assign(slots, { checklist, brief, classes: (r.classes ?? []).join(','), diff_base: r.diff_base ?? m.diff_base });
|
||||
targeted = { classes: parseClassList(slots.classes!), rules: TARGETED_RULES, brief_sha: targetedBriefSha(checklist), diff_base: slots.diff_base, patch_id: targetedPatchId(process.cwd(), slots.diff_base!) };
|
||||
}
|
||||
const text = renderBrief(role, slots, t);
|
||||
const file = path.join(briefs, briefFile(role, r.name));
|
||||
fs.writeFileSync(file, text, { mode: 0o600 });
|
||||
const sha = promptHash(text);
|
||||
const bytes = Buffer.byteLength(text);
|
||||
appendEvent(m.ledger, { event: 'rendered', reader: r.reader, role, name: r.name ?? null, file, sha256: sha, bytes, pass: m.pass, stage, ...(findingsSha ? { findings_sha256: findingsSha } : {}) });
|
||||
appendEvent(m.ledger, { event: 'rendered', reader: r.reader, role, name: r.name ?? null, file, sha256: sha, bytes, pass: m.pass, stage, ...(findingsSha ? { findings_sha256: findingsSha } : {}), ...targeted });
|
||||
Object.assign(r, { file, sha256: sha, bytes, chunks: chunkCount(text), state: 'rendered', ...(findingsSha ? { findings_sha256: findingsSha } : {}) });
|
||||
done.push(r);
|
||||
}
|
||||
|
||||
+4
-4
@@ -35,10 +35,10 @@ export interface ScopeFinding {
|
||||
export interface ScopedFinding extends ScopeFinding { locus: Locus; kind: Kind; blocking: boolean; scope: ScopeAction; raised_by?: string }
|
||||
|
||||
/** Who can raise a finding: the core checklist, QA probes, a specialist by name and the review-depth roles; anything else is `unknown`. */
|
||||
const RAISERS: readonly string[] = ['core', 'qa', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', ...SPECIALISTS];
|
||||
const RAISERS: readonly string[] = ['core', 'qa', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', 'targeted', ...SPECIALISTS];
|
||||
const RAISER_ALIASES: Record<string, string> = { adversarial: 'adversarial-native' };
|
||||
/** Reviewers that read after pass 1 finished (Step 11's voices, a fix-check): only a blocking finding of theirs is ever `fix`. */
|
||||
const AFTER_PASS_RAISERS: readonly string[] = ['adversarial-native', 'outside-voice', 'fix-check'];
|
||||
/** Reviewers that read after pass 1 finished (Step 11's voices and the targeted reader, a fix-check): only a blocking finding of theirs is ever `fix`. */
|
||||
const AFTER_PASS_RAISERS: readonly string[] = ['adversarial-native', 'outside-voice', 'fix-check', 'targeted'];
|
||||
|
||||
/** The reviewer that raised a finding, for attributing passes later (GSTA-152 D6): its `raised_by` when known, else its `specialist` tag, else `unknown`. */
|
||||
export function raisedBy(finding: ScopeFinding): string {
|
||||
@@ -297,7 +297,7 @@ export interface ReviewLoopSummary {
|
||||
passes: number; blocking_found: number; deferred: number; noted: number; fixed: number; scope: ReviewScopePolicy | 'unknown';
|
||||
/** One-reviewer re-checks of a fix (docs/review-policy.md#transitions); never passes. */
|
||||
fix_checks?: number;
|
||||
/** Reviewer dispatches of this invocation by role (specialist, red-team, adversarial-native, outside-voice, fix-check). */
|
||||
/** Reviewer dispatches of this invocation by role (specialist, red-team, adversarial-native, outside-voice, fix-check, targeted). */
|
||||
dispatches?: Record<string, number>;
|
||||
/** Prompt plus diff bytes the dispatches read, / 4: a relative estimate, never a bill. */
|
||||
est_input_tokens?: number;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* review-targeted — the targeted review pass on money, gate, auth and secret code
|
||||
* (docs/review-policy.md#targeted-pass-on-money-gate-auth-and-secret-code; GST-257).
|
||||
* A pure function from a collected diff (lib/review-collect.ts: changed paths with
|
||||
* their added and deleted lines), gstack-diff-scope's flags and the gate scan to the
|
||||
* classes the diff touches. bin/gstack-review-plan prints them and dispatches one
|
||||
* `targeted` reader with the matching blocks of review/specialists/targeted.md;
|
||||
* gstack-ship-receipt records the pass and gstack-ship-queue reports it.
|
||||
*
|
||||
* Matching reads added and deleted lines, so removing a cap, a check or a redaction
|
||||
* counts. Path and identifier signals are token-bounded: `costume` never names cost.
|
||||
* Docs and tests paths never trigger. Any input the classifier cannot read fails
|
||||
* closed to all four classes, as a depth error fails the depth closed to full.
|
||||
*
|
||||
* The gate class is the gate scan's tagged hunks (a relaxed threshold, a skipped test, a
|
||||
* loosened check), not GATE_PATH: over the last 100 non-release commits on main, GATE_PATH
|
||||
* alone hit 43 (the precision gate allows 25), and those paths already get full depth.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { classifyPath } from './review-depth';
|
||||
|
||||
export const TARGETED_CLASSES = ['money', 'gate', 'auth', 'secret'] as const;
|
||||
export type TargetedClass = (typeof TARGETED_CLASSES)[number];
|
||||
|
||||
const MONEY_PATH_TOKENS = ['cost', 'costs', 'spend', 'spending', 'budget', 'budgets', 'ledger', 'billing', 'price', 'prices', 'pricing', 'fee', 'fees', 'invoice', 'invoices'];
|
||||
/** Identifiers ending in _usd, Usd or cap_usd, any spend_cap spelling, and paid-provider call sites (the providers lib/pricing.ts prices). */
|
||||
const MONEY_LINE = [
|
||||
/\b[A-Za-z0-9_]*_usd\b/i, /\b[a-z][A-Za-z0-9]*Usd\b/, /\bspend[_-]?cap\b/i,
|
||||
/\bapi\.(?:anthropic|openai|voyageai)\.com\b/, /\bgenerativelanguage\.googleapis\.com\b/, /\bopenrouter\.ai\b/,
|
||||
/\bnew\s+(?:Anthropic|OpenAI|AnthropicBedrock|AnthropicVertex)\s*\(/, /\.(?:messages|chat\.completions|responses|embeddings)\.create\s*\(/,
|
||||
];
|
||||
const AUTH_PATH_TOKENS = ['auth', 'authn', 'authz', 'oauth', 'oauth2', 'jwt', 'session', 'sessions', 'permission', 'permissions', 'role', 'roles'];
|
||||
const AUTH_PATH_PREFIXES = ['authenticat', 'authoriz'];
|
||||
const SECRET_PATH = /(^|\/)(secrets?|credentials?|keychain|token[s]?)([._/-]|$)/i;
|
||||
/** An environment name whose underscore-separated segments include a key word: OPENAI_API_KEY, GH_TOKEN; never MAX_TOKENS. */
|
||||
const KEY_NAME = String.raw`(?:[A-Z0-9]+_)*(?:API_?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|ACCESS_KEY|CREDENTIALS?)(?:_[A-Z0-9]+)*\b`;
|
||||
const SECRET_LINE = [
|
||||
new RegExp(String.raw`\b(?:process\.env|Bun\.env|import\.meta\.env)(?:\.|\[\s*['"\x60])${KEY_NAME}`),
|
||||
new RegExp(String.raw`\b(?:os\.environ(?:\.get\(|\[)|os\.getenv\(|ENV(?:\.fetch\(|\[))\s*['"]${KEY_NAME}`),
|
||||
new RegExp(String.raw`\$\{?${KEY_NAME}`),
|
||||
/\b(?:keychain|keytar|find-generic-password|find-internet-password|secret-tool|git-credential|credential\.helper)\b/i, /\bgh auth token\b/, /(?:^|[\s'"/])\.netrc\b/,
|
||||
];
|
||||
|
||||
/** A hash of the patterns that decide the classes, not of this file's bytes: a comment edit keeps it, a pattern edit changes it. */
|
||||
export const TARGETED_RULES = createHash('sha256').update(JSON.stringify({
|
||||
TARGETED_CLASSES, MONEY_PATH_TOKENS, AUTH_PATH_TOKENS, AUTH_PATH_PREFIXES, KEY_NAME,
|
||||
patterns: Object.entries({ MONEY_LINE, SECRET_LINE, SECRET_PATH: [SECRET_PATH] }).map(([k, v]) => [k, v.map(r => `/${r.source}/${r.flags}`)]),
|
||||
})).digest('hex').slice(0, 16);
|
||||
|
||||
export interface TargetedInput {
|
||||
paths: readonly string[];
|
||||
/** Added and deleted lines per path (lib/review-collect.ts). */
|
||||
added?: Record<string, readonly string[]>;
|
||||
removed?: Record<string, readonly string[]>;
|
||||
/** gstack-diff-scope output (SCOPE_* keys); missing or SCOPE_ERROR fails closed. */
|
||||
scope?: Record<string, string>;
|
||||
/** Gate hunks the gate scan tagged; null when it did not run, which fails closed once a code path changed. */
|
||||
gateTagged: number | null;
|
||||
root?: string | readonly string[];
|
||||
/** Collector failures (a diff git could not list). */
|
||||
errors?: readonly { code: string; input: string }[];
|
||||
}
|
||||
export interface TargetedResult { classes: TargetedClass[]; reasons: string[]; errors: string[]; rules: string }
|
||||
|
||||
const tokens = (p: string) => p.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
||||
|
||||
/** The classes a diff touches, in TARGETED_CLASSES order, with one reason per class; any input error returns all four. */
|
||||
export function targetedClasses(i: TargetedInput): TargetedResult {
|
||||
const errors = (i.errors ?? []).map(e => `${e.code} (${e.input})`);
|
||||
const s = i.scope;
|
||||
if (!s) errors.push('scope unavailable (gstack-diff-scope did not run)');
|
||||
else if (s.SCOPE_ERROR) errors.push(`scope error: ${s.SCOPE_ERROR}`);
|
||||
const code = i.paths.filter(p => classifyPath(p, i.root) === 'code');
|
||||
if (code.length && i.gateTagged === null) errors.push('gate scan unavailable');
|
||||
if (errors.length) return { classes: [...TARGETED_CLASSES], reasons: errors.map(e => `fail-closed: ${e}`), errors, rules: TARGETED_RULES };
|
||||
const why = new Map<TargetedClass, string>();
|
||||
const hit = (c: TargetedClass, reason: string) => { if (!why.has(c)) why.set(c, reason); };
|
||||
if ((i.gateTagged ?? 0) > 0) hit('gate', `gate scan tagged ${i.gateTagged} hunk(s)`);
|
||||
for (const p of code) {
|
||||
const t = tokens(p);
|
||||
if (s!.SCOPE_AUTH === 'true' && t.some(x => AUTH_PATH_TOKENS.includes(x) || AUTH_PATH_PREFIXES.some(a => x.startsWith(a)))) hit('auth', `auth path: ${p}`);
|
||||
if (SECRET_PATH.test(p)) hit('secret', `secret path: ${p}`);
|
||||
if (t.some(x => MONEY_PATH_TOKENS.includes(x))) hit('money', `money path: ${p}`);
|
||||
for (const line of [...(i.added?.[p] ?? []), ...(i.removed?.[p] ?? [])]) {
|
||||
if (!why.has('money') && MONEY_LINE.some(r => r.test(line))) hit('money', `money line: ${p}`);
|
||||
if (!why.has('secret') && SECRET_LINE.some(r => r.test(line))) hit('secret', `secret line: ${p}`);
|
||||
}
|
||||
}
|
||||
const classes = TARGETED_CLASSES.filter(c => why.has(c));
|
||||
return { classes, reasons: classes.map(c => why.get(c)!), errors: [], rules: TARGETED_RULES };
|
||||
}
|
||||
|
||||
/** The static brief every targeted reader gets its blocks from; its sha256 is the `brief` the plan prints and the receipt records. */
|
||||
export const TARGETED_BRIEF_FILE = path.join(import.meta.dir, '..', 'review', 'specialists', 'targeted.md');
|
||||
export function targetedBriefSha(file = TARGETED_BRIEF_FILE): string {
|
||||
try { return createHash('sha256').update(fs.readFileSync(file)).digest('hex'); } catch { return 'missing'; }
|
||||
}
|
||||
|
||||
/**
|
||||
* The patch the targeted reader reads: the patch-id of the whole committed diff from `mergeBase`, tests included, so a
|
||||
* test-only fix that adds a gate hunk changes it. `uncommitted` with tracked edits or untracked files, `none` for an empty
|
||||
* or unreadable diff; neither proves what a reader read. Not the receipt's product_patch_id (that leaves tests out).
|
||||
*/
|
||||
export function targetedPatchId(root: string, mergeBase: string): string {
|
||||
const git = (args: string[], input?: string) => spawnSync('git', args, { cwd: root, input, encoding: 'utf8', timeout: 60_000, maxBuffer: 256 * 1024 * 1024 });
|
||||
if (git(['diff', '--quiet', 'HEAD', '--']).status !== 0 || git(['ls-files', '--others', '--exclude-standard']).stdout?.trim()) return 'uncommitted';
|
||||
const diff = git(['diff', '--no-color', '--no-ext-diff', mergeBase, 'HEAD']);
|
||||
if (diff.status !== 0 || !diff.stdout.trim()) return 'none';
|
||||
return git(['patch-id', '--verbatim'], diff.stdout).stdout?.trim().split(/\s+/)[0] || 'none';
|
||||
}
|
||||
|
||||
/** A class list as printed and recorded: comma-joined, or `none`. */
|
||||
export const classList = (classes: readonly string[]) => (classes.length ? classes.join(',') : 'none');
|
||||
|
||||
/** Parse a printed class list (`money,gate` or `none`); an unknown name gives null. */
|
||||
export function parseClassList(text: string): TargetedClass[] | null {
|
||||
const names = text.trim() === 'none' ? [] : text.split(',').map(s => s.trim()).filter(Boolean);
|
||||
if (names.some(n => !(TARGETED_CLASSES as readonly string[]).includes(n))) return null;
|
||||
return TARGETED_CLASSES.filter(c => names.includes(c));
|
||||
}
|
||||
|
||||
/** The brief a targeted reader gets: review/specialists/targeted.md's preamble plus the `## <class>` block of each class that hit. */
|
||||
export function targetedBrief(text: string, classes: readonly TargetedClass[]): string {
|
||||
const parts = text.replace(/\r\n/g, '\n').split(/\n(?=## )/);
|
||||
const blocks = new Map(parts.slice(1).map(b => [/^## (\w+)/.exec(b)?.[1] ?? '', b.trimEnd()]));
|
||||
const missing = classes.filter(c => !blocks.has(c));
|
||||
if (!classes.length || missing.length) throw new Error(`targeted brief: ${classes.length ? `no block for ${missing.join(', ')}` : 'no class to brief'}`);
|
||||
return [parts[0]!.trimEnd(), ...classes.map(c => blocks.get(c)!)].join('\n\n');
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ import { SMALL_DIFF_LINES, type Role } from './review-depth';
|
||||
|
||||
export const SHIP_STEPS = ['0.9', '1', '2', '3', '4', '5', '6', '6.5', '7', '8', '9', '10', '11', '11.5', '12', '13', '14', '14.5', '15', '16', '17', '18', '19', '20', '21', '22'] as const;
|
||||
/** Reviewer roles plus `docs`, the Step 14.5 documentation audit: each is one reader priced and reconciled the same way. */
|
||||
export const DISPATCH_ROLES = ['specialist', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', 'docs'] as const satisfies readonly (Role | 'docs')[];
|
||||
export const DISPATCH_ROLES = ['specialist', 'red-team', 'adversarial-native', 'outside-voice', 'fix-check', 'targeted', 'docs'] as const satisfies readonly (Role | 'docs')[];
|
||||
type MissingRole = Exclude<Role, typeof DISPATCH_ROLES[number]>;
|
||||
const ROLES_COMPLETE: [MissingRole] extends [never] ? true : never = true;
|
||||
void ROLES_COMPLETE;
|
||||
|
||||
@@ -365,6 +365,7 @@ function admitSlots(env: TrainEnv, pins: Array<{ pr: string; sha: string }>, dry
|
||||
}
|
||||
const shipped = bindShippedReceipt(ctx.repoRoot, `refs/remotes/${env.remote}/${ctx.base}`, info.body, p.pr, [info.headRefOid]);
|
||||
if (!shipped.ok) return outRefused({ code: shipped.code, message: shipped.message }, [shipped.line, ...(shipped.depthLine ? [shipped.depthLine] : [])], `#${p.pr}'s lane runs /ship, then ${Q} ready --pr ${p.pr} --screen|--no-screen; then ${Q} train build --prs <pins>`, 'none');
|
||||
lines.push(`${shipped.targetedLine} pr=${p.pr}`);
|
||||
const ready = loadReady(env.repo.keys, p.pr, info.headRefOid);
|
||||
if (!ready) return outRefused({ code: 'QUEUE_NOT_READY', message: `no READY receipt for #${p.pr} at ${info.headRefOid.slice(0, 12)} (local state or ${'refs/notes/gstack-trains'})` }, [], `the lane runs ${Q} ready --pr ${p.pr} --screen|--no-screen`, 'none');
|
||||
let screen = ready.screen;
|
||||
|
||||
@@ -271,6 +271,7 @@ export function ready(ctx: QueueContext, o: { pr: string; pregate?: string; gate
|
||||
gateLine(policy, attempt),
|
||||
receipt.line,
|
||||
...(receipt.depthLine ? [receipt.depthLine] : []),
|
||||
...(receipt.targetedLine ? [receipt.targetedLine] : []),
|
||||
];
|
||||
const gate = policy.policy.full_e2e ? gateEvidence(ctx, o.pr, head, attempt, o.gateExit, o.gateLog) : null;
|
||||
if (gate) lines.push(gate.line);
|
||||
|
||||
+57
-13
@@ -11,7 +11,10 @@
|
||||
* `review_loop_exception`: the owner's acceptance of a stop after a
|
||||
* fix-check edit under frozen scope (#3559), shaped like the docs exception.
|
||||
* The queue recomputes the depth the head needs from git objects
|
||||
* (lib/review-collect.ts) and refuses a receipt below it. Kept beside
|
||||
* (lib/review-collect.ts) and refuses a receipt below it.
|
||||
* `review_targeted`: the targeted pass on money, gate, auth and secret code
|
||||
* (lib/review-targeted.ts), which the queue reports beside the depth and does
|
||||
* not refuse on yet (GST-257 PR 1). Kept beside
|
||||
* lib/ship-receipt.ts so that module stays under the size ratchet.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
@@ -19,6 +22,7 @@ import * as path from 'node:path';
|
||||
import type { ShipReceipt } from './headless-artifacts';
|
||||
import { collectCommit } from './review-collect';
|
||||
import { depthOfCommitInput, gateScanNeeded, type Depth } from './review-depth';
|
||||
import { TARGETED_CLASSES, classList, targetedClasses, type TargetedClass } from './review-targeted';
|
||||
import { runScriptSync } from './script-invocation';
|
||||
import { bindReceipt, type ReceiptBindingCode } from './ship-receipt';
|
||||
import { docsExceptionLine, parseReceiptDocs } from './ship-receipt-docs';
|
||||
@@ -122,8 +126,46 @@ export function exceptionProblems(body: string, r: ShipReceipt, isAncestor?: (co
|
||||
return out;
|
||||
}
|
||||
|
||||
export type ReviewTargeted = NonNullable<ShipReceipt['review_targeted']>;
|
||||
|
||||
/** `write --review-targeted` input (gstack-review-dispatch verify --targeted-out), bound to the receipt's product patch-id here. */
|
||||
export function parseReviewTargeted(value: unknown, productPatchId: string): { ok: true; targeted: ReviewTargeted } | { ok: false; message: string } {
|
||||
const bad = (message: string) => ({ ok: false as const, message });
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return bad('review_targeted must be { classes, rules, brief_sha, status, findings, raised_by }');
|
||||
const v = value as Record<string, unknown>;
|
||||
for (const key of Object.keys(v)) if (!['classes', 'rules', 'brief_sha', 'status', 'findings', 'raised_by'].includes(key)) return bad(`review_targeted: unknown key ${JSON.stringify(key)}${key === 'product_patch_id' ? ' (write binds the pass to the receipt\'s product_patch_id)' : ''}`);
|
||||
if (!Array.isArray(v.classes) || !v.classes.every(c => (TARGETED_CLASSES as readonly unknown[]).includes(c))) return bad(`review_targeted.classes must list ${TARGETED_CLASSES.join(', ')}`);
|
||||
if (!text(v.rules) || !text(v.raised_by)) return bad('review_targeted.rules and raised_by must be non-empty one-line strings');
|
||||
if (typeof v.brief_sha !== 'string' || !/^[0-9a-f]{64}$/.test(v.brief_sha)) return bad('review_targeted.brief_sha must be the sha256 of review/specialists/targeted.md');
|
||||
if (!['done', 'failed', 'not-run'].includes(v.status as string)) return bad('review_targeted.status must be done, failed or not-run');
|
||||
if (typeof v.findings !== 'number' || !Number.isInteger(v.findings) || v.findings < 0) return bad('review_targeted.findings must be an integer >= 0');
|
||||
const classes = TARGETED_CLASSES.filter(c => (v.classes as string[]).includes(c));
|
||||
return { ok: true, targeted: { classes, rules: v.rules as string, brief_sha: v.brief_sha, product_patch_id: productPatchId, status: v.status as ReviewTargeted['status'], findings: v.findings, raised_by: (v.raised_by as string).trim() } };
|
||||
}
|
||||
|
||||
/** The queue's report-only line: the classes the head needs against the classes a done targeted pass covered; never a refusal (GST-257 PR 1). */
|
||||
export function receiptTargetedLine(required: readonly TargetedClass[], r: ShipReceipt): string {
|
||||
const rt = r.review_targeted;
|
||||
const covered: readonly string[] = rt?.status === 'done' ? rt.classes : [];
|
||||
const ok = required.every(c => covered.includes(c));
|
||||
return `REVIEW_TARGETED: required=${classList(required)} receipt=${rt ? classList(covered) : 'legacy'} verdict=${ok ? 'ok' : 'missing'}`;
|
||||
}
|
||||
|
||||
export interface DepthVerdict { ok: boolean; line: string; message?: string }
|
||||
|
||||
/** The head's committed range against the base, read once for the depth verdict and the targeted line. */
|
||||
interface HeadInputs { baseSha: { ok: boolean; out: string }; c: ReturnType<typeof collectCommit>; scope: Record<string, string> }
|
||||
function headInputs(root: string, baseRef: string, head: string): HeadInputs {
|
||||
const baseSha = git(root, ['rev-parse', '--verify', '--quiet', `${baseRef}^{commit}`]);
|
||||
const baseRev = baseSha.ok ? baseSha.out : baseRef;
|
||||
return { baseSha, c: collectCommit(root, baseRev, head), scope: scopeOfRange(root, BIN_DIR, baseRev, head) };
|
||||
}
|
||||
/** The receipt's recorded gate scan when it names this head; otherwise null (unavailable). */
|
||||
const boundScan = (r: ShipReceipt, head: string): number | null => {
|
||||
const g = r.review_depth?.gate_scan;
|
||||
return g && g.tagged !== null && /^[0-9a-f]{40,64}$/.test(g.head) && g.head === head.toLowerCase() ? g.tagged : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The depth the bound head needs against `baseRef`, recomputed from git
|
||||
* objects; the recorded gate scan counts only when it names the receipt's
|
||||
@@ -131,14 +173,13 @@ export interface DepthVerdict { ok: boolean; line: string; message?: string }
|
||||
* with no review_depth and no gstack_version predates depth receipts and is
|
||||
* accepted as legacy; one from a writer that records depth but lacks it is not.
|
||||
*/
|
||||
export function receiptDepthVerdict(root: string, baseRef: string, r: ShipReceipt, head: string): DepthVerdict {
|
||||
export function receiptDepthVerdict(root: string, baseRef: string, r: ShipReceipt, head: string, at?: HeadInputs): DepthVerdict {
|
||||
const rd = r.review_depth;
|
||||
if (!rd && !r.gstack_version) return { ok: true, line: 'REVIEW_DEPTH: legacy receipt (written before receipts recorded review_depth); accepted' };
|
||||
const baseSha = git(root, ['rev-parse', '--verify', '--quiet', `${baseRef}^{commit}`]);
|
||||
const baseRev = baseSha.ok ? baseSha.out : baseRef;
|
||||
const c = collectCommit(root, baseRev, head);
|
||||
const scanBound = !!rd && rd.gate_scan.tagged !== null && /^[0-9a-f]{40,64}$/.test(rd.gate_scan.head) && rd.gate_scan.head === head.toLowerCase();
|
||||
const need = depthOfCommitInput({ ...c, scope: scopeOfRange(root, BIN_DIR, baseRev, head), gateTagged: scanBound ? rd!.gate_scan.tagged : null });
|
||||
const { baseSha, c, scope } = at ?? headInputs(root, baseRef, head);
|
||||
const tagged = boundScan(r, head);
|
||||
const scanBound = tagged !== null;
|
||||
const need = depthOfCommitInput({ ...c, scope, gateTagged: tagged });
|
||||
const have = rd?.depth;
|
||||
const inputs = `base@${short(baseSha.ok ? baseSha.out : null)},registry@${short(c.registry.sha)},gate_scan@${scanBound ? short(rd!.gate_scan.head) : 'missing'}`;
|
||||
const line = `REVIEW_DEPTH: receipt=${have ?? 'none'} required=${need.depth} runtime_lines=${need.runtimeLines} class=${need.class} reasons=${need.reasons.join(',')} inputs=${inputs}`;
|
||||
@@ -148,17 +189,20 @@ export function receiptDepthVerdict(root: string, baseRef: string, r: ShipReceip
|
||||
return { ok: false, line, message: `the receipt ${why}, but head ${head.slice(0, 12)} needs ${need.depth} (${need.reasons.join(', ')}${errs ? `; ${errs}` : ''}); the receipt is unchanged; run /ship on ${head.slice(0, 12)}` };
|
||||
}
|
||||
|
||||
/** bindReceipt plus the owner exceptions and the review depth: what READY and train build accept. */
|
||||
/** bindReceipt plus the owner exceptions and the review depth: what READY and train build accept; the targeted line is reported, never refused on. */
|
||||
export function bindShippedReceipt(root: string, baseRef: string, body: string, pr: string | undefined, accepted: readonly string[]):
|
||||
| { ok: true; receipt: ShipReceipt; head: string; line: string; depthLine: string }
|
||||
| { ok: false; code: ReceiptBindingCode | 'QUEUE_RECEIPT_DEPTH_RAISED' | 'QUEUE_RECEIPT_EXCEPTION_INVALID'; message: string; line: string; depthLine?: string } {
|
||||
| { ok: true; receipt: ShipReceipt; head: string; line: string; depthLine: string; targetedLine: string }
|
||||
| { ok: false; code: ReceiptBindingCode | 'QUEUE_RECEIPT_DEPTH_RAISED' | 'QUEUE_RECEIPT_EXCEPTION_INVALID'; message: string; line: string; depthLine?: string; targetedLine?: string } {
|
||||
const b = bindReceipt(body, pr, accepted);
|
||||
if (!b.ok) return b;
|
||||
const ancestor = (commit: string) => b.receipt.carry?.ancestry === 'rewritten' || git(root, ['merge-base', '--is-ancestor', commit, b.head]).ok;
|
||||
const problems = exceptionProblems(body, b.receipt, ancestor);
|
||||
if (problems.length) return { ok: false, code: 'QUEUE_RECEIPT_EXCEPTION_INVALID', message: `the receipt's owner exception does not stand: ${problems.join('; ')}`, line: b.line };
|
||||
const d = receiptDepthVerdict(root, baseRef, b.receipt, b.head);
|
||||
if (!d.ok) return { ok: false, code: 'QUEUE_RECEIPT_DEPTH_RAISED', message: d.message!, line: b.line, depthLine: d.line };
|
||||
return { ...b, depthLine: d.line };
|
||||
const at = headInputs(root, baseRef, b.head);
|
||||
const d = receiptDepthVerdict(root, baseRef, b.receipt, b.head, at);
|
||||
const gateTagged = boundScan(b.receipt, b.head) ?? (gateScanNeeded(at.c.paths, at.c.root) ? null : 0);
|
||||
const targetedLine = receiptTargetedLine(targetedClasses({ ...at.c, scope: at.scope, gateTagged }).classes, b.receipt);
|
||||
if (!d.ok) return { ok: false, code: 'QUEUE_RECEIPT_DEPTH_RAISED', message: d.message!, line: b.line, depthLine: d.line, targetedLine };
|
||||
return { ...b, depthLine: d.line, targetedLine };
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ const KEY_ORDER: Array<keyof ShipReceipt | 'docs'> = [
|
||||
'schema_version', 'pr', 'head', 'version', 'base', 'gated_tree', 'predecessor', 'tree', 'gate_reuse', 'gate', 'ci', 'spend_usd',
|
||||
'session_kind', 'artifacts_consumed', 'run', 'thread', 'policy', 'queue_mode', 'preregistration', 'history',
|
||||
'head_tree', 'product_patch_id', 'gstack_version', 'review_depth', 'pregate', 'requires_remote', 'issues', 'approval', 'docs', 'review_loop',
|
||||
'review_loop_exception', 'carry',
|
||||
'review_loop_exception', 'review_targeted', 'carry',
|
||||
];
|
||||
|
||||
/** Keys in the fixed order so identifiers lead; undefined values are dropped. */
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.357",
|
||||
"version": "1.91.358",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Targeted Review Brief: money, gate, auth and secret code
|
||||
|
||||
Scope: every diff whose `gstack-review-plan` TARGETED line names a class, at every depth; the reader gets the preamble and the block of each class that hit.
|
||||
Output: JSON objects, one finding per line. Schema:
|
||||
{"severity":"CRITICAL|INFORMATIONAL","confidence":N,"path":"file","line":N,"category":"money|gate|auth|secret","summary":"...","fix":"...","fingerprint":"path:line:category","specialist":"targeted"}
|
||||
Optional: line, fix, fingerprint, evidence, test_stub, cause.
|
||||
If no findings: output `NO FINDINGS` and nothing else. `NO FINDINGS` is a complete result, not a failure to look.
|
||||
|
||||
---
|
||||
|
||||
Read-only: read the diff and the code it reaches, and edit, commit, post or run nothing that writes.
|
||||
Read deleted lines as closely as added ones: a removed cap, check or redaction is the change under review.
|
||||
Follow each changed value to where it is enforced: a cap, check or redaction that exists but is never reached is missing.
|
||||
|
||||
CRITICAL is the blocking floor: a finding that would fail a test, break production, lose data or secrets, or open a security hole (injection, auth bypass, secret exposure). Everything else is INFORMATIONAL.
|
||||
|
||||
## money
|
||||
The checklist's Forced-CRITICAL line covers this block (review/checklist.md): uncapped spend, "a paid call, retry or loop with no cap", is "CRITICAL on sight", and no lane, specialist, dedup step or saved decision downgrades it.
|
||||
- A paid call (a model API, a cloud VM, a paid search) reachable without a cap, or a retry, loop or fan-out around one with no bound on count or spend.
|
||||
- A cap read but not enforced: checked after the spend, compared in the wrong unit (cents and dollars, tokens and USD), or a missing value read as unlimited.
|
||||
- A cap check removed, loosened, or moved behind a flag that defaults to off.
|
||||
- Spend recorded wrong: an estimate stored as actual cost, a failed call recorded as zero, a double count, or a ledger write that can be skipped.
|
||||
|
||||
## gate
|
||||
- A check that fails open: an error, timeout, missing input or unreadable file read as a pass, or an empty result read as green.
|
||||
- A threshold, required check, allowlist or rule loosened, skipped or made optional; a test skipped or deleted together with the guard it covered.
|
||||
- A verdict bound to the wrong subject: another commit, tree, branch, PR or run than the one being merged, or a stale result reused.
|
||||
- A rule hash, pin or ratchet that detects such an edit changed in the same diff without the evidence it asks for.
|
||||
|
||||
## auth
|
||||
- A bypass: a route, command or path reachable without the identity check its siblings run, or a default that allows.
|
||||
- A permission checked on the wrong subject: the caller instead of the owner, a role read from input, or a check made before the object is resolved.
|
||||
- A session or token accepted after expiry, revocation or a role change, or a callback that skips its single-use state value.
|
||||
|
||||
## secret
|
||||
- A secret written where others can read it: logs, error messages, PR bodies, comments, artifacts, URLs, readable files, or a child process's arguments.
|
||||
- A redaction, scrub, mask or file mode removed or bypassed, or a new output path that skips the existing redactor.
|
||||
- A key read from a broader source than before, a fallback to a hardcoded value, or a real credential committed in code or fixtures.
|
||||
@@ -4,7 +4,8 @@
|
||||
`DISPATCH: adversarial-native` and the outside voice only on
|
||||
`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. Under a token cap,
|
||||
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 <TARGETED classes> --base <base> --diff-base <merge 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:<roles>`)
|
||||
and add `--reservation <id>` to the `--start adversarial-review` call below.
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
`DISPATCH: adversarial-native` and the outside voice only on
|
||||
`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. Under a token cap,
|
||||
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 <TARGETED classes> --base <base> --diff-base <merge 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:<roles>`)
|
||||
and add `--reservation <id>` to the `--start adversarial-review` call below.
|
||||
|
||||
|
||||
@@ -55,11 +55,12 @@ const NO_LOSS_REPLAYS = {
|
||||
'PR-a': "GSTA-167 variant G Arm 4: structural plan identity at the 14 cohort heads and the free exact re-score of saved C, D and A' findings (parts 4a and 4b)",
|
||||
'G1': "GSTA-167 variant G Arm G1: live no-loss replay of both prose voices with part 1's prompts, 2 passes, normalizer v2, paired rule against pooled C' (parts 1 and 3; specialists at medium as in C and C2)",
|
||||
'L': "GSTA-167 variant G Arm L: live no-loss replay of one batched reader (testing and security plus scoped checklists) and the Red Team on its findings, 2 passes, scored L∘G1 against G1 on the paired rule (part 2)",
|
||||
'GST-257': "GST-257 PR 1 dispatch-superset no-loss check (test/review-targeted.test.ts): on every fixture diff the branch dispatches main's reviewers at 8be7f21 unchanged plus only the targeted reader, and over the last 100 non-release commits on main it lost none; adds the targeted role and brief to the G1 and L bytes, whose existing reviewer text is unchanged",
|
||||
} as const;
|
||||
type Replay = keyof typeof NO_LOSS_REPLAYS;
|
||||
const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
|
||||
'ship/sections/review-army.md': { sha: 'b7ca291a1cc63750bfb9710a4c12fd971523b9598da2a32ef80fb258eb0eceab', replay: 'L' },
|
||||
'ship/sections/adversarial.md': { sha: 'c48aa7717c7438941fa23418367e19a62e07edda7445d0d5a55924e172dd5d33', replay: 'G1' },
|
||||
'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' },
|
||||
@@ -74,8 +75,9 @@ const REVIEWER_BRIEFS: Record<string, { sha: string; replay: Replay }> = {
|
||||
'review/specialists/simplification.md': { sha: 'f33846b1dcf2fdeef82d0744ab2ef48fb4a33b34a737dafc33dd0e891756689f', replay: '#3638' },
|
||||
'review/specialists/state-machines.md': { sha: '597215e4438d85d9cc71b48097c61eef7cba4f4b09dc14c92b4b94893b3d3ea2', replay: '#3638' },
|
||||
'review/specialists/testing.md': { sha: 'd6cc2bef7be562387d0c0a22fefe646dff9c295f1d34d8453a6cb496584150e4', replay: '#3638' },
|
||||
'review/specialists/targeted.md': { sha: '7f92a0d0a8319c3c0e076fb3b1a4689c45120b2be2c14dfc7d4cb03aafa9d6a5', replay: 'GST-257' },
|
||||
'scripts/resolvers/fix-check.ts': { sha: '712bf5b0c439e96bac32f19cbe25394dabb4c94aa66600100bfde9b100e15a08', replay: '#3638' },
|
||||
'lib/review-depth.ts': { sha: 'd4715a5f534d1470c63da879a3d66bb090a9b4929065d1f06c30acd97ef77bb0', replay: 'L' },
|
||||
'lib/review-depth.ts': { sha: '4542785a1e217d8dd054985978b42c055b9fd79082579cb7664f187f7066e48e', replay: 'GST-257' },
|
||||
};
|
||||
|
||||
describe('reviewer briefs stay at their no-loss-tested bytes (GSTA-167)', () => {
|
||||
|
||||
@@ -181,6 +181,41 @@ async function runPass(h: ReturnType<typeof host>, input: Record<string, unknown
|
||||
}
|
||||
const bin = (dir: string, env: NodeJS.ProcessEnv, a: string[]) => spawnSync(path.join(BIN, 'gstack-review-dispatch'), a, { cwd: dir, env, encoding: 'utf8', timeout: 60_000 });
|
||||
|
||||
describe('the targeted reader in a pass (GST-257 on main)', () => {
|
||||
// Value: protects=a pass whose plan dispatches the targeted reader stages it with Step 11's voices on the merge base, its brief byte for byte the render CLI's, and its ledger row carrying the classes, rules, brief and patch the receipt's review_targeted record reads; fails_when=the manifest refuses or drops the targeted role, renders it on another base, or writes a rendered row verify --targeted-out cannot use;
|
||||
// why_new=#3671 added the targeted reader after PR 2's manifest; seam=none; no_claim=the reader's findings
|
||||
test('a secret-path diff stages the targeted reader with the voices, identical to the render CLI, with its targeted ledger fields', () => {
|
||||
const { dir, env } = repo();
|
||||
const sh = (c: string) => { const r = spawnSync('bash', ['-c', c], { cwd: dir, env, encoding: 'utf8', timeout: 60_000 }); if (r.status !== 0) throw new Error(r.stderr); return r.stdout; };
|
||||
sh("echo 'export const apiKeyName = \"X\";' > src/secrets.ts && git add -A && git commit -qm secret");
|
||||
const out = path.join(dir, '.state/run/p1');
|
||||
const p = bin(dir, env, ['pass', '--base', 'main', '--out', out, '--pass', 't1p1', '--', '--depth-floor', 'full']);
|
||||
expect(p.status, p.stdout + p.stderr).toBe(0);
|
||||
expect(p.stdout).toMatch(/^DISPATCH: targeted /m);
|
||||
const mf = path.join(out, 'pass-manifest.json');
|
||||
let m = JSON.parse(fs.readFileSync(mf, 'utf8')) as Manifest;
|
||||
const t = m.readers.find(r => r.role === 'targeted')!;
|
||||
expect(t).toMatchObject({ reader: 'targeted', stage: 'voices', classes: ['secret'] });
|
||||
const mergeBase = sh('git merge-base main HEAD').trim();
|
||||
expect(t.diff_base).toBe(mergeBase);
|
||||
for (const st of ['specialists', 'red-team']) {
|
||||
if (st === 'red-team') expect(bin(dir, env, ['stage', '--manifest', mf, '--stage', st]).status).toBe(0);
|
||||
m = JSON.parse(fs.readFileSync(mf, 'utf8')) as Manifest;
|
||||
for (const r of m.readers.filter(x => x.stage === st)) for (const [s, extra] of [['intent', []], ['created', ['--path', '1']], ['collected', []]] as const) expect(bin(dir, env, ['mark', '--manifest', mf, '--reader', r.reader, '--state', s, ...extra]).status).toBe(0);
|
||||
}
|
||||
const v = bin(dir, env, ['stage', '--manifest', mf, '--stage', 'voices']);
|
||||
expect(v.status, v.stdout + v.stderr).toBe(0);
|
||||
m = JSON.parse(fs.readFileSync(mf, 'utf8')) as Manifest;
|
||||
const staged = m.readers.find(r => r.role === 'targeted')!;
|
||||
const cli = bin(dir, env, ['render', '--role', 'targeted', '--classes', 'secret', '--base', 'main', '--diff-base', mergeBase, '--out', path.join(dir, '.state/cli'), '--stack', m.stack, '--test-fw', m.test_fw, ...(staged.exclude ? ['--exclude', staged.exclude] : [])]);
|
||||
expect(cli.status, cli.stdout + cli.stderr).toBe(0);
|
||||
expect(cli.stdout).toContain('sha256=' + staged.sha256);
|
||||
const row = fs.readFileSync(m.ledger, 'utf8').trim().split('\n').map(l => JSON.parse(l)).find(e => e.event === 'rendered' && e.reader === 'targeted');
|
||||
const cliRow = fs.readFileSync(path.join(dir, '.state/cli/review-ledger.jsonl'), 'utf8').trim().split('\n').map(l => JSON.parse(l)).at(-1);
|
||||
for (const k of ['classes', 'rules', 'brief_sha', 'diff_base', 'patch_id', 'sha256']) expect(row[k]).toEqual(cliRow[k]);
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
describe('Capy pass program (fake host, real bins)', () => {
|
||||
test('gstack-review-dispatch program serves the checked-in file verbatim with its sha', () => {
|
||||
const r = spawnSync(path.join(BIN, 'gstack-review-dispatch'), ['program'], { encoding: 'utf8', timeout: 30_000 });
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* GST-257 PR 1: the targeted review pass on money, gate, auth and secret code
|
||||
* (lib/review-targeted.ts, docs/review-policy.md#targeted-pass-on-money-gate-auth-and-secret-code).
|
||||
* The classifier, the plan's TARGETED and DISPATCH lines, the dispatch-superset no-loss check
|
||||
* against main's reviewers, the production path from render to the receipt and the review-loop
|
||||
* summary, and the queue's report-only REVIEW_TARGETED line.
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { classifyDiff, planReview, scanNumbers, type PlanInput } from '../lib/review-depth';
|
||||
import { raisedBy, summarizeReviewLoop } from '../lib/review-scope';
|
||||
import { TARGETED_RULES, targetedBrief, targetedBriefSha, targetedClasses, type TargetedClass, type TargetedInput } from '../lib/review-targeted';
|
||||
import { carryReceipt, extractReceipt, renderReceiptBlock } from '../lib/ship-receipt';
|
||||
import { bindShippedReceipt, parseReviewTargeted, receiptTargetedLine } from '../lib/ship-receipt-depth';
|
||||
import type { ShipReceipt } from '../lib/headless-artifacts';
|
||||
import { cleanup, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/restamp-fixture';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const PLAN = path.join(ROOT, 'bin', 'gstack-review-plan');
|
||||
const dirs: string[] = [];
|
||||
const repos: FixtureRepo[] = [];
|
||||
afterEach(() => { while (dirs.length) fs.rmSync(dirs.pop()!, { recursive: true, force: true }); while (repos.length) cleanup(repos.pop()!); });
|
||||
|
||||
const input = (paths: string[], o: Partial<TargetedInput> = {}): TargetedInput => ({ paths, added: {}, removed: {}, scope: { SCOPE_AUTH: 'false' }, gateTagged: 0, ...o });
|
||||
const classesOf = (i: TargetedInput) => targetedClasses(i).classes;
|
||||
|
||||
describe('targetedClasses: the four classes from added and deleted lines', () => {
|
||||
// Value: protects=a diff that adds or removes money, auth or secret handling gets the targeted reader; fails_when=a class reads only
|
||||
// added lines, so a removed cap or redaction slips through, or a class fires on an unrelated diff; seam=lib/review-targeted.ts
|
||||
test('each class has a positive and a negative case, and deleted lines count', () => {
|
||||
expect(classesOf(input(['src/run.ts'], { added: { 'src/run.ts': ['const spend_cap_usd = 5;'] } }))).toEqual(['money']);
|
||||
expect(classesOf(input(['src/run.ts'], { removed: { 'src/run.ts': [' if (spent > capUsd) throw new Error("over cap");'] } }))).toEqual(['money']);
|
||||
expect(classesOf(input(['src/run.ts'], { added: { 'src/run.ts': ['const MAX_SPEND_USD = 10;'] } }))).toEqual(['money']);
|
||||
expect(classesOf(input(['src/evals.ts'], { added: { 'src/evals.ts': ['const client = new Anthropic();'] } }))).toEqual(['money']);
|
||||
expect(classesOf(input(['src/billing/invoice.ts']))).toEqual(['money']);
|
||||
expect(classesOf(input(['src/run.ts'], { added: { 'src/run.ts': ['const total = a + b;'] } }))).toEqual([]);
|
||||
expect(classesOf(input(['src/a.ts'], { gateTagged: 2 }))).toEqual(['gate']);
|
||||
expect(classesOf(input(['lib/pregate-check.ts']))).toEqual([]);
|
||||
expect(classesOf(input(['src/auth/session.ts'], { scope: { SCOPE_AUTH: 'true' } }))).toEqual(['auth']);
|
||||
expect(classesOf(input(['src/auth/session.ts']))).toEqual([]);
|
||||
expect(classesOf(input(['lib/authentication.ts', 'src/authorizer/index.ts'], { scope: { SCOPE_AUTH: 'true' } }))).toEqual(['auth']);
|
||||
expect(classesOf(input(['src/client.ts'], { added: { 'src/client.ts': ['const key = process.env.OPENAI_API_KEY;'] } }))).toEqual(['secret']);
|
||||
expect(classesOf(input(['src/log.ts'], { removed: { 'src/log.ts': [' console.log(s.replaceAll(process.env.ANTHROPIC_API_KEY ?? "", "***"));'] } }))).toEqual(['secret']);
|
||||
expect(classesOf(input(['bin/run.sh'], { added: { 'bin/run.sh': ['curl -H "Authorization: $GH_TOKEN" "$URL"'] } }))).toEqual(['secret']);
|
||||
expect(classesOf(input(['lib/credentials.ts']))).toEqual(['secret']);
|
||||
expect(classesOf(input(['src/limits.ts'], { added: { 'src/limits.ts': ['const n = process.env.MAX_TOKENS;'] } }))).toEqual([]);
|
||||
});
|
||||
|
||||
test('matching is token-bounded: costume, feeder, pricey and author name no class', () => {
|
||||
expect(classesOf(input(['src/costume.ts'], { added: { 'src/costume.ts': ['export const costume = 1;', 'export const feeder = 2;', 'export const pricey = 3;', 'const usdRate = 4;'] } }))).toEqual([]);
|
||||
expect(classesOf(input(['src/author.ts', 'src/feeder/list.ts'], { scope: { SCOPE_AUTH: 'true' } }))).toEqual([]);
|
||||
expect(classesOf(input(['src/spend_usd_total.ts'], { added: { 'src/spend_usd_total.ts': ['const spend_usd_total = 1;'] } }))).toEqual(['money']);
|
||||
});
|
||||
|
||||
test('docs and tests paths never trigger, whatever their lines say', () => {
|
||||
const lines = ['const spend_cap_usd = 5;', 'const k = process.env.OPENAI_API_KEY;', 'new Anthropic()'];
|
||||
const paths = ['docs/billing.md', 'test/billing.test.ts', 'test/fixtures/secrets/key.ts', 'CHANGELOG.md'];
|
||||
expect(classesOf(input(paths, { scope: { SCOPE_AUTH: 'true' }, added: Object.fromEntries(paths.map(p => [p, lines])), removed: Object.fromEntries(paths.map(p => [p, lines])) }))).toEqual([]);
|
||||
});
|
||||
|
||||
// Value: protects=the classifier fails closed like the depth does; fails_when=an unreadable diff, a scope error or a missing gate
|
||||
// scan returns no class, so the targeted reader silently skips; seam=targetedClasses errors
|
||||
test('an input the classifier cannot read fails closed to all four classes with the reason', () => {
|
||||
const all: TargetedClass[] = ['money', 'gate', 'auth', 'secret'];
|
||||
expect(targetedClasses(input(['src/a.ts'], { scope: undefined }))).toMatchObject({ classes: all, errors: ['scope unavailable (gstack-diff-scope did not run)'] });
|
||||
expect(targetedClasses(input(['src/a.ts'], { scope: { SCOPE_ERROR: 'unmatched' } })).classes).toEqual(all);
|
||||
expect(targetedClasses(input(['src/a.ts'], { gateTagged: null })).errors).toEqual(['gate scan unavailable']);
|
||||
expect(targetedClasses(input(['src/a.ts'], { errors: [{ code: 'numstat_failed', input: 'git diff --numstat' }] })).classes).toEqual(all);
|
||||
expect(classesOf(input(['docs/a.md'], { gateTagged: null }))).toEqual([]);
|
||||
expect(TARGETED_RULES).toMatch(/^[0-9a-f]{16}$/);
|
||||
});
|
||||
|
||||
test('the classifier and the brief are gate class, so an edit that weakens them gets full review', () => {
|
||||
for (const p of ['lib/review-targeted.ts', 'review/specialists/targeted.md']) expect(classifyDiff({ paths: [p], scope: {}, gateTagged: 0 }).class).toBe('gate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the targeted brief', () => {
|
||||
const text = fs.readFileSync(path.join(ROOT, 'review/specialists/targeted.md'), 'utf8');
|
||||
// Value: protects=the brief carries Garry's floor and the checklist's uncapped-spend rule word for word; fails_when=the floor is
|
||||
// paraphrased or the money block cites a line the checklist no longer has; seam=review/specialists/targeted.md
|
||||
test('states the floor verbatim, cites the Forced-CRITICAL spend line the checklist still has, and keeps the specialist schema', () => {
|
||||
expect(text).toContain('CRITICAL is the blocking floor: a finding that would fail a test, break production, lose data or secrets, or open a security hole (injection, auth bypass, secret exposure). Everything else is INFORMATIONAL.');
|
||||
const checklist = fs.readFileSync(path.join(ROOT, 'review/checklist.md'), 'utf8');
|
||||
for (const quote of ['a paid call, retry or loop with no cap', 'CRITICAL on sight']) { expect(text).toContain(quote); expect(checklist).toContain(quote); }
|
||||
expect(text).toContain('Read-only');
|
||||
expect(text).toContain('If no findings: output `NO FINDINGS` and nothing else.');
|
||||
expect(text).toContain('"specialist":"targeted"');
|
||||
});
|
||||
test('targetedBrief keeps the preamble and only the blocks of the classes that hit', () => {
|
||||
const brief = targetedBrief(text, ['money', 'secret']);
|
||||
expect(brief).toContain('CRITICAL is the blocking floor');
|
||||
expect(brief).toContain('## money');
|
||||
expect(brief).toContain('## secret');
|
||||
expect(brief).not.toContain('## gate');
|
||||
expect(brief).not.toContain('## auth');
|
||||
expect(() => targetedBrief(text, [])).toThrow(/no class/);
|
||||
expect(targetedBriefSha()).toBe(createHash('sha256').update(fs.readFileSync(path.join(ROOT, 'review/specialists/targeted.md'))).digest('hex'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('planReview dispatches the targeted reader', () => {
|
||||
const plan = (o: Partial<PlanInput>) => planReview({ paths: ['src/a.ts'], scope: {}, gateTagged: 0, diffLines: 5, numbers: scanNumbers({}), history: { fullPasses: 0, last: 'none' }, ...o } as PlanInput);
|
||||
const roles = (p: ReturnType<typeof plan>) => p.dispatches.map(d => `${d.role} ${d.effort}`);
|
||||
test('at light, standard and full depth when a class hit, never when none did', () => {
|
||||
expect(plan({ targeted: { classes: ['money'] } })).toMatchObject({ depth: 'light' });
|
||||
expect(roles(plan({ targeted: { classes: ['money'] } }))).toContain('targeted full');
|
||||
expect(plan({ diffLines: 80, perPath: [{ path: 'src/a.ts', lines: 80, binary: false }], targeted: { classes: ['secret'] } }).depth).toBe('standard');
|
||||
expect(roles(plan({ diffLines: 80, perPath: [{ path: 'src/a.ts', lines: 80, binary: false }], targeted: { classes: ['secret'] } }))).toContain('targeted full');
|
||||
expect(plan({ gateTagged: 1, targeted: { classes: ['gate'] } }).depth).toBe('full');
|
||||
expect(roles(plan({ gateTagged: 1, targeted: { classes: ['gate'] } }))).toContain('targeted full');
|
||||
expect(roles(plan({ targeted: { classes: [] } }))).not.toContain('targeted full');
|
||||
expect(roles(plan({ targeted: { classes: ['money'] }, history: { fullPasses: 1, last: 'full', fixedBelow: true } }))).toEqual(['fix-check full', 'targeted full']);
|
||||
expect(plan({ targeted: { classes: ['money'] }, history: { fullPasses: 1, last: 'full' } }).dispatches).toEqual([]);
|
||||
const ran = plan({ targeted: { classes: ['money'], ran: true } });
|
||||
expect(roles(ran)).not.toContain('targeted full');
|
||||
expect(ran.skipped).toContain('targeted (already read this product patch)');
|
||||
});
|
||||
});
|
||||
|
||||
// --- gstack-review-plan on fixture repositories outside the checkout ---------------------------------------------------
|
||||
const lines = (n: number, f: (i: number) => string) => Array.from({ length: n }, (_, i) => f(i)).join('\n') + '\n';
|
||||
type Fixture = { base?: Record<string, string>; head: Record<string, string>; args?: string[] };
|
||||
const FIXTURES: Record<string, Fixture> = {
|
||||
'none-light': { head: { 'src/util.ts': lines(5, i => `export const v${i} = ${i};`) } },
|
||||
'none-standard': { head: { 'src/util.ts': lines(80, i => `export const v${i} = ${i};`) } },
|
||||
'none-docs': { head: { 'docs/billing.md': 'Set spend_cap_usd to 5 and read process.env.OPENAI_API_KEY.\n' } },
|
||||
'none-tests': { head: { 'test/billing.test.ts': 'const spend_cap_usd = 5;\nconst k = process.env.OPENAI_API_KEY;\n' } },
|
||||
'none-costume': { head: { 'src/costume.ts': 'export const costume = 1;\nexport const feeder = 2;\nexport const pricey = 3;\n' } },
|
||||
'none-gate-path': { head: { 'lib/pregate-check.ts': 'export const strict = true;\n' } },
|
||||
'secret-light': { head: { 'src/client.ts': 'export const key = process.env.OPENAI_API_KEY;\n' } },
|
||||
'secret-path': { head: { 'lib/credentials.ts': 'export const store = new Map<string, string>();\n' } },
|
||||
'secret-removed': { base: { 'src/log.ts': 'export function log(s: string) {\n console.log(s.replaceAll(process.env.ANTHROPIC_API_KEY ?? "", "***"));\n}\n' }, head: { 'src/log.ts': 'export function log(s: string) {\n console.log(s);\n}\n' } },
|
||||
'gate-tagged': { head: { 'src/a.ts': 'export const a = 1;\n' }, args: ['--gate-tagged', '2'] },
|
||||
'money-light': { head: { 'src/charge.ts': 'export const spend_cap_usd = 5;\n' } },
|
||||
'money-path': { head: { 'src/billing/invoice.ts': 'export const n = 1;\n' } },
|
||||
'money-removed': { base: { 'src/run.ts': 'export function run(spent: number, capUsd: number) {\n if (spent > capUsd) throw new Error("over cap");\n return spent;\n}\n' }, head: { 'src/run.ts': 'export function run(spent: number, capUsd: number) {\n return spent;\n}\n' } },
|
||||
'money-standard': { head: { 'src/evals.ts': 'import Anthropic from "@anthropic-ai/sdk";\nconst client = new Anthropic();\n' + lines(78, i => `export const v${i} = ${i};`) } },
|
||||
'auth-path': { head: { 'src/auth/session.ts': 'export const ttl = 60;\n' } },
|
||||
'money-fix-check': { head: { 'src/charge.ts': 'export const spend_cap_usd = 5;\n' }, args: ['--pass', '1', '--last', 'full', '--fixed-below'] },
|
||||
'money-pass2': { head: { 'src/charge.ts': 'export const spend_cap_usd = 5;\n' }, args: ['--pass', '1', '--last', 'full', '--fixed-blocking'] },
|
||||
'gate-repeat': { head: { 'lib/pregate-check.ts': 'export const strict = true;\n' }, args: ['--gate-tagged', '1', '--pass', '1', '--last', 'full', '--fixed-blocking', '--adversarial-ran'] },
|
||||
};
|
||||
/**
|
||||
* main's reviewers on each fixture: gstack-review-plan at 8be7f21 (v1.91.351.0, before this change), DISPATCH lines verbatim.
|
||||
* The branch must dispatch every one of them unchanged, and add only the targeted reader (the no-loss check: a superset).
|
||||
*/
|
||||
const MAIN: Record<string, { depth: string; classes: string; main: string[] }> = {
|
||||
'none-light': { depth: 'light', classes: 'none', main: ['specialist batched effort=full checklists=testing,security,state-machines,red-team', 'adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'] },
|
||||
'none-standard': { depth: 'standard', classes: 'none', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'adversarial-native adversarial effort=full', 'outside-voice outside-voice effort=host'] },
|
||||
'none-docs': { depth: 'light', classes: 'none', main: [] },
|
||||
'none-tests': { depth: 'light', classes: 'none', main: [] },
|
||||
'none-costume': { depth: 'light', classes: 'none', main: ['specialist batched effort=full checklists=testing,security,state-machines,red-team', 'adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'] },
|
||||
'none-gate-path': { depth: 'full', classes: 'none', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'red-team red-team effort=full', 'adversarial-native adversarial effort=full', 'outside-voice outside-voice effort=host'] },
|
||||
'secret-light': { depth: 'light', classes: 'secret', main: ['specialist batched effort=full checklists=testing,security,state-machines,red-team', 'adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'] },
|
||||
'secret-path': { depth: 'full', classes: 'secret', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'red-team red-team effort=full', 'adversarial-native adversarial effort=full', 'outside-voice outside-voice effort=host'] },
|
||||
'secret-removed': { depth: 'light', classes: 'secret', main: ['specialist batched effort=full checklists=testing,security,state-machines,red-team', 'adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'] },
|
||||
'gate-tagged': { depth: 'full', classes: 'gate', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'red-team red-team effort=full', 'adversarial-native adversarial effort=full', 'outside-voice outside-voice effort=host'] },
|
||||
'money-light': { depth: 'light', classes: 'money', main: ['specialist batched effort=full checklists=testing,security,state-machines,red-team', 'adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'] },
|
||||
'money-path': { depth: 'light', classes: 'money', main: ['specialist batched effort=full checklists=testing,security,state-machines,red-team', 'adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'] },
|
||||
'money-removed': { depth: 'light', classes: 'money', main: ['specialist batched effort=full checklists=testing,security,state-machines,red-team', 'adversarial-native adversarial effort=full reason=backtest inconclusive (GSTA-147 D5)'] },
|
||||
'money-standard': { depth: 'standard', classes: 'money', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'adversarial-native adversarial effort=full', 'outside-voice outside-voice effort=host'] },
|
||||
'auth-path': { depth: 'full', classes: 'auth', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'red-team red-team effort=full', 'adversarial-native adversarial effort=full', 'outside-voice outside-voice effort=host'] },
|
||||
'money-fix-check': { depth: 'light', classes: 'money', main: ['fix-check fix-check effort=full'] },
|
||||
'money-pass2': { depth: 'light', classes: 'money', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'red-team red-team effort=full', 'adversarial-native adversarial effort=full', 'outside-voice outside-voice effort=host'] },
|
||||
'gate-repeat': { depth: 'full', classes: 'gate', main: ['specialist batched effort=medium checklists=testing,security,state-machines', 'red-team red-team effort=full'] },
|
||||
};
|
||||
|
||||
function planRepo(f: Fixture) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'targeted-plan-'));
|
||||
const state = fs.mkdtempSync(path.join(os.tmpdir(), 'targeted-plan-state-'));
|
||||
dirs.push(dir, state);
|
||||
const git = (...a: string[]) => spawnSync('git', a, { cwd: dir, encoding: 'utf8', timeout: 10_000 });
|
||||
const write = (files: Record<string, string>) => { for (const [p, t] of Object.entries(files)) { fs.mkdirSync(path.dirname(path.join(dir, p)), { recursive: true }); fs.writeFileSync(path.join(dir, p), t); } };
|
||||
git('init', '-q', '-b', 'main'); git('config', 'user.email', 't@t'); git('config', 'user.name', 't'); git('config', 'commit.gpgsign', 'false');
|
||||
write({ 'README.md': '# x\n', ...(f.base ?? {}) }); git('add', '.'); git('commit', '-qm', 'base');
|
||||
git('checkout', '-qb', 'feat'); write(f.head); git('add', '.'); git('commit', '-qm', 'feat');
|
||||
const run = (args: string[]) => spawnSync(PLAN, ['--base', 'main', ...(args.includes('--gate-tagged') ? [] : ['--gate-tagged', '0']), ...args], { cwd: dir, encoding: 'utf8', timeout: 60_000, env: { ...process.env, GSTACK_STATE_ROOT: state } });
|
||||
return { dir, run, write };
|
||||
}
|
||||
const field = (out: string, prefix: string, key: string) => new RegExp(`^${prefix}.* ${key}=(\\S+)`, 'm').exec(out)?.[1];
|
||||
|
||||
describe('gstack-review-plan prints TARGETED and dispatches the targeted reader', () => {
|
||||
// Value: protects=the no-loss rule for review changes (CREW, 2026-10-30): every reviewer main dispatches on these diffs, at light,
|
||||
// standard and full depth and on fix-check, scoped and repeat steps, is still dispatched unchanged, and the only addition is the
|
||||
// targeted reader on a class diff; fails_when=the change drops, renames or re-efforts any reviewer, or adds the reader on a diff
|
||||
// with no class; seam=bin/gstack-review-plan end to end; no_claim=that the reader finds the defect (the paid replay's question)
|
||||
test('the dispatch-superset no-loss check: on every fixture the branch dispatches main\'s reviewers plus only the targeted reader', () => {
|
||||
expect(Object.keys(MAIN).sort()).toEqual(Object.keys(FIXTURES).sort());
|
||||
expect(new Set(Object.values(MAIN).map(m => m.depth))).toEqual(new Set(['light', 'standard', 'full']));
|
||||
for (const [name, f] of Object.entries(FIXTURES)) {
|
||||
const r = planRepo(f).run(f.args ?? []);
|
||||
expect(r.status, `${name}: ${r.stderr}`).toBe(0);
|
||||
const got = r.stdout.split('\n').filter(l => l.startsWith('DISPATCH: ')).map(l => l.slice('DISPATCH: '.length));
|
||||
const want = MAIN[name]!;
|
||||
expect(r.stdout, name).toContain(`REVIEW_DEPTH: ${want.depth} `);
|
||||
expect(got.filter(d => !want.main.includes(d)), name).toEqual(want.classes === 'none' ? [] : ['targeted targeted effort=full']);
|
||||
expect(want.main.filter(d => !got.includes(d)), name).toEqual([]);
|
||||
expect(field(r.stdout, 'TARGETED:', 'classes'), name).toBe(want.classes);
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
test('the TARGETED line carries the rules hash and the brief\'s sha256; --targeted-ran skips the reader only for the same product patch', () => {
|
||||
const repo = planRepo(FIXTURES['money-light']!);
|
||||
const first = repo.run([]);
|
||||
expect(field(first.stdout, 'TARGETED:', 'rules')).toBe(TARGETED_RULES);
|
||||
expect(field(first.stdout, 'TARGETED:', 'brief')).toBe(targetedBriefSha());
|
||||
const patch = field(first.stdout, 'TARGETED:', 'targeted_patch_id')!;
|
||||
expect(patch).toMatch(/^[0-9a-f]{40}$/);
|
||||
const same = repo.run(['--pass', '1', '--last', 'full', '--fixed-below', '--targeted-ran', patch]);
|
||||
expect(same.stdout).not.toContain('DISPATCH: targeted');
|
||||
expect(same.stdout).toContain('SKIP: targeted (already read this product patch)');
|
||||
const other = repo.run(['--pass', '1', '--last', 'full', '--fixed-below', '--targeted-ran', 'f'.repeat(40)]);
|
||||
expect(other.stdout).toContain('DISPATCH: targeted targeted effort=full');
|
||||
expect(other.stdout).toContain('DIFF_EXCLUDE: targeted none');
|
||||
repo.write({ 'src/charge.ts': 'export const spend_cap_usd = 6;\n' });
|
||||
const dirty = repo.run(['--pass', '1', '--last', 'full', '--fixed-below', '--targeted-ran', patch]);
|
||||
expect(field(dirty.stdout, 'TARGETED:', 'targeted_patch_id')).toBe('uncommitted');
|
||||
expect(dirty.stdout).toContain('DISPATCH: targeted targeted effort=full');
|
||||
expect(JSON.parse(repo.run(['--json']).stdout).targeted).toMatchObject({ classes: ['money'], rules: TARGETED_RULES, targeted_patch_id: 'uncommitted' });
|
||||
const read = planRepo(FIXTURES['money-light']!);
|
||||
const readId = field(read.run([]).stdout, 'TARGETED:', 'targeted_patch_id')!;
|
||||
read.write({ 'test/charge.test.ts': 'test("cap", () => {});\n' });
|
||||
for (const args of [['add', '.'], ['commit', '-qm', 'test-only fix']]) expect(spawnSync('git', args, { cwd: read.dir, encoding: 'utf8', timeout: 30_000 }).status).toBe(0);
|
||||
const testFix = read.run(['--gate-tagged', '1', '--pass', '1', '--last', 'full', '--fixed-below', '--targeted-ran', readId]);
|
||||
expect(field(testFix.stdout, 'TARGETED:', 'classes')).toBe('money,gate');
|
||||
expect(field(testFix.stdout, 'TARGETED:', 'targeted_patch_id')).not.toBe(readId);
|
||||
expect(testFix.stdout).toContain('DISPATCH: targeted targeted effort=full');
|
||||
}, 90_000);
|
||||
});
|
||||
|
||||
// --- production path: render, sent, collect, judge, verify, receipt write, summary and the queue ------------------------
|
||||
describe('the targeted role end to end on a fixture repository', () => {
|
||||
// Value: protects=the targeted reader's record reaches the receipt only through the verbatim-dispatch ledger, bound to the
|
||||
// receipt's product patch, and the review loop and the queue see it; fails_when=render, sent, collect, judge or verify drop the
|
||||
// role, the record claims done without a collected output, write binds another product patch, summary loses the role, or the
|
||||
// queue refuses on it; seam=bin/gstack-review-dispatch, bin/gstack-ship-receipt write, bin/gstack-review-scope summary and
|
||||
// bindShippedReceipt; no_claim=PR 2's enforcement
|
||||
test('render → sent → collect → judge → verify --targeted-out → write --review-targeted → summary → REVIEW_TARGETED', () => {
|
||||
const repo = makeFixtureRepo({ prefix: 'targeted-e2e-' });
|
||||
repos.push(repo);
|
||||
repo.git('checkout', '-q', '-b', 'feat');
|
||||
fs.mkdirSync(path.join(repo.work, 'src'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 5;\n');
|
||||
repo.git('add', '-A'); repo.git('commit', '-q', '-m', 'charge');
|
||||
const head = repo.git('rev-parse', 'HEAD');
|
||||
const base = repo.git('merge-base', 'origin/main', 'HEAD');
|
||||
const plan = runBin(repo, 'gstack-review-plan', ['--base', 'main']);
|
||||
expect(plan.out).toContain('TARGETED: classes=money ');
|
||||
expect(plan.out).toContain('DISPATCH: targeted targeted effort=full');
|
||||
const out = path.join(repo.root, 'briefs');
|
||||
const ledger = path.join(out, 'review-ledger.jsonl');
|
||||
const render = runBin(repo, 'gstack-review-dispatch', ['render', '--role', 'targeted', '--classes', 'money', '--diff-base', base, '--out', out]);
|
||||
expect(render.status, render.err).toBe(0);
|
||||
const file = /file=(\S+)/.exec(render.out)![1]!;
|
||||
const brief = fs.readFileSync(file, 'utf8');
|
||||
expect(brief).toContain('## money');
|
||||
expect(brief).not.toContain('## gate');
|
||||
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');
|
||||
const finding = { severity: 'CRITICAL', confidence: 8, path: 'src/charge.ts', line: 1, category: 'money', summary: 'cap is never enforced', fingerprint: 'src/charge.ts:1:money', specialist: 'targeted' };
|
||||
fs.writeFileSync(output, JSON.stringify(finding) + '\n');
|
||||
const merged = path.join(repo.root, 'merged.jsonl');
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['collect', '--ledger', ledger, '--role', 'targeted', '--output', output, '--merged', merged]).out).toContain('COLLECT: reader=targeted findings=1');
|
||||
const judged = path.join(repo.root, 'judged.jsonl');
|
||||
fs.writeFileSync(judged, JSON.stringify({ id: 'targeted#1', severity: 'CRITICAL' }) + '\n');
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['judge', '--ledger', ledger, '--merged', merged, '--judged', judged]).out).toContain('JUDGMENT: ok');
|
||||
const record = path.join(repo.root, 'targeted.json');
|
||||
const verify = runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger, '--targeted-out', record]);
|
||||
expect(verify.status, verify.out).toBe(0);
|
||||
expect(verify.out).toContain(`TARGETED_RECORD: classes=money status=done findings=1 brief=${targetedBriefSha().slice(0, 12)} rules=${TARGETED_RULES}`);
|
||||
expect(verify.out).toContain('VERIFY: clean');
|
||||
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 50;\n');
|
||||
repo.git('commit', '-qam', 'later product change the reader never read');
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain('TARGETED_RECORD: classes=money status=failed findings=0');
|
||||
repo.git('reset', '-q', '--hard', head);
|
||||
fs.writeFileSync(path.join(repo.work, 'src', 'charge.ts'), 'export const spend_cap_usd = 7;\n');
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain('TARGETED_RECORD: classes=money status=failed findings=0');
|
||||
repo.git('checkout', '--', 'src/charge.ts');
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain('TARGETED_RECORD: classes=money status=done findings=1');
|
||||
|
||||
const body = path.join(repo.root, 'body.md');
|
||||
const write = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', '--review-targeted', `@${record}`, '--body', body, '--json']);
|
||||
expect(write.status, write.err).toBe(0);
|
||||
const receipt = extractReceipt(fs.readFileSync(body, 'utf8')).receipt!;
|
||||
expect(receipt.review_targeted).toEqual({ classes: ['money'], rules: TARGETED_RULES, brief_sha: targetedBriefSha(), product_patch_id: receipt.product_patch_id!, status: 'done', findings: 1, raised_by: 'targeted' });
|
||||
const rebound = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', '--review-targeted', JSON.stringify({ ...JSON.parse(fs.readFileSync(record, 'utf8')), product_patch_id: 'x' }), '--json']);
|
||||
expect(rebound.status).not.toBe(0);
|
||||
expect(rebound.err).toContain("write binds the pass to the receipt's product_patch_id");
|
||||
const plain = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', '--json']);
|
||||
expect(plain.status, plain.err).toBe(0);
|
||||
expect(JSON.parse(plain.out)).not.toHaveProperty('review_targeted');
|
||||
|
||||
const slug = runBin(repo, 'gstack-slug', ['--get', 'SLUG']).out.trim();
|
||||
const logDir = path.join(repo.home, 'projects', slug);
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
const row = { skill: 'review', via: 'ship', timestamp: '2026-10-30T10:00:00Z', status: 'clean', review_scope: 'frozen', findings: [], dispatches: [{ role: 'specialist' }, { role: 'adversarial-native' }, { role: 'targeted' }] };
|
||||
fs.writeFileSync(path.join(logDir, 'feat-reviews.jsonl'), JSON.stringify(row) + '\n');
|
||||
const summary = runBin(repo, 'gstack-review-scope', ['summary', '--since', '2026-10-30T09:00:00Z', '--json']);
|
||||
expect(summary.status, summary.err).toBe(0);
|
||||
expect(JSON.parse(summary.out).dispatches).toEqual({ specialist: 1, 'adversarial-native': 1, targeted: 1 });
|
||||
|
||||
const bodyText = fs.readFileSync(body, 'utf8');
|
||||
const ok = bindShippedReceipt(repo.work, 'origin/main', bodyText, undefined, [head]);
|
||||
expect(ok).toMatchObject({ ok: true, targetedLine: 'REVIEW_TARGETED: required=money receipt=money verdict=ok' });
|
||||
const { review_targeted: _drop, ...legacy } = receipt;
|
||||
const missing = bindShippedReceipt(repo.work, 'origin/main', renderReceiptBlock(legacy as ShipReceipt), undefined, [head]);
|
||||
expect(missing).toMatchObject({ ok: true, targetedLine: 'REVIEW_TARGETED: required=money receipt=legacy verdict=missing' });
|
||||
const failed = bindShippedReceipt(repo.work, 'origin/main', renderReceiptBlock({ ...receipt, review_targeted: { ...receipt.review_targeted!, status: 'failed' } }), undefined, [head]);
|
||||
expect(failed).toMatchObject({ ok: true, targetedLine: 'REVIEW_TARGETED: required=money receipt=none verdict=missing' });
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
describe('the receipt record and the queue line', () => {
|
||||
const record = { classes: ['gate', 'money'], rules: TARGETED_RULES, brief_sha: 'a'.repeat(64), status: 'done', findings: 0, raised_by: 'targeted' };
|
||||
test('parseReviewTargeted binds the receipt\'s product patch, orders the classes and refuses a malformed record', () => {
|
||||
expect(parseReviewTargeted(record, 'p1')).toEqual({ ok: true, targeted: { ...record, classes: ['money', 'gate'], product_patch_id: 'p1' } as any });
|
||||
for (const bad of [{ ...record, classes: ['cost'] }, { ...record, status: 'skipped' }, { ...record, findings: -1 }, { ...record, brief_sha: 'abc' }, { ...record, extra: 1 }, null]) expect(parseReviewTargeted(bad, 'p1').ok).toBe(false);
|
||||
});
|
||||
// Value: protects=PR 1's queue only reports; fails_when=the line misreads a legacy, failed or partial receipt as covered, or a
|
||||
// receipt without classes as required; seam=receiptTargetedLine (the line ready and train build print)
|
||||
test('REVIEW_TARGETED reports ok, missing and legacy', () => {
|
||||
const r = (rt?: object) => ({ schema_version: 1, head: 'h', base: 'main', ...(rt ? { review_targeted: { ...record, product_patch_id: 'p', ...rt } } : {}) }) as ShipReceipt;
|
||||
expect(receiptTargetedLine([], r())).toBe('REVIEW_TARGETED: required=none receipt=legacy verdict=ok');
|
||||
expect(receiptTargetedLine(['money'], r())).toBe('REVIEW_TARGETED: required=money receipt=legacy verdict=missing');
|
||||
expect(receiptTargetedLine(['money'], r({}))).toBe('REVIEW_TARGETED: required=money receipt=gate,money verdict=ok');
|
||||
expect(receiptTargetedLine(['money', 'secret'], r({}))).toBe('REVIEW_TARGETED: required=money,secret receipt=gate,money verdict=missing');
|
||||
expect(receiptTargetedLine(['gate'], r({ status: 'failed' }))).toBe('REVIEW_TARGETED: required=gate receipt=none verdict=missing');
|
||||
expect(receiptTargetedLine([], r({ classes: [], status: 'not-run' }))).toBe('REVIEW_TARGETED: required=none receipt=none verdict=ok');
|
||||
});
|
||||
test('a carry keeps review_targeted with the unchanged product patch', () => {
|
||||
const receipt = { schema_version: 1, head: 'a'.repeat(40), base: 'main', product_patch_id: 'p', review_targeted: { ...record, product_patch_id: 'p' } } as ShipReceipt;
|
||||
const carried = carryReceipt(receipt, 'b'.repeat(40), { from: 'a'.repeat(40), reason: 'base merge', product_patch_id: 'p', changed_paths: [], merge_commits: 1, range_diff: 'same', covers: [], reruns: [] }, { tree: 't', productPatchId: 'p' });
|
||||
expect(carried.review_targeted).toEqual(receipt.review_targeted);
|
||||
expect(carried.product_patch_id).toBe(carried.review_targeted!.product_patch_id);
|
||||
});
|
||||
test('the targeted reader\'s findings are attributed to it and read as a Step 11 voice', () => {
|
||||
expect(raisedBy({ specialist: 'targeted' })).toBe('targeted');
|
||||
expect(summarizeReviewLoop([{ skill: 'review', via: 'ship', timestamp: '2026-10-30T10:00:00Z', dispatches: [{ role: 'targeted', prompt_bytes: 40 }] }], '2026-10-30T09:00:00Z')).toMatchObject({ dispatches: { targeted: 1 }, est_input_tokens: 10 });
|
||||
});
|
||||
});
|
||||
@@ -383,7 +383,7 @@ describe('variant E: the review segment and the docs reader (GSTA-167 replay 5)'
|
||||
expect(settled.out).toMatch(new RegExp(`^DRIFT: id=${id} kind=dispatch next=dispatch:docs projected=${K.per_dispatch} actual=2000000 `, 'm'));
|
||||
const step = check(h, s.run, '14.5', '15', ['--reading', '3500000']);
|
||||
expect(step.out).toMatch(/^DRIFT: id=\S+ kind=step next=14\.5 projected=\d+ actual=400000 /m);
|
||||
expect(h.sb(['check', '--run', s.run, '--step', '15', '--next', 'dispatch:doc', '--reading', '3600000']).err).toContain('valid: specialist, red-team, adversarial-native, outside-voice, fix-check, docs');
|
||||
expect(h.sb(['check', '--run', s.run, '--step', '15', '--next', 'dispatch:doc', '--reading', '3600000']).err).toContain('valid: specialist, red-team, adversarial-native, outside-voice, fix-check, targeted, docs');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"bytes": 302,
|
||||
"why": "GST-257: Step 11 dispatches the targeted reader on money, gate, auth and secret diffs, on the merge base every pass"
|
||||
}
|
||||
Reference in new issue
Block a user