Skip to content

Commit be133b0

Browse files
authored
fix(review): distinguish carried findings in severity gate (#34)
* fix(review): distinguish carried findings in severity gate * fix(review): guard provenance counters
1 parent 737b2b8 commit be133b0

7 files changed

Lines changed: 472 additions & 85 deletions

File tree

__tests__/unit/core/orchestrator.health.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ describe('ReviewOrchestrator health check guard rails', () => {
293293
expect(incrementalReviewer.saveReview).not.toHaveBeenCalled();
294294
expect(review.summary).toBe('Previous completed review');
295295
expect(review.metrics.cached).toBe(true);
296+
expect(review.findingProvenance).toEqual({
297+
fromCurrentReview: { critical: 0, major: 0, minor: 0 },
298+
carriedForward: { critical: 0, major: 1, minor: 0 },
299+
});
296300
expect(commentPoster.postSummary).toHaveBeenCalledWith(
297301
1,
298302
'Cached review',
@@ -301,6 +305,155 @@ describe('ReviewOrchestrator health check guard rails', () => {
301305
);
302306
});
303307

308+
it('marks unchanged-file findings as carried forward in a delta review', async () => {
309+
const carriedFinding: Finding = {
310+
file: 'src/unchanged.ts',
311+
line: 7,
312+
severity: 'major',
313+
title: 'Existing finding',
314+
message: 'Still applies to an unchanged file.',
315+
};
316+
const incrementalReviewer = {
317+
planReview: jest.fn().mockResolvedValue({
318+
mode: IncrementalReviewPlanMode.Delta,
319+
files: [],
320+
invalidatedPaths: [],
321+
lastReview: {
322+
prNumber: 1,
323+
lastReviewedCommit: 'previous-head',
324+
baseSha: 'b',
325+
timestamp: Date.now(),
326+
findings: [carriedFinding],
327+
reviewSummary: 'Previous review',
328+
},
329+
}),
330+
mergeFindings: jest.fn().mockReturnValue([carriedFinding]),
331+
generateIncrementalSummary: jest.fn().mockReturnValue('Delta review'),
332+
saveReview: jest.fn(),
333+
} as any;
334+
const orchestrator = makeOrchestrator({
335+
config: {
336+
...DEFAULT_CONFIG,
337+
dryRun: true,
338+
enableCaching: false,
339+
analyticsEnabled: false,
340+
graphEnabled: false,
341+
skipTrivialChanges: false,
342+
incrementalEnabled: true,
343+
providers: [],
344+
fallbackProviders: [],
345+
},
346+
incrementalReviewer,
347+
});
348+
349+
const review = await orchestrator.executeReview(
350+
makePR([
351+
{
352+
filename: 'docs/update.md',
353+
status: 'modified',
354+
additions: 1,
355+
deletions: 0,
356+
changes: 1,
357+
},
358+
])
359+
);
360+
361+
expect(review.findingProvenance).toEqual({
362+
fromCurrentReview: { critical: 0, major: 0, minor: 0 },
363+
carriedForward: { critical: 0, major: 1, minor: 0 },
364+
});
365+
expect(incrementalReviewer.mergeFindings).toHaveBeenCalledWith(
366+
[carriedFinding],
367+
[],
368+
[],
369+
[]
370+
);
371+
});
372+
373+
it('preserves finding provenance when an incremental adapter clones findings', async () => {
374+
const changedFile: FileChange = {
375+
filename: 'src/changed.ts',
376+
status: 'modified',
377+
additions: 1,
378+
deletions: 0,
379+
changes: 1,
380+
};
381+
const carriedFinding: Finding = {
382+
file: 'src/unchanged.ts',
383+
line: 7,
384+
severity: 'critical',
385+
title: 'Existing finding',
386+
message: 'Still applies to an unchanged file.',
387+
};
388+
const currentFinding: Finding = {
389+
file: changedFile.filename,
390+
line: 1,
391+
severity: 'major',
392+
title: 'Current finding',
393+
message: 'Produced while reviewing the changed file.',
394+
};
395+
const incrementalReviewer = {
396+
planReview: jest.fn().mockResolvedValue({
397+
mode: IncrementalReviewPlanMode.Delta,
398+
files: [changedFile],
399+
invalidatedPaths: [],
400+
lastReview: {
401+
prNumber: 1,
402+
lastReviewedCommit: 'previous-head',
403+
baseSha: 'b',
404+
timestamp: Date.now(),
405+
findings: [carriedFinding],
406+
reviewSummary: 'Previous review',
407+
},
408+
}),
409+
mergeFindings: jest
410+
.fn()
411+
.mockImplementation((previous: Finding[], current: Finding[]) =>
412+
JSON.parse(JSON.stringify([...previous, ...current]))
413+
),
414+
generateIncrementalSummary: jest.fn().mockReturnValue('Delta review'),
415+
saveReview: jest.fn(),
416+
} as any;
417+
const orchestrator = makeOrchestrator({
418+
config: {
419+
...DEFAULT_CONFIG,
420+
dryRun: true,
421+
enableCaching: false,
422+
analyticsEnabled: false,
423+
graphEnabled: false,
424+
skipTrivialChanges: false,
425+
incrementalEnabled: true,
426+
providers: [],
427+
fallbackProviders: [],
428+
},
429+
incrementalReviewer,
430+
providerRegistry: {
431+
createProviders: jest.fn().mockResolvedValue([]),
432+
discoverAdditionalFreeProviders: jest.fn().mockResolvedValue([]),
433+
} as any,
434+
llmExecutor: {
435+
filterHealthyProviders: jest.fn().mockResolvedValue({
436+
healthy: [],
437+
healthCheckResults: [],
438+
}),
439+
execute: jest.fn(),
440+
} as any,
441+
synthesis: {
442+
synthesize: jest.fn().mockReturnValue({
443+
...emptyReview,
444+
findings: [currentFinding],
445+
}),
446+
} as any,
447+
});
448+
449+
const review = await orchestrator.executeReview(makePR([changedFile]));
450+
451+
expect(review.findingProvenance).toEqual({
452+
fromCurrentReview: { critical: 0, major: 1, minor: 0 },
453+
carriedForward: { critical: 1, major: 0, minor: 0 },
454+
});
455+
});
456+
304457
it('detects a trivial PR before building its code graph', async () => {
305458
const graphBuilder = {
306459
buildGraph: jest.fn(),

__tests__/unit/output/severity-gate.test.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Finding,
77
LifecycleThreadRecord,
88
Review,
9+
ReviewFindingProvenance,
910
Severity,
1011
} from '../../../src/types';
1112

@@ -30,10 +31,12 @@ const previousThread = (
3031

3132
const review = (
3233
findings: Finding[],
33-
previousStillValid: LifecycleThreadRecord[] = []
34+
previousStillValid: LifecycleThreadRecord[] = [],
35+
findingProvenance?: ReviewFindingProvenance
3436
): Review =>
3537
({
3638
findings,
39+
findingProvenance,
3740
threadLifecycle: {
3841
previousStillValid,
3942
},
@@ -47,7 +50,7 @@ describe('severity gate formatting', () => {
4750
);
4851

4952
expect(result).toBe(
50-
'ReviewRouter found 1 blocking major+ finding: 1 new current major+ finding. Review comments were posted before failing this check.'
53+
'ReviewRouter found 1 blocking major+ finding: 1 major+ finding produced by this review. Review comments were posted before failing this check.'
5154
);
5255
});
5356

@@ -56,11 +59,37 @@ describe('severity gate formatting', () => {
5659

5760
expect(getBlockingFindingBreakdown(input, 'major')).toEqual({
5861
current: 0,
62+
fromCurrentReview: 0,
63+
carriedForward: 0,
64+
unclassifiedCurrent: 0,
5965
previousStillValid: 1,
6066
total: 1,
6167
});
6268
expect(formatBlockingFindingFailure(input, 'major')).toBe(
63-
'ReviewRouter found 1 blocking major+ finding: 1 previous unresolved major+ finding still valid. No new current major+ findings were kept after filtering. Review comments were posted before failing this check.'
69+
'ReviewRouter found 1 blocking major+ finding: 1 previous unresolved major+ finding still valid. No major+ findings were produced by this review. Review comments were posted before failing this check.'
70+
);
71+
});
72+
73+
it('reports carried-forward findings without calling them new', () => {
74+
const input = review(
75+
[finding('major'), finding('major'), finding('major')],
76+
[],
77+
{
78+
fromCurrentReview: { critical: 0, major: 0, minor: 0 },
79+
carriedForward: { critical: 0, major: 3, minor: 0 },
80+
}
81+
);
82+
83+
expect(getBlockingFindingBreakdown(input, 'major')).toEqual({
84+
current: 3,
85+
fromCurrentReview: 0,
86+
carriedForward: 3,
87+
unclassifiedCurrent: 0,
88+
previousStillValid: 0,
89+
total: 3,
90+
});
91+
expect(formatBlockingFindingFailure(input, 'major')).toBe(
92+
'ReviewRouter found 3 blocking major+ findings: 3 carried-forward major+ findings from unchanged files. No major+ findings were produced by this review. Review comments were posted before failing this check.'
6493
);
6594
});
6695

@@ -71,7 +100,7 @@ describe('severity gate formatting', () => {
71100
);
72101

73102
expect(result).toBe(
74-
'ReviewRouter found 2 blocking major+ findings: 1 new current major+ finding and 1 previous unresolved major+ finding still valid. Review comments were posted before failing this check.'
103+
'ReviewRouter found 2 blocking major+ findings: 1 major+ finding produced by this review and 1 previous unresolved major+ finding still valid. Review comments were posted before failing this check.'
75104
);
76105
});
77106

@@ -83,6 +112,9 @@ describe('severity gate formatting', () => {
83112

84113
expect(getBlockingFindingBreakdown(input, 'major')).toEqual({
85114
current: 1,
115+
fromCurrentReview: 1,
116+
carriedForward: 0,
117+
unclassifiedCurrent: 0,
86118
previousStillValid: 0,
87119
total: 1,
88120
});

0 commit comments

Comments
 (0)