Skip to content
Merged
Show file tree
Hide file tree
Changes from 89 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
c72ba07
feat(review): borrowed-verification trio — test-plan check, base-tree…
wenshao Jul 31, 2026
c0481dd
fix(review): survive real runner output — ANSI-laced and trimmed-away…
wenshao Jul 31, 2026
5054121
fix(review): address the eight findings from live review of this branch
wenshao Jul 31, 2026
31658c3
Merge branch 'main' into feat/review-borrowed-verification
wenshao Jul 31, 2026
67ac8aa
Merge branch 'main' into feat/review-borrowed-verification
qwen-code-dev-bot Jul 31, 2026
7a33545
fix(cli): never score a hunk survived when its own test left the base…
qwen-code-dev-bot Jul 31, 2026
436809b
fix(review): silence-bias hardening from four live review rounds of t…
wenshao Jul 31, 2026
e896669
feat(review): four round-2 borrowings — measured failure attribution,…
wenshao Jul 31, 2026
c50547c
feat(review): render adjudication, workflow step extraction, three ve…
wenshao Jul 31, 2026
1cfff3e
feat(review): sibling-entrance discipline for the fixed verdict
wenshao Jul 31, 2026
efe9582
feat(review): three measured-verification lenses from live rounds 803…
wenshao Jul 31, 2026
15a4560
feat(review): carry the round ledger in the posted review body
wenshao Jul 31, 2026
6e0d50c
fix(review): address review feedback — false-positive hardening, bina…
qwen-code Jul 31, 2026
ec3fda8
fix(review): address review feedback — base-tree availability gate, t…
qwen-code Jul 31, 2026
eae6485
fix(review): address review feedback — diff-header false positives, s…
Jul 31, 2026
416622c
fix(review): address review feedback — cd-base exclusion, Test Files …
Aug 1, 2026
ce9d1ee
Merge branch 'main' into feat/review-borrowed-verification
qwen-code-dev-bot Aug 1, 2026
329415a
Merge remote-tracking branch 'origin/main' into feat/review-borrowed-…
wenshao Aug 1, 2026
b1a38ad
fix(review): port the collocated-dropout test to the post-#8050 runne…
wenshao Aug 1, 2026
9bb8d97
Merge branch 'feat/review-borrowed-verification' into feat/review-rou…
wenshao Aug 1, 2026
ddf227c
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
3008454
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
cdff569
fix(review): bound the summary rescue, apply the ATX heading rule, sw…
wenshao Aug 1, 2026
7860e18
Merge branch 'feat/review-borrowed-verification' into feat/review-rou…
wenshao Aug 1, 2026
b0c0a74
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
452bade
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
7c6b678
fix(review): EEXIST-only lock busy, bun test alias, chained cd bail, …
wenshao Aug 1, 2026
47ef86a
Merge branch 'feat/review-borrowed-verification' into feat/review-rou…
wenshao Aug 1, 2026
20899a6
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
058a3b5
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
5ec626f
Merge remote-tracking branch 'origin/main' into feat/review-round2-bo…
wenshao Aug 1, 2026
e915dc2
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
de522e0
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
554aac1
fix(review): make the ledger marker actually reach the posted body
wenshao Aug 1, 2026
f5d6c2b
fix(review): close the ten open findings on this PR
wenshao Aug 1, 2026
8579d3a
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
ccf3052
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
b792a0c
Merge branch 'main' into feat/review-round2-borrowings
wenshao Aug 1, 2026
f4b6d07
fix(review): the test helper's cwd parameter is required, not optional
wenshao Aug 1, 2026
db1b524
Merge remote-tracking branch 'origin/feat/review-round2-borrowings' i…
wenshao Aug 1, 2026
12a9eae
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
0caa34c
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
df4ea84
fix(review): a base rerun that could not RUN attributes nothing
wenshao Aug 1, 2026
3b317b4
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
4d9ccd0
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
1748fce
fix(review): extract-step resolves all three env/defaults levels, and…
wenshao Aug 1, 2026
ea084c3
fix(review): compare failing files by a normalised, project-keyed ide…
wenshao Aug 1, 2026
4b39b99
Merge branch 'feat/review-round2-borrowings' into feat/review-render-…
wenshao Aug 1, 2026
5cff748
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
0e5bd00
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
8835bf3
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
93e0ef2
fix(review): restore the whole-command budget, keep generics out of g…
wenshao Aug 1, 2026
2030d1e
fix(review): a budget-shortened deadline is not the same fact as a sl…
wenshao Aug 1, 2026
f4a13ec
fix(review): brace-tolerant stub list, pipefail fidelity, and extract…
wenshao Aug 1, 2026
98362d4
fix(review): restore the replacement sub-cap, stranded on a downstrea…
wenshao Aug 1, 2026
11bdcac
fix(review): one severity predicate, carried ids, and a lossless marker
wenshao Aug 1, 2026
3bbe6a0
docs(review): complete the "delta cannot rule" enumeration in both pl…
wenshao Aug 1, 2026
0e78efa
fix(review): the invokes list was mostly prose, not commands
wenshao Aug 1, 2026
e0747d1
docs(review): the rationale named only one of the ways base goes unme…
wenshao Aug 1, 2026
c777a36
fix(review): a quoted `<<EOF` is not a heredoc, and a continued line …
wenshao Aug 1, 2026
12c2f62
Merge remote-tracking branch 'origin/feat/review-round2-borrowings' i…
wenshao Aug 1, 2026
2721a3a
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
d16bf28
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
82fd3b6
fix(review): case labels, a second heredoc, and an expression in comm…
wenshao Aug 1, 2026
56652fd
fix(review): annotate the continuation accumulator so tsc can type it
wenshao Aug 1, 2026
0d97c3c
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
4d585d3
fix(review): a file-count label stops counting at the end of its line
wenshao Aug 1, 2026
6c474d0
Merge remote-tracking branch 'origin/feat/review-round2-borrowings' i…
wenshao Aug 1, 2026
067c993
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
e07c954
fix(review): parse the base rerun before its output is trimmed
wenshao Aug 1, 2026
0f15b61
Merge remote-tracking branch 'origin/feat/review-round2-borrowings' i…
wenshao Aug 1, 2026
dff65f4
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
0af039e
test(review): pin the `sh` set-line to exactly what the runner uses
wenshao Aug 1, 2026
b5800c1
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
e7b3f67
fix(review): rerun only the command shapes build-test emits
wenshao Aug 1, 2026
b5d10f7
Merge remote-tracking branch 'origin/feat/review-round2-borrowings' i…
wenshao Aug 1, 2026
40afca9
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
7e33f06
Merge branch 'main' into feat/review-render-and-step-harness
wenshao Aug 1, 2026
f337980
Merge branch 'feat/review-render-and-step-harness' into feat/review-p…
wenshao Aug 1, 2026
06b7831
Merge remote-tracking branch 'origin/main' into feat/review-render-an…
wenshao Aug 1, 2026
b4ebd3b
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
4201e23
fix(review): the working directory is a setting the extraction was lo…
wenshao Aug 1, 2026
1eb300c
fix(review): $(( )) is arithmetic, and a heredoc's form decides where…
wenshao Aug 1, 2026
8acc1d2
fix(review): refuse an ambiguous step name, and report errors like th…
wenshao Aug 1, 2026
698d08d
docs(review): the brief's list of limits was one short
wenshao Aug 1, 2026
ffc71d9
chore(review): drop a scratch probe file that reached the branch
wenshao Aug 1, 2026
9251a5f
Merge remote-tracking branch 'origin/feat/review-render-and-step-harn…
wenshao Aug 1, 2026
81898af
Merge branch 'main' into feat/review-persistent-ledger
wenshao Aug 1, 2026
2ebb18c
fix(review): bound the whole ledger marker, not just each of its fields
wenshao Aug 1, 2026
051b204
fix(review): a partial ledger must not render as a complete one
wenshao Aug 1, 2026
e89508e
fix(review): an empty ledger title is a cap the next round cannot lift
wenshao Aug 1, 2026
24c6d3e
fix(review): count both caps as dropped, not just the byte one
wenshao Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions packages/cli/src/commands/review/compose-review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { promptRecordDir, briefPath } from './lib/prompt-record.js';
import { getGhHost, setGhHost } from './lib/gh.js';
import { parseLedger } from './lib/ledger.js';
import { countInlineFindings } from './lib/inline-counts.js';
import {
composeReview,
buildLedger,
scriptLintGate,
testPlanGate,
composeReviewCommand,
Expand Down Expand Up @@ -3122,3 +3125,174 @@ describe('testPlanGate — Test Plan rulings, disclosed but never capping', () =
expect(notes[5]).toBe('and 3 more');
});
});

describe('buildLedger', () => {
it('numbers findings round-scoped, inline first then body Criticals', () => {
const l = buildLedger(
3,
[
{
path: 'src/a.ts',
line: 12,
body: '**[Critical]**: double free\ndetail',
},
{ path: 'src/b.ts', line: 4, body: '**[Suggestion]** untested guard' },
{ path: 'src/c.ts', body: 'no marker — not a finding' },
],
['`src/d.ts` unanchorable blocker'],
);
expect(l.round).toBe(3);
expect(l.findings).toEqual([
{
id: 'R3-1',
sev: 'C',
file: 'src/a.ts',
line: 12,
title: 'double free',
},
{
id: 'R3-2',
sev: 'S',
file: 'src/b.ts',
line: 4,
title: 'untested guard',
},
{
id: 'R3-3',
sev: 'C',
file: '(body)',
title: '`src/d.ts` unanchorable blocker',
},
]);
});

it('classifies through `severityOf`, whitespace and all', () => {
// The ledger restated the severity predicate as a bare `startsWith`, while
// `countInlineFindings` — the count the VERDICT is computed from — trims
// first. A Critical whose body opened with a newline was therefore counted,
// posted, blocked the merge, and was silently missing from the ledger,
// shifting the id of every finding after it.
const drafted = [
{ path: 'src/a.ts', line: 1, body: '\n **[Critical]** leading space' },
{ path: 'src/b.ts', line: 2, body: '**[Suggestion]** plain' },
];
expect(countInlineFindings(drafted)).toEqual({
criticalsInline: 1,
suggestionsInline: 1,
});
expect(buildLedger(1, drafted, []).findings).toEqual([
{
id: 'R1-1',
sev: 'C',
file: 'src/a.ts',
line: 1,
title: 'leading space',
},
{ id: 'R1-2', sev: 'S', file: 'src/b.ts', line: 2, title: 'plain' },
]);
});

it('keeps a carried-forward id instead of renumbering it by position', () => {
// Step 6 re-reports a still-standing finding under its ORIGINAL id, so the
// report says `R1-2 still stands` — and a ledger that renumbered it `R3-1`
// handed the next round a work list keyed by ids the report never used,
// which is the whole thing `R1-2 names the same claim every round` promised.
const l = buildLedger(
3,
[
{ path: 'a.ts', line: 4, body: '**[Critical]** R1-2: still leaking' },
{ path: 'b.ts', body: '**[Suggestion]** brand new this round' },
{ path: 'c.ts', body: '**[Critical]** R2-1 — moved but the same' },
],
['R1-5) the unanchorable one, still open'],
);
expect(l.findings.map((f) => `${f.id}|${f.title}`)).toEqual([
'R1-2|still leaking',
'R3-1|brand new this round',
'R2-1|— moved but the same',
'R1-5|the unanchorable one, still open',
]);
});

it('never issues one id twice, however the comments are worded', () => {
// A duplicated carried id (a copy-paste, or a title that merely opens like
// one) must not collapse two claims onto one ledger entry.
const l = buildLedger(
2,
[
{ path: 'a.ts', body: '**[Critical]** R1-1: one' },
{ path: 'b.ts', body: '**[Critical]** R1-1: two, same id' },
],
[],
);
expect(l.findings.map((f) => f.id)).toEqual(['R1-1', 'R2-1']);
});
});

describe('the ledger marker reaches the POSTED body', () => {
// The feature was inert end to end: the marker was appended in the CLI
// handler, after composeReview() returned, so it reached only the composed
// JSON on disk — and `submit` posts what the PURE function returns. Every
// assertion here goes through composeReview, the path GitHub receives.
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'ledger-e2e-'));
});
afterEach(() => rmSync(dir, { recursive: true, force: true }));

const plan = (over: Record<string, unknown> = {}) => {
const p = join(dir, 'plan.json');
writeFileSync(p, JSON.stringify({ prNumber: 8255, ...over }));
return p;
};

it('appends the marker to the body composeReview returns', () => {
const r = composeReview({
planPath: plan(),
modelId: 'm',
criticalsInline: 0,
suggestionsInline: 0,
draftedComments: [
{ path: 'src/a.ts', line: 3, body: '**[Suggestion]** untested guard' },
],
});
expect(r.body).toContain('<!-- qwen-review-ledger ');
const ledger = parseLedger(r.body)!;
expect(ledger.round).toBe(1);
expect(ledger.findings).toEqual([
{
id: 'R1-1',
sev: 'S',
file: 'src/a.ts',
line: 3,
title: 'untested guard',
},
]);
});

it('counts the round from the side file pr-context recovered, +1', () => {
writeFileSync(
join(dir, 'qwen-review-pr-8255-prev-ledger.json'),
JSON.stringify({ v: 1, round: 4, findings: [] }),
);
const r = composeReview({
planPath: plan(),
modelId: 'm',
criticalsInline: 0,
suggestionsInline: 0,
draftedComments: [{ path: 'a.ts', body: '**[Critical]** boom' }],
});
expect(parseLedger(r.body)?.round).toBe(5);
});

it('carries NO marker on a local review — there is no PR to hold it', () => {
const r = composeReview({
planPath: plan({ prNumber: undefined }),
modelId: 'm',
criticalsInline: 0,
suggestionsInline: 0,
draftedComments: [{ path: 'a.ts', body: '**[Critical]** boom' }],
});
expect(r.body).not.toContain('qwen-review-ledger');
});
});
161 changes: 161 additions & 0 deletions packages/cli/src/commands/review/compose-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ import {
} from './lib/roster.js';
import { diffHashOf, type ScriptLintReport } from './script-lint.js';
import type { TestPlanReport } from './test-plan.js';
import {
serializeLedger,
type Ledger,
type LedgerFinding,
} from './lib/ledger.js';
import {
CRITICAL_PREFIX,
SUGGESTION_PREFIX,
countInlineFindings,
severityOf,
unmarkedComments,
type DraftedComment,
} from './lib/inline-counts.js';
Expand Down Expand Up @@ -144,6 +150,13 @@ export interface ComposeReviewInput {
downgradeRequestChanges?: boolean;
downgradeReasons?: string[];
};
/**
* The drafted inline comments this review is posting — the ledger's own
* input. A seam like `criticalsInline`, filled by the two CLI boundaries
* from the same array they count, never by the model's state JSON (the
* handler strips it, as it does `env` and `prBodyFetcher`).
*/
draftedComments?: Array<{ path?: unknown; line?: unknown; body?: unknown }>;
/** Model id for the footer, e.g. `qwen3.7-max`. */
modelId: string;
}
Expand Down Expand Up @@ -238,6 +251,69 @@ function toBool(value: unknown, field: string): boolean {
export function composeReview(
input: ComposeReviewInput,
cliVersion = 'unknown',
): ComposeReviewResult {
const result = composeReviewBody(input, cliVersion);
// The ledger marker rides the body THIS function returns, because this — not
// the CLI handler — is what `submit` calls and posts. Appending it in the
// handler left the feature inert end to end: the marker reached only the
// composed JSON on disk, which nothing in the posting path reads, so no
// posted review ever carried one and every round recovered `null`.
const marker = ledgerMarkerFor(input);
return marker ? { ...result, body: `${result.body}\n\n${marker}` } : result;
}

/**
* The next round's marker, or null when this review has no PR to carry one.
* Round number comes from the side file `pr-context` wrote from the PREVIOUS
* posted round (+1) — never from the model, never from this input.
*/
function ledgerMarkerFor(input: ComposeReviewInput): string | null {
try {
if (!input.planPath) return null;
const plan = JSON.parse(readFileSync(input.planPath, 'utf8')) as {
prNumber?: unknown;
};
const pr = plan?.prNumber;
const isPr =
(typeof pr === 'number' && Number.isInteger(pr) && pr > 0) ||
(typeof pr === 'string' && /^\d+$/.test(pr));
if (!isPr) return null;
let prevRound = 0;
try {
const prev = JSON.parse(
readFileSync(
join(
dirname(input.planPath),
`qwen-review-pr-${pr}-prev-ledger.json`,
),
'utf8',
),
) as Ledger;
if (Number.isInteger(prev.round) && prev.round > 0)
prevRound = prev.round;
} catch {
// No previous posted round recovered: this is round 1.
}
return serializeLedger(
buildLedger(
prevRound + 1,
(input.draftedComments ?? []) as Array<{
path?: unknown;
line?: unknown;
body?: unknown;
}>,
toStringList(input.bodyCriticals, 'bodyCriticals'),
),
);
} catch {
// A carry-forward convenience, never worth failing the verdict over.
return null;
}
}

function composeReviewBody(
input: ComposeReviewInput,
cliVersion: string,
): ComposeReviewResult {
const criticalsInline = toCount(input.criticalsInline, 'criticalsInline');
const suggestionsInline = toCount(
Expand Down Expand Up @@ -1622,6 +1698,9 @@ export const composeReviewCommand: CommandModule = {
// fail-safe. Stripping it here keeps the register the CLI's own, not the
// caller's, which is the whole point of the seam.
delete parsed.prBodyFetcher;
// Same reasoning: the ledger's contents are the comments this run drafted,
// read from `--comments` below — not something a state JSON may assert.
delete parsed.draftedComments;
// The inline counts are counted, not accepted — `submit` has refused them
// since the count-beside-the-comments bug, and this boundary refusing them
// too is what makes the Step 6 line and the posted verdict the same
Expand All @@ -1644,6 +1723,7 @@ export const composeReviewCommand: CommandModule = {
{
...parsed,
...countInlineFindings(drafted),
draftedComments: drafted,
},
await getCliVersion(),
);
Expand Down Expand Up @@ -1680,6 +1760,87 @@ export const composeReviewCommand: CommandModule = {
},
};

/**
* A carried-forward finding names its ORIGINAL id right after the severity
* marker — `**[Critical]** R1-2: the same claim, re-reported`. Step 6 already
* mandates re-reporting a still-standing entry under the id it has; reading
* that id back here is what makes the machine ledger agree with the report it
* rides in, instead of renumbering the entry to a fresh `R<round>-<n>` the
* report never used.
*/
const CARRIED_ID_RE = /^(R\d+-\d+)[:.)\]]?(?=\s|$)\s*/;

/**
* The next round's ledger: every finding this review is posting as its own —
* the drafted inline comments plus the body Criticals. Low-confidence findings
* never reach either input (they are terminal-only), so the ledger holds only
* claims the review stands behind, which is what the next round re-asserts.
*/
export function buildLedger(
round: number,
drafted: Array<{ path?: unknown; line?: unknown; body?: unknown }>,
bodyCriticals: string[],
): Ledger {
const findings: LedgerFinding[] = [];
const taken = new Set<string>();
let next = 0;
/** A carried id if it is free, else the next unused id of THIS round. */
const idFor = (carried: string | undefined): string => {
if (carried && !taken.has(carried)) {
taken.add(carried);
return carried;
}
let id: string;
do {
id = `R${round}-${++next}`;
} while (taken.has(id));
taken.add(id);
return id;
};
/** The first line of what follows the severity marker, minus any carried id. */
const titleOf = (rest: string): { id?: string; title: string } => {
const line = rest.split('\n')[0].trim();
const carried = CARRIED_ID_RE.exec(line);
return {
id: carried?.[1],
title: (carried ? line.slice(carried[0].length) : line).trim(),
};
};

for (const c of drafted) {
// ONE severity predicate for the whole package. `severityOf` trims leading
// whitespace before matching, and it is what `countInlineFindings` — the
// count the verdict is computed from — and the unmarked-comment gate both
// use. A second `startsWith` here disagreed on exactly that whitespace: a
// Critical whose body opened with a newline was counted, was posted, and
// was silently absent from the ledger, shifting every id after it.
const sev = severityOf(c);
if (!sev) continue;
const marker = sev === 'critical' ? CRITICAL_PREFIX : SUGGESTION_PREFIX;
const body = (typeof c.body === 'string' ? c.body : '').trimStart();
const { id: carried, title } = titleOf(
body.slice(marker.length).replace(/^:?\s*/, ''),
);
findings.push({
id: idFor(carried),
sev: sev === 'critical' ? 'C' : 'S',
file: typeof c.path === 'string' ? c.path : '(unknown)',
...(typeof c.line === 'number' ? { line: c.line } : {}),
title,
});
}
for (const b of bodyCriticals) {
const { id: carried, title } = titleOf(b);
findings.push({
id: idFor(carried),
sev: 'C',
file: '(body)',
title,
});
}
return { v: 1, round, findings };
}

/** The terminal verdict, in the words Step 6 is told to print. */
export function verdictLine(r: ComposeReviewResult): string {
const label: Record<ReviewEvent, string> = {
Expand Down
Loading
Loading