Skip to content

Commit 94b5649

Browse files
committed
fix(investigation): refine oversized context searches
1 parent 9bac08d commit 94b5649

10 files changed

Lines changed: 90 additions & 15 deletions

__tests__/unit/context-gateway/filesystem-context-gateway-v4.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,40 @@ describe('FilesystemContextGatewayV4', () => {
233233
}
234234
});
235235

236+
it('rejects searches spanning more relation paths than deterministic expansion can represent', async () => {
237+
const fixture = await createWideSearchFixture();
238+
try {
239+
const { gateway, recorder, replayMaterial } =
240+
await createGateway(fixture);
241+
242+
await expect(
243+
gateway.searchText({ query: 'WIDE_RELATION_CANARY', paths: ['.'] })
244+
).rejects.toThrow('context_gateway_relation_path_limit_exceeded');
245+
246+
expect(recorder.snapshot()).toMatchObject({
247+
confinementTainted: false,
248+
events: [
249+
expect.objectContaining({
250+
outcome: 'rejected',
251+
failureClass: 'budget_exceeded',
252+
operationReceiptId: null,
253+
sanitizedReason: 'context_gateway_relation_path_limit_exceeded',
254+
}),
255+
],
256+
});
257+
expect(replayMaterial.snapshot().entries).toHaveLength(0);
258+
259+
await expect(
260+
gateway.searchText({
261+
query: 'SENSITIVE_QUERY_CANARY',
262+
paths: ['src'],
263+
})
264+
).resolves.toMatchObject({ complete: true });
265+
} finally {
266+
await rm(fixture.parent, { recursive: true, force: true });
267+
}
268+
});
269+
236270
it('canonicalizes safe virtual-root aliases without duplicating evidence', async () => {
237271
const fixture = await createFixture();
238272
try {
@@ -492,6 +526,30 @@ async function createFixture() {
492526
return { parent, root, mergeBaseSha, headSha, headTreeOid };
493527
}
494528

529+
async function createWideSearchFixture() {
530+
const fixture = await createFixture();
531+
await Promise.all(
532+
Array.from({ length: 513 }, async (_, index) => {
533+
await writeFile(
534+
path.join(
535+
fixture.root,
536+
'docs',
537+
`wide-${String(index).padStart(3, '0')}.md`
538+
),
539+
'WIDE_RELATION_CANARY\n'
540+
);
541+
})
542+
);
543+
await git(fixture.root, ['add', '.']);
544+
await git(fixture.root, ['commit', '-qm', 'wide relation fixture']);
545+
const headSha = await gitText(fixture.root, ['rev-parse', 'HEAD']);
546+
const headTreeOid = await gitText(fixture.root, [
547+
'rev-parse',
548+
`${headSha}^{tree}`,
549+
]);
550+
return { ...fixture, headSha, headTreeOid };
551+
}
552+
495553
async function initializeRepo(root: string) {
496554
await git(root, ['init', '-q']);
497555
await git(root, ['config', 'user.name', 'ReviewRouter Test']);

__tests__/unit/review-orchestration/review-investigation-recording-adapter.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ describe('ReviewInvestigationRecordingAdapter', () => {
237237
expect(prompt).toContain(
238238
'Never bind an exploratory search to a deterministic_expansion obligation'
239239
);
240+
expect(prompt).toContain('context_gateway_relation_path_limit_exceeded');
241+
expect(prompt).toContain('at most 512 files');
240242
expect(prompt).toContain('provider-neutral obligationProposals entry');
241243
expect(prompt).toContain(
242244
'exactly kind, canonicalSubject, canonicalRequirement, and riskPriority'

dist/context-gateway.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14111,6 +14111,7 @@ var import_crypto2 = require("crypto");
1411114111
var CONTEXT_GATEWAY_V4_POLICY_VERSION = "context-gateway-v4";
1411214112
var CONTEXT_GATEWAY_V4_CURSOR_VERSION = 1;
1411314113
var CONTEXT_GATEWAY_V4_PAGE_MAX_ITEMS = 2e3;
14114+
var CONTEXT_GATEWAY_V4_RELATION_PATH_MAX_ITEMS = 512;
1411414115
var CONTEXT_GATEWAY_V4_CURSOR_MAX_LIFETIME_MS = 15 * 60 * 1e3;
1411514116
var CONTEXT_GATEWAY_V4_ENABLED_TOOLS = Object.freeze([
1411614117
"review_read_file",
@@ -14309,10 +14310,10 @@ var review_investigation_capability_v1_golden_default = {
1430914310
probePolicyVersion: "review-investigation-probe-policy.v2",
1431014311
runtimeProfileVersion: "gateway-attested-agent.v1",
1431114312
searchPolicyVersion: "review-investigation-fixed-string-search.v1",
14312-
turnPromptContractHash: "41ad2e193eb96dfe8d091a76051652d4db4eb90a48560a33d07b31ef7f46b3d0"
14313+
turnPromptContractHash: "87996321aa77575ce5c434a555d238c57fe319cd4131b8cac412c9fa6bf08feb"
1431314314
},
14314-
canonicalJson: '{"coverageContractVersion":"review-investigation-coverage.v1","criticPolicyVersion":"review-investigation-critic.v2","expansionRulesVersion":"review-investigation-expansion.v3","gatewayPolicyVersion":"context-gateway-v4","probePolicyVersion":"review-investigation-probe-policy.v2","runtimeProfileVersion":"gateway-attested-agent.v1","searchPolicyVersion":"review-investigation-fixed-string-search.v1","turnPromptContractHash":"41ad2e193eb96dfe8d091a76051652d4db4eb90a48560a33d07b31ef7f46b3d0"}',
14315-
sha256: "a5e7cec2158b3c8ef91e51f633e51e43287c2e50deb572716f63d7007d978407"
14315+
canonicalJson: '{"coverageContractVersion":"review-investigation-coverage.v1","criticPolicyVersion":"review-investigation-critic.v2","expansionRulesVersion":"review-investigation-expansion.v3","gatewayPolicyVersion":"context-gateway-v4","probePolicyVersion":"review-investigation-probe-policy.v2","runtimeProfileVersion":"gateway-attested-agent.v1","searchPolicyVersion":"review-investigation-fixed-string-search.v1","turnPromptContractHash":"87996321aa77575ce5c434a555d238c57fe319cd4131b8cac412c9fa6bf08feb"}',
14316+
sha256: "064b245d9ac5b710a70ec2e3c1efdefa8f5b2de0efd73332a413f846c08783db"
1431614317
},
1431714318
policy: {
1431814319
value: {
@@ -16648,6 +16649,9 @@ var FilesystemContextGatewayV4 = class _FilesystemContextGatewayV4 {
1664816649
)).map(
1664916650
(value) => value.startsWith(`${revisionSha}:`) ? value.slice(revisionSha.length + 1) : value
1665016651
).sort();
16652+
if (matchedPaths.length > CONTEXT_GATEWAY_V4_RELATION_PATH_MAX_ITEMS) {
16653+
throw new Error("context_gateway_relation_path_limit_exceeded");
16654+
}
1665116655
return this.pageResult({
1665216656
operationKind: "text_search" /* TextSearch */,
1665316657
treeOid,

dist/context-gateway.release.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"artifactKind": "reviewrouter-context-gateway",
3-
"contextGatewayEntrypointDigest": "fee4fc416bed1632381f560a5504017edbf91f311d82e71df387a2407ed67e23",
3+
"contextGatewayEntrypointDigest": "adc0c03552313c3bd9cd2060b454f91e8ae706d7c693477ebb6b36f09aa13630",
44
"contextGatewayEntrypointPath": "dist/context-gateway.js",
55
"contextGatewayPolicyVersion": "context-gateway-v4",
66
"metadataVersion": 2,
77
"reviewInvestigationCapability": "review_investigation_v1",
8-
"reviewInvestigationCoverageProfileHash": "a5e7cec2158b3c8ef91e51f633e51e43287c2e50deb572716f63d7007d978407",
8+
"reviewInvestigationCoverageProfileHash": "064b245d9ac5b710a70ec2e3c1efdefa8f5b2de0efd73332a413f846c08783db",
99
"reviewInvestigationPolicyHash": "6af1a3ecea3cb01f8a0e1cea614c8578c6bc7e4010db13603c114757291af698",
1010
"supportedContextGatewayPolicyVersions": [
1111
"context-gateway-v3",

dist/index.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22141,6 +22141,7 @@ var import_crypto2 = require("crypto");
2214122141
var CONTEXT_GATEWAY_V4_POLICY_VERSION = "context-gateway-v4";
2214222142
var CONTEXT_GATEWAY_V4_CURSOR_VERSION = 1;
2214322143
var CONTEXT_GATEWAY_V4_PAGE_MAX_ITEMS = 2e3;
22144+
var CONTEXT_GATEWAY_V4_RELATION_PATH_MAX_ITEMS = 512;
2214422145
var CONTEXT_GATEWAY_V4_CURSOR_MAX_LIFETIME_MS = 15 * 60 * 1e3;
2214522146
var CONTEXT_GATEWAY_V4_ENABLED_TOOLS = Object.freeze([
2214622147
"review_read_file",
@@ -95141,10 +95142,10 @@ var review_investigation_capability_v1_golden_default = {
9514195142
probePolicyVersion: "review-investigation-probe-policy.v2",
9514295143
runtimeProfileVersion: "gateway-attested-agent.v1",
9514395144
searchPolicyVersion: "review-investigation-fixed-string-search.v1",
95144-
turnPromptContractHash: "41ad2e193eb96dfe8d091a76051652d4db4eb90a48560a33d07b31ef7f46b3d0"
95145+
turnPromptContractHash: "87996321aa77575ce5c434a555d238c57fe319cd4131b8cac412c9fa6bf08feb"
9514595146
},
95146-
canonicalJson: '{"coverageContractVersion":"review-investigation-coverage.v1","criticPolicyVersion":"review-investigation-critic.v2","expansionRulesVersion":"review-investigation-expansion.v3","gatewayPolicyVersion":"context-gateway-v4","probePolicyVersion":"review-investigation-probe-policy.v2","runtimeProfileVersion":"gateway-attested-agent.v1","searchPolicyVersion":"review-investigation-fixed-string-search.v1","turnPromptContractHash":"41ad2e193eb96dfe8d091a76051652d4db4eb90a48560a33d07b31ef7f46b3d0"}',
95147-
sha256: "a5e7cec2158b3c8ef91e51f633e51e43287c2e50deb572716f63d7007d978407"
95147+
canonicalJson: '{"coverageContractVersion":"review-investigation-coverage.v1","criticPolicyVersion":"review-investigation-critic.v2","expansionRulesVersion":"review-investigation-expansion.v3","gatewayPolicyVersion":"context-gateway-v4","probePolicyVersion":"review-investigation-probe-policy.v2","runtimeProfileVersion":"gateway-attested-agent.v1","searchPolicyVersion":"review-investigation-fixed-string-search.v1","turnPromptContractHash":"87996321aa77575ce5c434a555d238c57fe319cd4131b8cac412c9fa6bf08feb"}',
95148+
sha256: "064b245d9ac5b710a70ec2e3c1efdefa8f5b2de0efd73332a413f846c08783db"
9514895149
},
9514995150
policy: {
9515095151
value: {
@@ -99251,14 +99252,15 @@ function compareCodeUnits3(left, right) {
9925199252
}
9925299253

9925399254
// src/review-investigation/application/review-investigation-turn-prompt.ts
99254-
var REVIEW_INVESTIGATION_TURN_PROMPT_CONTRACT = "review_investigation_turn_prompt.v2";
99255+
var REVIEW_INVESTIGATION_TURN_PROMPT_CONTRACT = "review_investigation_turn_prompt.v3";
9925599256
var TURN_INSTRUCTIONS = Object.freeze([
9925699257
"REVIEW INVESTIGATION TURN CONTRACT:",
9925799258
"Use only the reviewrouter Context Gateway tools. Investigate every obligation in the authenticated turn brief.",
9925899259
'For typed search requirements, execute the exact literal query with paths=["."], revision="head", caseSensitive=true, and pageSize=500, then follow every cursor to completion.',
9925999260
"For a typed complete_page_chain obligation, put its complete receipt chain in closureClaims only. The control plane derives its discovery evidence; do not duplicate that chain in operationBackedDiscoveryClaims.",
9926099261
"For a typed complete_relation_context obligation, rerun its hydrated query and include the complete matching text_search receipt chain plus complete file_read receipts for exactly every requiredPathHashes entry. Never include unrelated search or directory receipts.",
9926199262
"During discovery turns, use operationBackedDiscoveryClaims only for additional exploratory text-search chains. Bind each chain to the coverage_contract changed_content obligation that directly motivated the search, copy the exact query passed to the tool, and include every operationReceiptId from the chain.",
99263+
"If an exploratory text search reports context_gateway_relation_path_limit_exceeded, do not claim that rejected search. Narrow the literal query and/or paths until each accepted search covers at most 512 files, or leave the related obligation open when no sound bounded query exists.",
9926299264
"Never bind an exploratory search to a deterministic_expansion obligation. If no changed_content source directly motivated it, omit the advisory discovery claim and leave related obligations open.",
9926399265
"When inspected evidence reveals additional review scope, add a provider-neutral obligationProposals entry instead of silently broadening an existing obligation.",
9926499266
"Each obligation proposal must contain exactly kind, canonicalSubject, canonicalRequirement, and riskPriority. Use only schema-listed kinds; never provide an obligation ID, state, authority decision, or receipt claim.",
@@ -102728,6 +102730,9 @@ var FilesystemContextGatewayV4 = class _FilesystemContextGatewayV4 {
102728102730
)).map(
102729102731
(value) => value.startsWith(`${revisionSha}:`) ? value.slice(revisionSha.length + 1) : value
102730102732
).sort();
102733+
if (matchedPaths.length > CONTEXT_GATEWAY_V4_RELATION_PATH_MAX_ITEMS) {
102734+
throw new Error("context_gateway_relation_path_limit_exceeded");
102735+
}
102731102736
return this.pageResult({
102732102737
operationKind: "text_search" /* TextSearch */,
102733102738
treeOid,

dist/index.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/context-gateway/context-gateway-v4-contract.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
export const CONTEXT_GATEWAY_V4_POLICY_VERSION = 'context-gateway-v4' as const;
1111
export const CONTEXT_GATEWAY_V4_CURSOR_VERSION = 1 as const;
1212
export const CONTEXT_GATEWAY_V4_PAGE_MAX_ITEMS = 2_000;
13+
export const CONTEXT_GATEWAY_V4_RELATION_PATH_MAX_ITEMS = 512;
1314
export const CONTEXT_GATEWAY_V4_CURSOR_MAX_LIFETIME_MS = 15 * 60 * 1_000;
1415
export const CONTEXT_GATEWAY_V4_ENABLED_TOOLS = Object.freeze([
1516
'review_read_file',

src/context-gateway/filesystem-context-gateway-v4.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import {
1616
CONTEXT_GATEWAY_V4_POLICY_VERSION,
1717
CONTEXT_GATEWAY_V4_PAGE_MAX_ITEMS,
18+
CONTEXT_GATEWAY_V4_RELATION_PATH_MAX_ITEMS,
1819
ContextGatewayV4OperationKind,
1920
ContextGatewayV4Revision,
2021
ContextOperationFailureClass,
@@ -411,6 +412,9 @@ export class FilesystemContextGatewayV4 {
411412
: value
412413
)
413414
.sort();
415+
if (matchedPaths.length > CONTEXT_GATEWAY_V4_RELATION_PATH_MAX_ITEMS) {
416+
throw new Error('context_gateway_relation_path_limit_exceeded');
417+
}
414418
return this.pageResult({
415419
operationKind: ContextGatewayV4OperationKind.TextSearch,
416420
treeOid,

src/review-investigation/application/review-investigation-turn-prompt.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ReviewInvestigationTurnBrief } from '../domain/investigation-state
22
import { canonicalJson, sha256 } from '../domain/canonical-json';
33

44
export const REVIEW_INVESTIGATION_TURN_PROMPT_CONTRACT =
5-
'review_investigation_turn_prompt.v2' as const;
5+
'review_investigation_turn_prompt.v3' as const;
66

77
const TURN_INSTRUCTIONS = Object.freeze([
88
'REVIEW INVESTIGATION TURN CONTRACT:',
@@ -11,6 +11,7 @@ const TURN_INSTRUCTIONS = Object.freeze([
1111
'For a typed complete_page_chain obligation, put its complete receipt chain in closureClaims only. The control plane derives its discovery evidence; do not duplicate that chain in operationBackedDiscoveryClaims.',
1212
'For a typed complete_relation_context obligation, rerun its hydrated query and include the complete matching text_search receipt chain plus complete file_read receipts for exactly every requiredPathHashes entry. Never include unrelated search or directory receipts.',
1313
'During discovery turns, use operationBackedDiscoveryClaims only for additional exploratory text-search chains. Bind each chain to the coverage_contract changed_content obligation that directly motivated the search, copy the exact query passed to the tool, and include every operationReceiptId from the chain.',
14+
'If an exploratory text search reports context_gateway_relation_path_limit_exceeded, do not claim that rejected search. Narrow the literal query and/or paths until each accepted search covers at most 512 files, or leave the related obligation open when no sound bounded query exists.',
1415
'Never bind an exploratory search to a deterministic_expansion obligation. If no changed_content source directly motivated it, omit the advisory discovery claim and leave related obligations open.',
1516
'When inspected evidence reveals additional review scope, add a provider-neutral obligationProposals entry instead of silently broadening an existing obligation.',
1617
'Each obligation proposal must contain exactly kind, canonicalSubject, canonicalRequirement, and riskPriority. Use only schema-listed kinds; never provide an obligation ID, state, authority decision, or receipt claim.',

src/review-investigation/fixtures/review-investigation-capability-v1.golden.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
"probePolicyVersion": "review-investigation-probe-policy.v2",
99
"runtimeProfileVersion": "gateway-attested-agent.v1",
1010
"searchPolicyVersion": "review-investigation-fixed-string-search.v1",
11-
"turnPromptContractHash": "41ad2e193eb96dfe8d091a76051652d4db4eb90a48560a33d07b31ef7f46b3d0"
11+
"turnPromptContractHash": "87996321aa77575ce5c434a555d238c57fe319cd4131b8cac412c9fa6bf08feb"
1212
},
13-
"canonicalJson": "{\"coverageContractVersion\":\"review-investigation-coverage.v1\",\"criticPolicyVersion\":\"review-investigation-critic.v2\",\"expansionRulesVersion\":\"review-investigation-expansion.v3\",\"gatewayPolicyVersion\":\"context-gateway-v4\",\"probePolicyVersion\":\"review-investigation-probe-policy.v2\",\"runtimeProfileVersion\":\"gateway-attested-agent.v1\",\"searchPolicyVersion\":\"review-investigation-fixed-string-search.v1\",\"turnPromptContractHash\":\"41ad2e193eb96dfe8d091a76051652d4db4eb90a48560a33d07b31ef7f46b3d0\"}",
14-
"sha256": "a5e7cec2158b3c8ef91e51f633e51e43287c2e50deb572716f63d7007d978407"
13+
"canonicalJson": "{\"coverageContractVersion\":\"review-investigation-coverage.v1\",\"criticPolicyVersion\":\"review-investigation-critic.v2\",\"expansionRulesVersion\":\"review-investigation-expansion.v3\",\"gatewayPolicyVersion\":\"context-gateway-v4\",\"probePolicyVersion\":\"review-investigation-probe-policy.v2\",\"runtimeProfileVersion\":\"gateway-attested-agent.v1\",\"searchPolicyVersion\":\"review-investigation-fixed-string-search.v1\",\"turnPromptContractHash\":\"87996321aa77575ce5c434a555d238c57fe319cd4131b8cac412c9fa6bf08feb\"}",
14+
"sha256": "064b245d9ac5b710a70ec2e3c1efdefa8f5b2de0efd73332a413f846c08783db"
1515
},
1616
"policy": {
1717
"value": {

0 commit comments

Comments
 (0)