mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
fix(ship): GST-986 targeted record binds to the read commit; write/carry run the targeted verdict; fix-check ledgers count
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
f0d9d42fda
commit
e0ff828349
18 files changed
+132
-57
No files matched your search
@@ -24,7 +24,7 @@ import { cleanMarkers, collect, mayBeUnavailable, parseFindings, readJsonl, read
|
||||
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 { TARGETED_RULES, parseClassList, targetedBriefSha, targetedRead } from '../lib/review-targeted';
|
||||
import { resolveBaseBranch } from '../lib/ship-policy';
|
||||
|
||||
const HELP = `gstack-review-dispatch — verbatim reviewer briefs and loss-free collection for /ship
|
||||
@@ -130,7 +130,7 @@ function render(): number {
|
||||
fs.writeFileSync(file, text, { mode: 0o600 });
|
||||
const ledger = flag('--ledger') ?? path.join(out, 'review-ledger.jsonl');
|
||||
const sha = promptHash(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' };
|
||||
const targeted = brief === undefined ? {} : { classes: parseClassList(flag('--classes')!), rules: TARGETED_RULES, brief_sha: targetedBriefSha(checklist), diff_base: diffBase ?? null, ...(diffBase ? targetedRead(process.cwd(), diffBase) : { patch_id: 'none', head: null }) };
|
||||
appendEvent(ledger, { event: 'rendered', reader: readerKey(role, name), role, name: name ?? null, file, sha256: sha, bytes: Buffer.byteLength(text), workdir, tree, ...targeted });
|
||||
process.stdout.write(`BRIEF: reader=${readerKey(role, name)} sha256=${sha} bytes=${Buffer.byteLength(text)} file=${file}\n`);
|
||||
return EXIT.ok;
|
||||
@@ -279,19 +279,22 @@ function program(): number {
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* collected, and it was rendered on a committed diff (uncommitted never proves what was read). `head` names the commit it read:
|
||||
* a later commit does not undo the read, and gstack-ship-receipt write binds the record only while that commit's product patch is
|
||||
* the receipt's (GST-986: Step 11 fixes after the read printed status=failed findings=0 for a completed reader).
|
||||
*/
|
||||
function targetedRecord(ledger: string, rows: VerifyRow[]): { classes: string[]; rules: string; brief_sha: string; status: 'done' | 'failed'; findings: number; raised_by: string } | null {
|
||||
function targetedRecord(ledger: string, rows: VerifyRow[]): { classes: string[]; rules: string; brief_sha: string; status: 'done' | 'failed'; findings: number; raised_by: string; head: string | null } | 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 head = typeof render.head === 'string' && /^[0-9a-f]{40,64}$/.test(render.head) ? render.head : null;
|
||||
const committed = typeof render.patch_id === 'string' && !['uncommitted', 'none'].includes(render.patch_id) && head !== null;
|
||||
const done = brief?.status === 'MATCH' && output?.status === 'MATCH' && committed;
|
||||
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' };
|
||||
return { classes: (render.classes as string[]) ?? [], rules: String(render.rules), brief_sha: String(render.brief_sha), status: done ? 'done' : 'failed', findings: Array.isArray(ids) ? ids.length : 0, raised_by: 'targeted', head };
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
@@ -359,7 +362,7 @@ function main(): number {
|
||||
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`);
|
||||
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} read=${record.head?.slice(0, 12) ?? 'none'}\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}${v.unavailable ? ` unavailable=${v.unavailable}` : ''}\n`);
|
||||
|
||||
@@ -27,7 +27,7 @@ import { runScriptSync } from '../lib/script-invocation';
|
||||
import { isUnavailable, loadPolicyFromBase, policyUnavailableLine, resolveBaseBranch } from '../lib/ship-policy';
|
||||
import { EXIT as POST_EXIT, runPost } from '../lib/gstack-post';
|
||||
import {
|
||||
parseHunks, renderDeferredIssue, renderDeferredSection, reviewLoopLine, scopeFindings, scopeLine, summarizeReviewLoop,
|
||||
ledgerPass, parseHunks, renderDeferredIssue, renderDeferredSection, reviewLoopLine, scopeFindings, scopeLine, summarizeReviewLoop,
|
||||
ALREADY_FILED_REASON, DEFERRED_MARKER, findingIdentity, findingWhere, REVIEW_SCOPE_POLICIES, type DeferredRecord, type ReviewScopePolicy, type ScopeFinding, type ScopedFinding,
|
||||
} from '../lib/review-scope';
|
||||
import { ledgerBlockers, type LedgerBlocker } from '../lib/review-ledger';
|
||||
@@ -269,7 +269,7 @@ function cmdSummary(rest: string[], json: boolean): number {
|
||||
if (!noLedger && !files.length) return refuse('summary', 'REVIEW_SCOPE_LEDGER', 'name the review\'s dispatch ledgers (--run <run dir> or --ledger <f>), or --no-ledger when no reader ran through gstack-review-dispatch');
|
||||
let ledgers: LedgerBlocker[][];
|
||||
try { ledgers = files.map(ledgerBlockers); } catch (e: any) { return refuse('summary', 'REVIEW_SCOPE_LEDGER', e.message); }
|
||||
const summary = summarizeReviewLoop(rows, since, ledgers);
|
||||
const summary = summarizeReviewLoop(rows, since, ledgers, files.map(ledgerPass));
|
||||
const line = reviewLoopLine(summary);
|
||||
// A /ship invocation always runs Step 9 once; zero matching rows means the wrong --since or a review log written elsewhere, never a loop to record.
|
||||
if (summary.passes === 0) return refuse('summary', 'REVIEW_SCOPE_INPUT', `no review-log row of this branch is a via:ship review at or after ${since}; check REVIEW_SINCE (${line})`);
|
||||
|
||||
+13
-5
@@ -48,7 +48,7 @@ import {
|
||||
} from '../lib/ship-receipt';
|
||||
import { checkDocsCaps, docsCapLine, docsExceptionLine, parseReceiptDocs } from '../lib/ship-receipt-docs';
|
||||
import { comparePath, contextPacket, PACKET_SECTION_CAP, readScopedReview, touchedPaths } from '../lib/ship-patch';
|
||||
import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, parseReviewTargeted, receiptDepthAtHead, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
|
||||
import { deeper, depthBelow, exceptionProblems, parseReviewLoopException, parseReviewTargeted, receiptDepthAtHead, receiptTargetedAtHead, reviewLoopExceptionLine } from '../lib/ship-receipt-depth';
|
||||
import { checkReceiptProse, proseLine } from '../lib/ship-receipt-prose';
|
||||
import { resolveStateRoot } from '../lib/state-root';
|
||||
import { readPause, receiptBaseRed } from '../lib/base-red';
|
||||
@@ -95,9 +95,10 @@ 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.
|
||||
--review-targeted <json|@file> ({classes, rules, brief_sha, status, findings, raised_by, head}: gstack-review-dispatch verify
|
||||
--targeted-out); recorded as review_targeted bound to the receipt's product_patch_id, and refused (RECEIPT_INVALID) when the
|
||||
commit the reader read (head) has another product patch. write and carry refuse a receipt whose head needs a targeted class
|
||||
(money, gate, auth, secret) that no done pass covers, as the queue would (RECEIPT_INVALID, RECEIPT_CARRY_REFUSED).
|
||||
--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
|
||||
@@ -221,7 +222,7 @@ function cmdWrite(rest: string[], json: boolean): number {
|
||||
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}` }; }
|
||||
try { targeted = parseReviewTargeted(jsonArg(flag(rest, '--review-targeted')), product.product_patch_id ?? 'none', read => reviewedProduct(repoRoot, base, read, pregateFile).product_patch_id); } 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') });
|
||||
@@ -266,6 +267,11 @@ function cmdWrite(rest: string[], json: boolean): number {
|
||||
process.stderr.write('gstack-ship-receipt write: nothing written\n');
|
||||
return EXIT.fail;
|
||||
}
|
||||
const targetedVerdict = receiptTargetedAtHead(repoRoot, `origin/${base.replace(/^origin\//, '')}`, receipt, receipt.head);
|
||||
if (!targetedVerdict.ok) {
|
||||
process.stderr.write(`${targetedVerdict.line}\n${resultLine('gstack-ship-receipt write', 'RECEIPT_INVALID', `${targetedVerdict.message}; run the targeted reader on HEAD and pass its record (--review-targeted @<gstack-review-dispatch verify --targeted-out file>); nothing written`)}\n`);
|
||||
return EXIT.fail;
|
||||
}
|
||||
const body = bodyFile;
|
||||
const missing = body && receipt.issues ? missingIssueLines(fs.existsSync(body) ? fs.readFileSync(body, 'utf8') : '', receipt.issues) : [];
|
||||
if (missing.length) {
|
||||
@@ -733,6 +739,8 @@ function cmdCarry(rest: string[], json: boolean): number {
|
||||
const carried = carryReceipt(rebound, head, plan.carry, { tree: headTree, fromTree, productPatchId: headPatch });
|
||||
const errors = validateReceipt(carried);
|
||||
if (errors.length) return refused('RECEIPT_CARRY_REFUSED', `the carried receipt does not validate: ${errors[0]!.message} at ${errors[0]!.path}`);
|
||||
const targetedVerdict = receiptTargetedAtHead(repoRoot, baseRef, carried, head);
|
||||
if (!targetedVerdict.ok) return refused('RECEIPT_CARRY_REFUSED', `${targetedVerdict.line}: ${targetedVerdict.message}, so the queue would refuse the carried receipt; run /ship on ${head.slice(0, 12)}`);
|
||||
const newBody = placeReceiptFirst(body, carried);
|
||||
const line = receiptSummaryLine(carried);
|
||||
if (scopedDryRun) return refused('RECEIPT_CARRY_SCOPED_REVIEW', scopedDryRun);
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ import * as path from 'node:path';
|
||||
import { BriefError, briefFile, checkoutRoot, checkoutTree, promptHash, renderBrief, withWorkspace, 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';
|
||||
import { TARGETED_RULES, parseClassList, targetedBriefSha, targetedRead } from './review-targeted';
|
||||
|
||||
export const STAGES = ['first', 'specialists', 'red-team', 'voices'] as const;
|
||||
export type Stage = typeof STAGES[number];
|
||||
@@ -179,7 +179,7 @@ export function renderStage(m: Manifest, stage: Stage, t: SectionTexts, diffFor:
|
||||
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!) };
|
||||
targeted = { classes: parseClassList(slots.classes!), rules: TARGETED_RULES, brief_sha: targetedBriefSha(checklist), diff_base: slots.diff_base, ...targetedRead(process.cwd(), slots.diff_base!) };
|
||||
}
|
||||
const text = withWorkspace(renderBrief(role, slots, t), workdir);
|
||||
const file = path.join(briefs, briefFile(role, r.name));
|
||||
|
||||
+29
-6
@@ -14,7 +14,7 @@
|
||||
* Pure functions here; bin/gstack-review-scope runs git and gh.
|
||||
*/
|
||||
import { SPECIALISTS } from './review-depth';
|
||||
import { rank, type LedgerBlocker } from './review-ledger';
|
||||
import { rank, readLedger, type LedgerBlocker } from './review-ledger';
|
||||
import { parseSpecialistOutput } from './state-matrix';
|
||||
|
||||
export type Locus = 'diff' | 'fix-range' | 'outside';
|
||||
@@ -370,6 +370,18 @@ function rowLocus(f: Record<string, any>): string | null {
|
||||
return m ? `${m[1]!.replace(/^\.\//, '')}:${m[2]}` : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a dispatch ledger started (its first event) and whether it holds a fix-check reader's review: a fix-check run through
|
||||
* gstack-review-dispatch (the pass program's `fix-check` step) leaves this ledger even when no review-fix-check row was logged.
|
||||
*/
|
||||
export interface LedgerPass { started: number; fixCheck: boolean }
|
||||
export function ledgerPass(ledger: string): LedgerPass {
|
||||
const events = readLedger(ledger);
|
||||
const started = Date.parse(String(events[0]?.at ?? ''));
|
||||
const fixCheck = events.some(e => e.event === 'output' && e.reader === 'fix-check' && e.tree_ok !== false && ((Array.isArray(e.ids) && e.ids.length > 0) || e.review === true));
|
||||
return { started: Number.isFinite(started) ? started : NaN, fixCheck };
|
||||
}
|
||||
|
||||
/**
|
||||
* The loop summary. blocking_found counts every blocker found (GST-951). Each CRITICAL the dispatch ledgers hold
|
||||
* (`ledgerBlockers`, one list per ledger) is paired with at most one row finding per ledger at the same path:line, raised
|
||||
@@ -380,8 +392,13 @@ function rowLocus(f: Record<string, any>): string | null {
|
||||
* A ledger CRITICAL with no locus, or a second one at a paired line, counts on its own. A different defect the same reviewer
|
||||
* raises at the exact line of a finding a row classified below the floor pairs with that finding: path:line is the only
|
||||
* identity rows and ledgers share until rows carry the ledger id.
|
||||
* Fix-checks (GST-986, #3862; `ledgerPasses`, one per ledger): fix_checks is the larger of the review-fix-check rows and the
|
||||
* fix-check ledgers, so one logged both ways counts once. When fix-checks ran without their review-fix-check rows (more fix-check
|
||||
* ledgers than rows), a blocker no row disposes counts as fixed if a fix-check ledger started after the ledger it came from:
|
||||
* decision 3.5 runs a fix-check only after the loop applied that pass's fixes, and a blocker the loop does not fix is deferred
|
||||
* or noted by a row. A blocker the last pass found stays unrecorded, and with every fix-check logged the rows decide.
|
||||
*/
|
||||
export function summarizeReviewLoop(rows: readonly Record<string, any>[], since: string, ledgers: readonly (readonly LedgerBlocker[])[] = []): ReviewLoopSummary {
|
||||
export function summarizeReviewLoop(rows: readonly Record<string, any>[], since: string, ledgers: readonly (readonly LedgerBlocker[])[] = [], ledgerPasses: readonly LedgerPass[] = []): ReviewLoopSummary {
|
||||
const sinceMs = Date.parse(since);
|
||||
const inRun = (r: Record<string, any>) => r && r.via === 'ship' && Number.isFinite(rowTime(r)) && (!Number.isFinite(sinceMs) || rowTime(r) >= sinceMs);
|
||||
const loopRows = rows.filter(r => inRun(r) && (r.skill === 'review' || r.skill === LATE_DEFER_SKILL || r.skill === FIX_CHECK_SKILL));
|
||||
@@ -414,20 +431,26 @@ export function summarizeReviewLoop(rows: readonly Record<string, any>[], since:
|
||||
}
|
||||
const paired = new Set<string>();
|
||||
let ledgerOnly = 0;
|
||||
let ledgerOnlyFixed = 0;
|
||||
const pairedIds = new Set<string>();
|
||||
const followedIds = new Set<string>();
|
||||
const fixCheckStarts = ledgerPasses.filter(p => p.fixCheck && Number.isFinite(p.started)).map(p => p.started);
|
||||
const rowFixChecks = loopRows.filter(r => r.skill === FIX_CHECK_SKILL).length;
|
||||
const followed = (n: number) => fixCheckStarts.length > rowFixChecks && fixCheckStarts.some(t => t > (ledgerPasses[n]?.started ?? Infinity));
|
||||
for (const [n, list] of ledgers.entries()) {
|
||||
for (const b of list) {
|
||||
const by = b.id.startsWith('fix-check') ? null : raiserOf(b.id);
|
||||
const id = b.locus ? [...seen].find(([k, s]) => s.candidate && s.locus === b.locus && (!s.raiser || !by || s.raiser === by) && !paired.has(`${n}\0${k}`))?.[0] : undefined;
|
||||
if (id) { paired.add(`${n}\0${id}`); pairedIds.add(id); } else ledgerOnly++;
|
||||
if (id) { paired.add(`${n}\0${id}`); pairedIds.add(id); if (followed(n)) followedIds.add(id); } else { ledgerOnly++; if (followed(n)) ledgerOnlyFixed++; }
|
||||
}
|
||||
}
|
||||
const blockers = [...seen].filter(([k, s]) => s.blocks || (pairedIds.has(k) && !s.classified));
|
||||
const unrecorded = ledgerOnly + blockers.filter(([, s]) => !s.disposed).length;
|
||||
const out: ReviewLoopSummary = { passes: passes.length, blocking_found: blockers.length + ledgerOnly, deferred: deferred.size, noted: noted.size, fixed: fixed.size, scope };
|
||||
for (const [k, s] of blockers) if (!s.disposed && followedIds.has(k)) { s.disposed = true; fixed.add(k); }
|
||||
const unrecorded = ledgerOnly - ledgerOnlyFixed + blockers.filter(([, s]) => !s.disposed).length;
|
||||
const out: ReviewLoopSummary = { passes: passes.length, blocking_found: blockers.length + ledgerOnly, deferred: deferred.size, noted: noted.size, fixed: fixed.size + ledgerOnlyFixed, scope };
|
||||
if (unrecorded) out.unrecorded = unrecorded;
|
||||
if (ledgers.length) out.ledgers_read = ledgers.length;
|
||||
const fixChecks = loopRows.filter(r => r.skill === FIX_CHECK_SKILL).length;
|
||||
const fixChecks = Math.max(rowFixChecks, fixCheckStarts.length);
|
||||
if (fixChecks) out.fix_checks = fixChecks;
|
||||
out.rounds = passes.length + fixChecks;
|
||||
const byRole: Record<string, number> = {};
|
||||
|
||||
@@ -82,6 +82,12 @@ export function targetedPatchId(root: string, mergeBase: string): string {
|
||||
return git(['patch-id', '--verbatim'], diff.stdout).stdout?.trim().split(/\s+/)[0] || 'none';
|
||||
}
|
||||
|
||||
/** What a targeted render records: the patch the reader reads and the commit it sits on, which receipt write binds to its product patch. */
|
||||
export function targetedRead(root: string, mergeBase: string): { patch_id: string; head: string | null } {
|
||||
const head = spawnSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], { cwd: root, encoding: 'utf8', timeout: 30_000 }).stdout?.trim();
|
||||
return { patch_id: targetedPatchId(root, mergeBase), head: head || null };
|
||||
}
|
||||
|
||||
/** A class list as printed and recorded: comma-joined, or `none`. */
|
||||
export const classList = (classes: readonly string[]) => (classes.length ? classes.join(',') : 'none');
|
||||
|
||||
|
||||
@@ -128,17 +128,24 @@ export function exceptionProblems(body: string, r: ShipReceipt, isAncestor?: (co
|
||||
|
||||
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 } {
|
||||
/**
|
||||
* `write --review-targeted` input (gstack-review-dispatch verify --targeted-out), bound to the receipt's product patch-id here.
|
||||
* `head` is the commit the reader read; `productPatchOf` gives that commit's product patch-id by the receipt's own rules, and the
|
||||
* record binds only when it equals the receipt's, so a record verified before a product change never vouches for it (GST-986).
|
||||
*/
|
||||
export function parseReviewTargeted(value: unknown, productPatchId: string, productPatchOf: (head: string) => string | undefined): { 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 }');
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return bad('review_targeted must be { classes, rules, brief_sha, status, findings, raised_by, head }');
|
||||
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)' : ''}`);
|
||||
for (const key of Object.keys(v)) if (!['classes', 'rules', 'brief_sha', 'status', 'findings', 'raised_by', 'head'].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');
|
||||
if (typeof v.head !== 'string' || !/^[0-9a-f]{40,64}$/.test(v.head)) return bad('review_targeted.head must be the commit the targeted reader read (rerun gstack-review-dispatch verify --targeted-out with this gstack)');
|
||||
const read = productPatchOf(v.head);
|
||||
if (read !== productPatchId) return bad(`the targeted reader read ${v.head.slice(0, 12)}, whose product patch ${read?.slice(0, 12) ?? 'cannot be read'} is not this receipt's ${productPatchId.slice(0, 12)}: the product changed after the read, so run the targeted reader on HEAD and verify again`);
|
||||
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() } };
|
||||
}
|
||||
@@ -153,6 +160,23 @@ export function receiptTargetedLine(required: readonly TargetedClass[], r: ShipR
|
||||
|
||||
export interface DepthVerdict { ok: boolean; line: string; message?: string }
|
||||
|
||||
/**
|
||||
* The classes the head needs against the classes a done targeted pass covered; a required class left uncovered (no pass,
|
||||
* a failed or unrun pass, or a pass on other classes) is refused (GST-966). A receipt with neither gstack_version nor
|
||||
* review_depth predates targeted receipts, as receiptDepthVerdict treats it, and is accepted as legacy.
|
||||
*/
|
||||
export function receiptTargetedVerdict(required: readonly TargetedClass[], r: ShipReceipt): DepthVerdict {
|
||||
const rt = r.review_targeted;
|
||||
const covered: readonly string[] = rt?.status === 'done' ? rt.classes : [];
|
||||
const missing = required.filter(c => !covered.includes(c));
|
||||
const verdict = !missing.length ? 'ok' : rt?.status === 'failed' ? 'failed' : 'missing';
|
||||
const line = `REVIEW_TARGETED: required=${classList(required)} receipt=${rt ? classList(covered) : 'legacy'} verdict=${verdict}`;
|
||||
if (!missing.length) return { ok: true, line };
|
||||
if (!rt && !r.review_depth && !r.gstack_version) return { ok: true, line: `${line} (legacy receipt, written before receipts recorded review_targeted; accepted)` };
|
||||
const why = !rt ? 'records no targeted pass' : rt.status === 'failed' ? 'records that its targeted pass failed' : rt.status === 'not-run' ? 'records that its targeted pass did not run' : `covers ${classList(covered)} only`;
|
||||
return { ok: false, line, message: `the head requires the targeted reader on ${missing.join(', ')}, but the receipt ${why}` };
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
@@ -206,3 +230,9 @@ export function bindShippedReceipt(root: string, baseRef: string, body: string,
|
||||
return { ...b, depthLine: d.line, targetedLine };
|
||||
}
|
||||
|
||||
/** receiptTargetedVerdict at the receipt's head, with the classes the queue computes: what write and carry check before they publish (#3854). */
|
||||
export function receiptTargetedAtHead(root: string, baseRef: string, r: ShipReceipt, head: string): DepthVerdict {
|
||||
const at = headInputs(root, baseRef, head);
|
||||
const gateTagged = boundScan(r, head) ?? (gateScanNeeded(at.c.paths, at.c.root) ? null : 0);
|
||||
return receiptTargetedVerdict(targetedClasses({ ...at.c, scope: at.scope, gateTagged }).classes, r);
|
||||
}
|
||||
+5
-5
@@ -89,8 +89,8 @@ Step 8.1, 9.2.1's probes, 8.2 (`Scope Check:`) on the todos and commits files, t
|
||||
one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or
|
||||
`scoped`: one program runs the failing test, fix, `--tests-only` and Phase 3, each gated (`diffBase` RANGE_BASE on `full` only; scoped fix-check: Step 3.5); `fix-check`: the
|
||||
pass with `fixCheck: {fix_list, hunks, tests, callers}`; `done`: that program runs Step 11 from
|
||||
`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (neither
|
||||
prints `ADVERSARIAL: skipped (review plan)`).
|
||||
`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (none:
|
||||
`ADVERSARIAL: skipped (review plan)`).
|
||||
|
||||
## 5. Voices, bind and version: Steps 11, 11.5, 12
|
||||
|
||||
@@ -102,7 +102,7 @@ approvals; `VERSION: skipped|kept`: no write.
|
||||
|
||||
## 6. Notes and documentation: Steps 13, 14, 14.5
|
||||
|
||||
Step 13: one user-facing entry from the commits file, where `NEXT:` says;
|
||||
Step 13: one user-facing entry from the commits file where `NEXT:` says;
|
||||
`gstack-prose-lint <it> --base origin/<base> --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and
|
||||
completions, edited from the todos file. Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as
|
||||
one program (above), then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`.
|
||||
@@ -119,8 +119,8 @@ STALE or MISSING reruns once), `PREGATE:`; print them, the last `Gate edits:` an
|
||||
## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22
|
||||
|
||||
- Step 18: the open-PR lookup; a title file starting `v<NEW_VERSION> `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H.
|
||||
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
|
||||
- Step 20: the metrics row via `gstack-review-log`.
|
||||
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> [--review-targeted @<f>] --body <body>`; `gstack-post pr-body` and `pr-title` (new PR: host PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
|
||||
- Step 20: a metrics row via `gstack-review-log`.
|
||||
- Step 22: `gstack-ship-budget close --reading R --credits C` (report `BUDGET_FINAL:`, `BUDGET_TIMELINE:` (tokens, credits), any `METER:` line), then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
|
||||
|
||||
## Where each STOP sends you (paths under `<root>/ship/`)
|
||||
|
||||
+5
-5
@@ -87,8 +87,8 @@ Step 8.1, 9.2.1's probes, 8.2 (`Scope Check:`) on the todos and commits files, t
|
||||
one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or
|
||||
`scoped`: one program runs the failing test, fix, `--tests-only` and Phase 3, each gated (`diffBase` RANGE_BASE on `full` only; scoped fix-check: Step 3.5); `fix-check`: the
|
||||
pass with `fixCheck: {fix_list, hunks, tests, callers}`; `done`: that program runs Step 11 from
|
||||
`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (neither
|
||||
prints `ADVERSARIAL: skipped (review plan)`).
|
||||
`sections/adversarial.md` as written (above), only the voices the last full-pass plan lists (none:
|
||||
`ADVERSARIAL: skipped (review plan)`).
|
||||
|
||||
## 5. Voices, bind and version: Steps 11, 11.5, 12
|
||||
|
||||
@@ -100,7 +100,7 @@ approvals; `VERSION: skipped|kept`: no write.
|
||||
|
||||
## 6. Notes and documentation: Steps 13, 14, 14.5
|
||||
|
||||
Step 13: one user-facing entry from the commits file, where `NEXT:` says;
|
||||
Step 13: one user-facing entry from the commits file where `NEXT:` says;
|
||||
`gstack-prose-lint <it> --base origin/<base> --gate` until `PROSE_LINT: ok`. Step 14: TODOS.md deferrals and
|
||||
completions, edited from the todos file. Step 14.5, every ship: read `sections/documentation.md` and run its audit as written, as
|
||||
one program (above), then `gstack-ship-receipt docs-caps --base <base>`; print `Documentation:`.
|
||||
@@ -117,8 +117,8 @@ STALE or MISSING reruns once), `PREGATE:`; print them, the last `Gate edits:` an
|
||||
## 8. Publish: Steps 18, 19, 19-C, 20, 21, 22
|
||||
|
||||
- Step 18: the open-PR lookup; a title file starting `v<NEW_VERSION> `. `UNATTENDED_SHIP:` with no open PR at HEAD takes Step 18-H.
|
||||
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> --body <body>`; `gstack-post pr-body` and `pr-title` (a new PR via the host's PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
|
||||
- Step 20: the metrics row via `gstack-review-log`.
|
||||
- Step 19 body: the receipt block, then the sections `sections/pr-body.md` lists (Pre-Landing Review: `REVIEW_LOOP:`, `Gate edits:`, deferred, notes), led under `CAPY_PR: on` (Step 19-C) by What changed, Why, Blast radius and Approvals (`gstack-approvals render`). `gstack-prose-lint <top> --sections 'Why,Summary,Blast radius' --gate`; `gstack-issue-links`; `gstack-review-scope summary --since "$REVIEW_SINCE" --run <dir> && gstack-ship-receipt write --pr <n> --base <base> --gate @<f> --pregate <f> --issues @<f> --review-loop @<f> --docs @<f> [--review-targeted @<f>] --body <body>`; `gstack-post pr-body` and `pr-title` (new PR: host PR tool; draft while a gate finding is `open`). `SHIP_RECEIPT:` opens your report.
|
||||
- Step 20: a metrics row via `gstack-review-log`.
|
||||
- Step 22: `gstack-ship-budget close --reading R --credits C` (report `BUDGET_FINAL:`, `BUDGET_TIMELINE:` (tokens, credits), any `METER:` line), then `gstack-ship-receipt result --pr <n>`: `GSTACK_RESULT: skill=ship status=complete run=<dir>`. Under a merge queue, `gstack-ship-queue ready --pr <n> [--screen|--no-screen]` at the pushed head; paste its output.
|
||||
|
||||
## Where each STOP sends you (paths under `<root>/ship/`)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
`DISPATCH: outside-voice`. Without the native line (light depth at runtime 0;
|
||||
above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`;
|
||||
with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader
|
||||
(`gstack-review-dispatch render --role targeted --classes <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,
|
||||
(`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 .gstack/tmp/targeted.json` 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.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
`DISPATCH: outside-voice`. Without the native line (light depth at runtime 0;
|
||||
above 0 the plan lists it beside the batched specialist), print `ADVERSARIAL: skipped (review plan)`;
|
||||
with neither line, take decision 3 of Finish the adversarial phase. On `DISPATCH: targeted`, also run the targeted reader
|
||||
(`gstack-review-dispatch render --role targeted --classes <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,
|
||||
(`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 .gstack/tmp/targeted.json` 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.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ unattended run may do is docs/unattended.md#what-an-unattended-session-may-do.
|
||||
~/.claude/skills/gstack/bin/gstack-issue-links --base <base> --out .gstack/tmp/issues.json
|
||||
~/.claude/skills/gstack/bin/gstack-ship-receipt write --base <base> --gate @.gstack/tmp/ship-gate.json --issues @.gstack/tmp/issues.json --out .gstack/tmp/ship-receipt.md
|
||||
```
|
||||
When Step 11 saved `.gstack/tmp/targeted.json`, add `--review-targeted @.gstack/tmp/targeted.json` to that write.
|
||||
2. Send the completion message, identifiers first, in this order: the `SHIP_RECEIPT:`
|
||||
line and receipt block; `branch:` and `sha:` (`git rev-parse HEAD`),
|
||||
pushed or `not pushed`; `inputs:` the plan or issue, the base SHA and the spend
|
||||
|
||||
@@ -11,6 +11,7 @@ unattended run may do is docs/unattended.md#what-an-unattended-session-may-do.
|
||||
~/.claude/skills/gstack/bin/gstack-issue-links --base <base> --out .gstack/tmp/issues.json
|
||||
~/.claude/skills/gstack/bin/gstack-ship-receipt write --base <base> --gate @.gstack/tmp/ship-gate.json --issues @.gstack/tmp/issues.json --out .gstack/tmp/ship-receipt.md
|
||||
```
|
||||
When Step 11 saved `.gstack/tmp/targeted.json`, add `--review-targeted @.gstack/tmp/targeted.json` to that write.
|
||||
2. Send the completion message, identifiers first, in this order: the `SHIP_RECEIPT:`
|
||||
line and receipt block; `branch:` and `sha:` (`git rev-parse HEAD`),
|
||||
pushed or `not pushed`; `inputs:` the plan or issue, the base SHA and the spend
|
||||
|
||||
@@ -221,7 +221,7 @@ per-lane counts (`{"<lane>":{"pass":N,"fail":N,"skip":N}}`) to
|
||||
```bash
|
||||
rm -f .gstack/tmp/review-loop.json
|
||||
~/.claude/skills/gstack/bin/gstack-review-scope summary --since "$REVIEW_SINCE" --no-ledger --out .gstack/tmp/review-loop.json &&
|
||||
~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr <pr-number> --base <base> --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json --body "$PR_BODY_FILE"
|
||||
~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr <pr-number> --base <base> --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json $([ -s .gstack/tmp/targeted.json ] && echo --review-targeted @.gstack/tmp/targeted.json) --body "$PR_BODY_FILE"
|
||||
```
|
||||
|
||||
A Step 5 red-base pause adds `--base-red <pause file>` (a conditional receipt the queue
|
||||
|
||||
@@ -204,7 +204,7 @@ per-lane counts (`{"<lane>":{"pass":N,"fail":N,"skip":N}}`) to
|
||||
```bash
|
||||
rm -f .gstack/tmp/review-loop.json
|
||||
~/.claude/skills/gstack/bin/gstack-review-scope summary --since "$REVIEW_SINCE" --no-ledger --out .gstack/tmp/review-loop.json &&
|
||||
~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr <pr-number> --base <base> --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json --body "$PR_BODY_FILE"
|
||||
~/.claude/skills/gstack/bin/gstack-ship-receipt write --pr <pr-number> --base <base> --gate @.gstack/tmp/ship-gate.json --pregate .gstack/tmp/pregate.json --issues @.gstack/tmp/issues.json --review-loop @.gstack/tmp/review-loop.json --docs @.gstack/tmp/docs-audit.json $([ -s .gstack/tmp/targeted.json ] && echo --review-targeted @.gstack/tmp/targeted.json) --body "$PR_BODY_FILE"
|
||||
```
|
||||
|
||||
A Step 5 red-base pause adds `--base-red <pause file>` (a conditional receipt the queue
|
||||
|
||||
@@ -270,10 +270,10 @@ describe('GST-986 item 5: fix-check passes and their findings count from the led
|
||||
expect(s.unrecorded).toBeUndefined();
|
||||
});
|
||||
|
||||
// Value: protects=a fix-check with both a row and a ledger counts once; a blocker no later fix-check followed stays unrecorded,
|
||||
// and one a row deferred stays deferred; fails_when=the ledger count adds to the row count, or every ledger blocker turns fixed;
|
||||
// seam=gstack-review-scope summary --run
|
||||
test('a fix-check counted by row and ledger counts once; the last pass\'s blocker stays unrecorded; a deferred blocker is not fixed', () => {
|
||||
// Value: protects=a fix-check with both a row and a ledger counts once, and once every fix-check has its row the rows decide
|
||||
// dispositions (a blocker no row disposes stays unrecorded); fails_when=the ledger count adds to the row count, or a logged
|
||||
// fix-check turns undisposed blockers fixed; seam=gstack-review-scope summary --run
|
||||
test('a fix-check counted by row and ledger counts once; with its row logged, undisposed blockers stay unrecorded', () => {
|
||||
const repo = branchWith([
|
||||
review(0, 1, [{ fingerprint: 'lib/a.ts:5:logic', severity: 'CRITICAL', blocking: true, action: 'deferred', raised_by: 'red-team' }]),
|
||||
fixCheck(30, []),
|
||||
@@ -283,6 +283,6 @@ describe('GST-986 item 5: fix-check passes and their findings count from the led
|
||||
step('p1', [['red-team', '[CRITICAL] lib/a.ts:5 a deferred defect outside the diff\n[CRITICAL] lib/b.ts:9 a fixed defect']]);
|
||||
step('fc1', [['fix-check', '[CRITICAL] lib/c.ts:3 the fix broke the caller']]);
|
||||
const s = loop(summary(repo, '--run', run));
|
||||
expect(s).toMatchObject({ passes: 1, blocking_found: 3, deferred: 1, fixed: 1, fix_checks: 1, rounds: 2, unrecorded: 1 });
|
||||
expect(s).toMatchObject({ passes: 1, blocking_found: 3, deferred: 1, fixed: 0, fix_checks: 1, rounds: 2, unrecorded: 2 });
|
||||
});
|
||||
});
|
||||
@@ -166,7 +166,7 @@ describe('write and carry refuse a receipt the queue\'s targeted verdict refuses
|
||||
return { ...runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body], { GSTACK_STATE_ROOT: path.join(repo.root, 'state') }), body };
|
||||
};
|
||||
const uncovered = carry(versioned);
|
||||
expect(uncovered.status).toBe(EXIT.fail);
|
||||
expect(uncovered.status).toBe(EXIT.refused);
|
||||
expect(uncovered.err).toMatch(/requires the targeted reader on money, but the receipt records no targeted pass.*\(RECEIPT_CARRY_REFUSED/);
|
||||
expect(extractReceipt(fs.readFileSync(uncovered.body, 'utf8')).receipt!.head).toBe(head);
|
||||
const ok = carry(covered);
|
||||
@@ -189,7 +189,7 @@ describe('/ship\'s receipt writes pass the targeted record (item 3, #3853)', ()
|
||||
const optional = /\$\(\[ -s (\S+) \] && echo (--review-targeted @\S+)\)/.exec(writeLine);
|
||||
expect(optional?.slice(1)).toEqual([recordPath!, `--review-targeted @${recordPath}`]);
|
||||
expect(section('handoff')).toContain(`When Step 11 saved \`${recordPath}\`, add \`--review-targeted @${recordPath}\` to that write.`);
|
||||
expect(fs.readFileSync(path.join(ROOT, 'ship', 'DRIVER.md'), 'utf8')).toContain(`[--review-targeted @${recordPath}]`);
|
||||
expect(fs.readFileSync(path.join(ROOT, 'ship', 'DRIVER.md'), 'utf8')).toContain('[--review-targeted @<f>] --body <body>');
|
||||
|
||||
const { repo } = moneyBranch();
|
||||
const expand = () => spawnSync('bash', ['-c', `printf '%s\\n' ${optional![0]}`], { cwd: repo.work, encoding: 'utf8', timeout: 10_000 }).stdout.split('\n').filter(Boolean);
|
||||
|
||||
@@ -298,14 +298,14 @@ describe('the targeted role end to end on a fixture repository', () => {
|
||||
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');
|
||||
expect(JSON.parse(fs.readFileSync(record, 'utf8')).head).toBe(head);
|
||||
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');
|
||||
expect(runBin(repo, 'gstack-review-dispatch', ['verify', '--ledger', ledger]).out).toContain(`TARGETED_RECORD: classes=money status=done findings=1 brief=${targetedBriefSha().slice(0, 12)} rules=${TARGETED_RULES} read=${head.slice(0, 12)}`);
|
||||
const later = runBin(repo, 'gstack-ship-receipt', ['write', '--base', 'main', '--session-kind', 'unattended', '--review-targeted', `@${record}`, '--json']);
|
||||
expect(later.status).not.toBe(0);
|
||||
expect(later.err).toContain(`the targeted reader read ${head.slice(0, 12)}`);
|
||||
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']);
|
||||
@@ -316,8 +316,8 @@ describe('the targeted role end to end on a fixture repository', () => {
|
||||
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');
|
||||
expect(plain.status).not.toBe(0);
|
||||
expect(plain.err).toContain('REVIEW_TARGETED: required=money receipt=legacy verdict=missing');
|
||||
|
||||
const slug = runBin(repo, 'gstack-slug', ['--get', 'SLUG']).out.trim();
|
||||
const logDir = path.join(repo.home, 'projects', slug);
|
||||
@@ -342,8 +342,11 @@ describe('the targeted role end to end on a fixture repository', () => {
|
||||
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);
|
||||
const read = { ...record, head: 'c'.repeat(40) };
|
||||
const patchOf = (head: string) => (head === read.head ? 'p1' : undefined);
|
||||
expect(parseReviewTargeted(read, 'p1', patchOf)).toEqual({ ok: true, targeted: { ...record, classes: ['money', 'gate'], product_patch_id: 'p1' } as any });
|
||||
for (const bad of [{ ...read, classes: ['cost'] }, { ...read, status: 'skipped' }, { ...read, findings: -1 }, { ...read, brief_sha: 'abc' }, { ...read, extra: 1 }, record, { ...read, head: 'abc' }, null]) expect(parseReviewTargeted(bad, 'p1', patchOf).ok).toBe(false);
|
||||
expect(parseReviewTargeted(read, 'p2', patchOf)).toMatchObject({ ok: false, message: expect.stringContaining(`the targeted reader read ${'c'.repeat(12)}, whose product patch p1 is not this receipt's p2`) });
|
||||
});
|
||||
// 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)
|
||||
|
||||
Reference in new issue
Block a user