fix(ci): require a line range for context evidence

The tri-review's adversarial lane executed `context({name: 'AGENTS.md'})`
and had the result accepted: the gate checked only that the resolved
filePath was in the changed set, so a bare File node passed for a review
of that file's contents. The trusted prescan already defines an indexable
symbol as one with startLine and endLine, so require the same here.

Pre-existing rather than introduced by this branch, but it is the same
"what counts as proof" surface the rest of this PR tightens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-07-28 15:15:39 +00:00
parent 0432214d80
commit 1d9f2d753a
2 changed files with 27 additions and 1 deletions

View file

@ -1748,11 +1748,16 @@ jobs:
throw new Error('context tool result is not strict JSON');
}
validateBoundedJson(decoded, { nodes: 0 });
// A line range is what the trusted prescan calls an indexable
// symbol, so a bare File node — `context({name: 'AGENTS.md'})` —
// must not pass for a review of that file's contents.
if (
!isRecord(decoded) ||
Object.hasOwn(decoded, 'error') ||
decoded.status !== 'found' ||
!isRecord(decoded.symbol)
!isRecord(decoded.symbol) ||
!Number.isFinite(decoded.symbol.startLine) ||
!Number.isFinite(decoded.symbol.endLine)
) {
rejected.unresolved += 1;
return false;

View file

@ -1776,6 +1776,27 @@ describe('gitnexus review-agent workflow security contract', () => {
expect(structural.artifact.failure_code).toBe('invalid_execution_transcript');
});
it('refuses a bare File node as evidence, matching the prescan definition of indexable', () => {
const fileNode = runArtifactScenario({
rawTranscript: JSON.stringify(
reviewTranscript({
toolInput: { name: 'status.ts' },
toolResultContent: JSON.stringify({
status: 'found',
symbol: {
uid: `File:${CHANGED_PATH}`,
name: 'status.ts',
kind: 'File',
filePath: CHANGED_PATH,
},
}),
}),
),
});
expect(fileNode.artifact.failure_code).toBe('missing_graph_evidence');
expect(fileNode.stderr).toContain('results that resolved nothing: 1');
});
it('scopes a deletion-only PR to the merge-base set instead of an empty head set', () => {
const deletedPath = 'gitnexus/src/cli/deleted-command.ts';
const headScopedCall = runArtifactScenario({