Skip to content

Commit 8d9d8d9

Browse files
JohnMcLearclaude
andcommitted
fix(reviews): aggregate accepts repoRoot to resolve repo-relative paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f9f24b8 commit 8d9d8d9

3 files changed

Lines changed: 35 additions & 9 deletions

File tree

.claude/commands/release-review.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Block until all four complete. Verify each output JSON exists. For any that didn
5555

5656
```bash
5757
pnpm --filter ep_etherpad-lite exec tsx node/utils/releaseReview/cli.ts \
58-
aggregate /tmp/release-review/<run-id> docs/reviews/known-findings.yml medium
58+
aggregate /tmp/release-review/<run-id> docs/reviews/known-findings.yml medium "$(git rev-parse --show-toplevel)"
5959
```
6060
(Replace `<run-id>` with the literal run-id from Phase 0.)
6161
Reads all `*.json` from the run-dir except `merged.json` / `triage.json`. Writes `merged.json`.

src/node/utils/releaseReview/cli.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@ const cmds: Record<string, (args: string[]) => void> = {
2929
},
3030

3131
aggregate: (args) => {
32-
const [runDir, supPath, floor] = args;
33-
if (!runDir || !supPath || !floor) die('usage: aggregate <runDir> <suppressionPath> <severityFloor>');
32+
const [runDir, supPath, floor, repoRoot] = args;
33+
if (!runDir || !supPath || !floor || !repoRoot) die('usage: aggregate <runDir> <suppressionPath> <severityFloor> <repoRoot>');
3434
const fileLineCache = new Map<string, string[]>();
3535
const readLines = (file: string): string[] => {
36-
if (!fileLineCache.has(file)) {
36+
const abs = path.isAbsolute(file) ? file : path.join(repoRoot, file);
37+
if (!fileLineCache.has(abs)) {
3738
fileLineCache.set(
38-
file,
39-
fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n') : [],
39+
abs,
40+
fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8').split('\n') : [],
4041
);
4142
}
42-
return fileLineCache.get(file)!;
43+
return fileLineCache.get(abs)!;
4344
};
4445
const enrich = (raw: any): Finding => {
4546
// Subagent JSON may be top-level array OR {findings: [...]}.

src/tests/backend/specs/releaseReview-utils.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ describe(__filename, function () {
320320
]));
321321
const supPath = path.join(tmpDir, 'sup.yml');
322322
fs.writeFileSync(supPath, 'findings: []\n');
323-
runCli(['aggregate', runDir, supPath, 'medium']);
323+
runCli(['aggregate', runDir, supPath, 'medium', '/']);
324324
const merged = JSON.parse(fs.readFileSync(path.join(runDir, 'merged.json'), 'utf8'));
325325
assert.equal(merged.length, 1);
326326
assert.equal(merged[0].severity, 'high');
@@ -339,11 +339,36 @@ describe(__filename, function () {
339339
}));
340340
const supPath = path.join(tmpDir, 'sup-empty.yml');
341341
fs.writeFileSync(supPath, 'findings: []\n');
342-
runCli(['aggregate', runDir, supPath, 'medium']);
342+
runCli(['aggregate', runDir, supPath, 'medium', '/']);
343343
const merged = JSON.parse(fs.readFileSync(path.join(runDir, 'merged.json'), 'utf8'));
344344
assert.equal(merged.length, 1);
345345
assert.match(merged[0].fingerprint, /^[0-9a-f]{64}$/);
346346
});
347+
348+
it('aggregate resolves repo-relative file paths against repoRoot', function () {
349+
this.timeout(15000);
350+
const runDir = path.join(tmpDir, 'run-2026-05-09-3');
351+
fs.mkdirSync(runDir);
352+
// Use a relative path that requires repoRoot resolution.
353+
const fakeRepoRoot = FIXTURE_DIR;
354+
const relPath = 'sample-source.ts'; // exists at FIXTURE_DIR/sample-source.ts
355+
fs.writeFileSync(path.join(runDir, 'auth-sessions.json'), JSON.stringify({
356+
findings: [
357+
{source: 'auth-sessions', severity: 'high', category: 'bug', file: relPath, line: 6, ruleId: 'auth-sessions.x', message: 'm'},
358+
],
359+
}));
360+
const supPath = path.join(tmpDir, 'sup-rel.yml');
361+
fs.writeFileSync(supPath, 'findings: []\n');
362+
runCli(['aggregate', runDir, supPath, 'medium', fakeRepoRoot]);
363+
const merged = JSON.parse(fs.readFileSync(path.join(runDir, 'merged.json'), 'utf8'));
364+
assert.equal(merged.length, 1);
365+
// Fingerprint should be computed from real file content.
366+
// Compare to a known fingerprint we can derive directly.
367+
const {computeFingerprint} = require('../../../node/utils/releaseReview/fingerprint');
368+
const lines = fs.readFileSync(path.join(fakeRepoRoot, relPath), 'utf8').split('\n');
369+
const expected = computeFingerprint('auth-sessions.x', relPath, 6, lines);
370+
assert.equal(merged[0].fingerprint, expected);
371+
});
347372
});
348373

349374
describe('suppression', function () {

0 commit comments

Comments
 (0)