Skip to content

Commit 7c499d1

Browse files
committed
fix(review): a heavy file in a Step-3A diff must not demand invariant agents
From a human review of QwenLM#6892 (doudouOUC). `heavy` is decided independently of topology (lib/heavy.ts): a ~300-line source file with ~120 changed lines clears the rewrite-ratio branch while srcDiffLines stays under 500 — a Step 3A review. The invariant-agent loop in requiredAgents ran in both topologies, so it added invariant-a/b/c to the roster of a 3A review that never launches them; check-coverage then reported them as missingRoles and exit-3'd, and compose-review capped the verdict — an otherwise-complete small PR, falsely blocked. Gate the loop on isTerritoryFanOut. Step 3A's dimension agents each walk the whole diff, so one already sees both ends of a rewritten file; invariant agents are a 3B mechanism for when the diff is carved into territories and no single agent holds the whole file. roster.test.ts now pins the 3A-heavy case. Also, same review: merge() in coverage.ts copied its first tuple and pushes copies, so it no longer mutates a tuple owned by rec.diffReads (harmless today, pure now).
1 parent c2cb5b6 commit 7c499d1

3 files changed

Lines changed: 50 additions & 5 deletions

File tree

packages/cli/src/commands/review/lib/coverage.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,12 @@ function pointedAt(prompt: string, plan: Plan): Array<[number, number]> {
215215
function merge(ranges: Array<[number, number]>): Array<[number, number]> {
216216
if (ranges.length < 2) return ranges;
217217
const sorted = [...ranges].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
218-
const out: Array<[number, number]> = [sorted[0]];
218+
// Start with a COPY of the first tuple, and push copies. `sorted` shares its
219+
// element references with the caller's array — which includes `rec.diffReads` —
220+
// so writing `last[1] = …` below would mutate a tuple the record owns. Harmless
221+
// today (the record is not read again after this), but a pure function here is
222+
// one fewer latent foot-gun for the next caller.
223+
const out: Array<[number, number]> = [[...sorted[0]]];
219224
for (const [s, e] of sorted.slice(1)) {
220225
const last = out[out.length - 1];
221226
// `s <= last[1] + 1` — abutting counts. Lines 1-200 then 201-400 is one walk of

packages/cli/src/commands/review/lib/roster.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,32 @@ describe('requiredAgents — Step 3B', () => {
174174
expect(keys(heavy)).not.toContain('invariant-a--src/small.ts');
175175
});
176176
});
177+
178+
describe('a heavy file in a Step-3A-sized diff', () => {
179+
it('does NOT demand invariant agents — Step 3A never launches them', () => {
180+
// `heavy` is decided independently of topology: a 300-line source file with
181+
// ~120 changed lines clears the rewrite-ratio branch while `srcDiffLines` stays
182+
// under 500 — a Step 3A review. Requiring invariant agents there demanded agents
183+
// the review never launches, and `check-coverage` then exit-3'd an otherwise
184+
// complete small PR. (A real finding from a human review of this change.)
185+
const smallButHeavy = {
186+
...PR, // srcDiffLines 200, diffLines 300 → Step 3A
187+
files: [
188+
{
189+
path: 'src/rewritten.ts',
190+
kind: 'source',
191+
removedLines: 40,
192+
heavy: true,
193+
},
194+
],
195+
};
196+
expect(isTerritoryFanOut(smallButHeavy)).toBe(false);
197+
const k = keys(smallButHeavy);
198+
expect(k).not.toContain('invariant-a--src/rewritten.ts');
199+
expect(k).not.toContain('invariant-b--src/rewritten.ts');
200+
expect(k).not.toContain('invariant-c--src/rewritten.ts');
201+
// It is still a normal 3A review: the dimension agents each walk the whole diff,
202+
// and one that walks the whole diff already sees both ends of the file.
203+
expect(k).toEqual(expect.arrayContaining(['1a', '2', '6a']));
204+
});
205+
});

packages/cli/src/commands/review/lib/roster.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,21 @@ export function requiredAgents(plan: RosterPlan): RequiredAgent[] {
171171
// invariant sit two thousand lines apart. Three agents, one checklist slice each
172172
// — measured, one agent holding all eight checks found one of five defects and
173173
// the same model split three ways found all five.
174-
for (const file of heavyFiles(plan)) {
175-
add('invariant-a', file);
176-
add('invariant-b', file);
177-
add('invariant-c', file);
174+
//
175+
// **Step 3B only.** `heavy` is decided independently of topology (`lib/heavy.ts`):
176+
// a 300-line source file with ~120 changed lines clears the rewrite-ratio branch
177+
// while `srcDiffLines` stays under 500 — a Step 3A review. Step 3A launches no
178+
// invariant agents (its dimension agents each walk the whole diff, so they
179+
// already see both ends of a file), and the skill's 3A section never mentions
180+
// them. Requiring them there demanded agents the review was never meant to launch,
181+
// and `check-coverage` then exit-3'd an otherwise-complete small PR. Gate the loop
182+
// on the topology that actually runs them.
183+
if (isTerritoryFanOut(plan)) {
184+
for (const file of heavyFiles(plan)) {
185+
add('invariant-a', file);
186+
add('invariant-b', file);
187+
add('invariant-c', file);
188+
}
178189
}
179190

180191
return out;

0 commit comments

Comments
 (0)