feat(evidence): read selected complete records without the whole bundle (#8528)

* feat(evidence): read selected complete records without the whole bundle

get_evidence_bundle could discover a record by exact ID, but reading it
required the complete canonical bundle: complete + filters was rejected,
and inspect_analysis_view refuses parents without a view, such as
annotations, pseudocode sources and native traces.

- Add detail: records, a page (offset, limit; default 0, 8) of the
  complete matching records in canonical bundle order. Records keep their
  original IDs, subjects, providers and readback; missing_evidence_links
  names direct links of the returned records that are not retained.
- The selection is labelled as historical and not an importable bundle;
  complete keeps returning the canonical bundle, and offset/limit
  require detail: records.
- Filters keep one owner shared by summary and records delivery, for the
  MCP session and the CLI's inspect-evidence-bundle.
- An oversized record page now advises a smaller limit or one
  evidence_id; whole-bundle overflow guidance mentions record reads.

Fixes #8307

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(evidence): compare record-selection guidance with its owners

Assert the record-selection limitations and oversized-page guidance
against their owning constants, and check the reported limit facts
separately, instead of pinning explanatory phrases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-11-01 08:56:58 -08:00
1 parent 4b840aa1fb
commit eb39a07966
15 files changed
+523 -41

No files matched your search

@@ -431,7 +431,10 @@ output also streams without a single MCP frame.
When the retained parent ID is unknown, use `get_evidence_bundle` with
`detail: "summary"` and exact filters for operation, target SHA-256,
analysis-profile digest or canonical procedure address. Select its returned
Evidence ID for the next view instead of requesting the complete bundle.
Evidence ID for the next view instead of requesting the complete bundle. When
that record has no supported view (for example an annotation), read it with
`detail: "records"` and an `evidence_id` filter; `missing_evidence_links`
names linked sources this session does not retain.
`retention: "complete-record"` means the whole record is stored; native
value-flow truncation and unknown metadata remain separate. For an exported
portable bundle, use `rea inspect-evidence-bundle` with JSON containing `path`,
+3 -2
View File
@@ -65,8 +65,9 @@ confidence and basis, and does not prove a backend endpoint or reachability.
Pages obey the shared 1 MiB projection budget; MCP transport applies its own
wire-size guard. A byte-limited page returns a continuation; an indivisible
oversized record or metadata returns a typed resource error. Select fewer
fields or retrieve the complete retained parent through `get_evidence_bundle`,
`export_evidence_bundle`, or the CLI's exported bundle. Full Evidence is never
fields or retrieve the complete retained parent through `get_evidence_bundle`
(`detail: "records"` with its `evidence_id`), `export_evidence_bundle`, or the
CLI's exported bundle. Full Evidence is never
silently truncated.
Collections are `summary`, `routes`, `occurrences`, `sources`, and `unknowns`.
+21 -1
View File
@@ -333,7 +333,26 @@ a provider. `retention: "complete-record"` describes stored Evidence, while
`native_dossier.value_flow` independently reports truncation, unavailable or
unknown metadata. Missing omission counts remain null. Summary output carries
every matching record; actual transport admission still applies. Omitted
`detail` preserves complete canonical bundle delivery; filters require summary.
`detail` preserves complete canonical bundle delivery; filters require summary
or records.
To read a discovered record without transferring the whole bundle, call
`get_evidence_bundle` with `detail: "records"` and the same exact `filters`.
The result (`kind: "evidence-record-selection"`) pages the matching complete
records in canonical bundle (Evidence ID) order with `offset` (default 0) and
`limit` (default 8), and reports `matching_records`, `total_retained_records`
and `next_offset`. Each record is the retained Evidence itself, with an
unchanged ID, subject digest, provider, profile and readback, so it works for
any operation, including records that `inspect_analysis_view` does not support
such as annotations. `missing_evidence_links` lists direct `evidence_links` of
the returned records that this session or bundle does not retain; links are not
traversed. A selection is not an Evidence bundle: it omits manifests and
residual unknowns, cannot be imported, and describes historical observations
rather than current provider or database state. `offset` and `limit` require
`detail: "records"`. If one page exceeds the response budget, select fewer
records or one `evidence_id`; a single record larger than one response still
needs `export_evidence_bundle`, or `inspect_analysis_view` pages where its parent
supports views.
Residual-unknown mutations reject missing Evidence or unknown references and
unproven verified resolutions with nonretryable `invalid_request` diagnostics.
@@ -371,6 +390,7 @@ fit.
For a portable exported bundle, the CLI shares this projection:
`rea inspect-evidence-bundle '{"path":"/tmp/evidence.json","detail":"summary","filters":{"operation":"analyze_function"}}' --format json`.
`"detail":"records"` with an `evidence_id` filter returns that complete record.
The CLI validates and materializes that local bundle under its existing memory
budget; summary delivery does not make arbitrary large files cheap to import.
+2 -1
View File
@@ -83,7 +83,8 @@ valid: partial results state whether WABT validation is unknown, rejected the
module, or succeeded before a later inspection failure.
CLI errors include this partial Evidence inline. MCP errors can return its
`retained-evidence` reference in `details.partial_observation`; use
`get_evidence_bundle` with `detail: "complete"` to retrieve the matching record.
`get_evidence_bundle` with `detail: "records"` and that `evidence_id` filter to
retrieve the matching record.
Core validation remains authoritative when an optional custom section cannot
be interpreted. WABT warning output is preserved in `custom_section_warnings`;
+3 -1
View File
@@ -292,8 +292,10 @@ const projectWrite = (
export const inspectEvidenceBundleCommand = async (
input: {
readonly path: string;
readonly detail: "complete" | "summary";
readonly detail: "complete" | "summary" | "records";
readonly filters?: EvidenceRecordFilters | undefined;
readonly offset?: number | undefined;
readonly limit?: number | undefined;
},
signal?: AbortSignal,
) => {
@@ -2,19 +2,26 @@ import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
selectEvidenceRecords,
summarizeEvidenceBundle,
type EvidenceBundleSummary,
type EvidenceRecordFilters,
type EvidenceRecordSelection,
} from "../../domain/evidenceRecordSummary.js";
/** Select complete bundle delivery or discovery metadata without analysis payloads. */
/** Select complete bundle delivery, discovery metadata, or a page of complete selected records. */
export const inspectEvidenceBundle = (
bundle: EvidenceBundle,
options: {
readonly detail: "complete" | "summary";
readonly detail: "complete" | "summary" | "records";
readonly filters?: EvidenceRecordFilters | undefined;
readonly offset?: number | undefined;
readonly limit?: number | undefined;
},
): Result<EvidenceBundle | EvidenceBundleSummary, AnalysisInputError> => {
): Result<
EvidenceBundle | EvidenceBundleSummary | EvidenceRecordSelection,
AnalysisInputError
> => {
if (options.detail === "complete" && options.filters !== undefined)
return err(
new AnalysisInputError("get_evidence_bundle", undefined, [
@@ -22,11 +29,36 @@ export const inspectEvidenceBundle = (
path: ["filters"],
reason: "invalid_value",
message:
"Record filters require detail: summary; complete returns the canonical bundle.",
expected: "detail: summary",
"Record filters require detail: summary or records; complete returns the canonical bundle.",
expected: "detail: summary or records",
},
]),
);
if (options.detail !== "records") {
const paging = (["offset", "limit"] as const).filter(
(field) => options[field] !== undefined,
);
if (paging.length > 0)
return err(
new AnalysisInputError(
"get_evidence_bundle",
undefined,
paging.map((field) => ({
path: [field],
reason: "invalid_value" as const,
message: `${field} pages complete records and requires detail: records.`,
expected: "detail: records",
})),
),
);
}
if (options.detail === "records")
return ok(
selectEvidenceRecords(bundle, options.filters, {
offset: options.offset,
limit: options.limit,
}),
);
return ok(
options.detail === "summary"
? summarizeEvidenceBundle(bundle, options.filters)
+2 -2
View File
@@ -26,12 +26,12 @@ export const registerAnalysisViewCommands = (
): void => {
cli.command(CLI_COMMANDS.inspectEvidenceBundle, {
description:
"Inspect complete or selected discovery metadata from a local Evidence bundle",
"Inspect a local Evidence bundle completely, as discovery metadata, or as a page of selected complete records",
args: z.object({
inputJson: z
.string()
.describe(
"JSON or JSON file with absolute path, detail and optional record filters",
"JSON or JSON file with absolute path, detail, optional record filters, and records offset/limit",
),
}),
run: ({ args }) =>
+1 -1
View File
@@ -173,7 +173,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"get_evidence_bundle",
"Return every retained Evidence record and residual unknown as a complete canonical bundle by default. detail=summary discovers retained records without copying or transferring their payloads; optionally filter by exact Evidence ID, operation, target SHA-256, analysis-profile digest or canonical native procedure address. Metadata distinguishes complete record retention from native value-flow truncation or unavailable/unknown coverage. Use a discovered evidence_id with inspect_analysis_view without restarting a provider. Filters apply only to summary delivery.",
"Return every retained Evidence record and residual unknown as a complete canonical bundle by default. detail=summary discovers retained records without copying or transferring their payloads; optionally filter by exact Evidence ID, operation, target SHA-256, analysis-profile digest or canonical native procedure address. Metadata distinguishes complete record retention from native value-flow truncation or unavailable/unknown coverage. detail=records returns a page (offset, limit) of the matching complete records with unchanged IDs and their missing direct evidence_links; it is not an importable bundle. Use a discovered evidence_id with inspect_analysis_view without restarting a provider. Filters apply to summary and records delivery.",
getEvidenceBundleInputSchema,
),
{
+18 -3
View File
@@ -4,13 +4,28 @@ import { absoluteLocalPathStringSchema } from "../domain/localPath.js";
import { bundleComparisonInputSchema } from "../domain/bundleComparison.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { evidenceRecordFiltersSchema } from "../domain/evidenceRecordSummary.js";
import {
EVIDENCE_RECORD_SELECTION_LIMIT,
evidenceRecordFiltersSchema,
} from "../domain/evidenceRecordSummary.js";
import { residualUnknownSchema } from "../domain/residualUnknown.js";
/** Complete retained Evidence, or selected discovery metadata without payloads. */
/** Complete retained Evidence, selected discovery metadata, or a page of selected complete records. */
export const getEvidenceBundleInputSchema = z.strictObject({
detail: z.enum(["complete", "summary"]).default("complete"),
detail: z.enum(["complete", "summary", "records"]).default("complete"),
filters: evidenceRecordFiltersSchema.optional(),
offset: z
.int()
.nonnegative()
.optional()
.describe("records only: first matching record to return; default 0."),
limit: z
.int()
.positive()
.optional()
.describe(
`records only: maximum complete records to return; default ${String(EVIDENCE_RECORD_SELECTION_LIMIT)}.`,
),
});
/** Optional document selection for volatile navigation context. */
+9 -2
View File
@@ -42,7 +42,10 @@ import {
} from "../domain/investigationLifecycle.js";
import { jsonRecordSchema } from "../domain/jsonRecord.js";
import { byteSearchResultSchema } from "../domain/native/bytePattern.js";
import { evidenceBundleSummarySchema } from "../domain/evidenceRecordSummary.js";
import {
evidenceBundleSummarySchema,
evidenceRecordSelectionSchema,
} from "../domain/evidenceRecordSummary.js";
import { liveBinaryTargetSummaryVariants } from "../domain/liveBinaryTargetSummary.js";
import { nativeFunctionAnnotationsSchema } from "../domain/native/nativeFunctionAnnotations.js";
import { nativeLoadImageSchema } from "../domain/native/nativeLoadImage.js";
@@ -574,7 +577,11 @@ export const sessionOutputSchemas = {
),
export_evidence_bundle: lifecycleResultOf(evidenceBundleExportReceiptSchema),
get_evidence_bundle: lifecycleResultOf(
z.union([evidenceBundleSchema, evidenceBundleSummarySchema]),
z.union([
evidenceBundleSchema,
evidenceBundleSummarySchema,
evidenceRecordSelectionSchema,
]),
),
get_navigation_context: lifecycleResultOf(
z.object({
+4 -1
View File
@@ -48,7 +48,10 @@ export const MCP_TRANSPORT_EVIDENCE_EXPORT =
"Export complete retained session evidence through export_evidence_bundle to a caller-selected path, or use complete CLI JSON output. The connection remains usable.";
/** Next step when one MCP frame cannot carry a complete retained session bundle. */
export const MCP_TRANSPORT_EVIDENCE_DISCOVERY = `Discover retained records with get_evidence_bundle and detail: summary, then inspect compatible records through inspect_analysis_view using an exact Evidence reference. ${MCP_TRANSPORT_EVIDENCE_EXPORT}`;
export const MCP_TRANSPORT_EVIDENCE_DISCOVERY = `Discover retained records with get_evidence_bundle and detail: summary, then read selected complete records with detail: records and an exact evidence_id, or inspect compatible records through inspect_analysis_view using an exact Evidence reference. ${MCP_TRANSPORT_EVIDENCE_EXPORT}`;
/** Next step when one MCP frame cannot carry a selected page of complete records. */
export const MCP_TRANSPORT_EVIDENCE_RECORDS = `Request fewer records with get_evidence_bundle detail: records, a smaller limit or an exact evidence_id filter; inspect compatible records in pages through inspect_analysis_view. A single record larger than one response needs export. ${MCP_TRANSPORT_EVIDENCE_EXPORT}`;
export const analysisErrorRemediationAction = (
error: AnalysisError,
+96 -10
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { uniqueSorted } from "./canonicalOrdering.js";
import { digestSchema, prefixedDigestSchema } from "./digests.js";
import { evidenceSchema, type Evidence } from "./evidence.js";
import type { EvidenceBundle } from "./evidenceBundle.js";
@@ -107,15 +108,24 @@ const nativeSummary = (
};
};
/** Discover retained Evidence without reading or copying its complete payloads. */
export const summarizeEvidenceBundle = (
bundle: EvidenceBundle,
filters: EvidenceRecordFilters = {},
): EvidenceBundleSummary => {
const selectedFilters: Record<string, string> = {};
/** Caller-selected filters with omitted fields removed, echoed by every selection. */
const selectedFilters = (
filters: EvidenceRecordFilters,
): Record<string, string> => {
const selected: Record<string, string> = {};
for (const [field, value] of Object.entries(filters))
if (value !== undefined) selectedFilters[field] = value;
const records: EvidenceBundleSummary["records"] = [];
if (value !== undefined) selected[field] = value;
return selected;
};
/** One owner for exact filter semantics, in canonical bundle (Evidence ID) order. */
function* matchingEvidenceRecords(
bundle: EvidenceBundle,
filters: EvidenceRecordFilters,
): Generator<{
readonly evidence: Evidence;
readonly native: z.infer<typeof nativeSummarySchema>;
}> {
for (const evidence of bundle.records) {
if (
(filters.evidence_id !== undefined &&
@@ -135,6 +145,17 @@ export const summarizeEvidenceBundle = (
filters.procedure_address !== native.procedure_address)
)
continue;
yield { evidence, native };
}
}
/** Discover retained Evidence without reading or copying its complete payloads. */
export const summarizeEvidenceBundle = (
bundle: EvidenceBundle,
filters: EvidenceRecordFilters = {},
): EvidenceBundleSummary => {
const records: EvidenceBundleSummary["records"] = [];
for (const { evidence, native } of matchingEvidenceRecords(bundle, filters))
records.push({
evidence_id: evidence.evidence_id,
subject: evidence.subject,
@@ -147,13 +168,78 @@ export const summarizeEvidenceBundle = (
limitation_count: evidence.limitations.length,
native_dossier: native,
});
}
return {
kind: "evidence-bundle-summary",
filters: selectedFilters,
filters: selectedFilters(filters),
total_retained_records: bundle.records.length,
matching_records: records.length,
unknown_revisions: bundle.unknowns.length,
records,
};
};
/** Default page size for complete record selection; each record may be large. */
export const EVIDENCE_RECORD_SELECTION_LIMIT = 8;
/** Labels every record selection as historical and as distinct from an importable bundle. */
export const EVIDENCE_RECORD_SELECTION_LIMITATIONS = [
"Selected records are retained historical observations; they do not establish current live provider or database state.",
"A record selection is not an Evidence bundle: it omits manifests and residual unknowns and cannot be imported. Use export_evidence_bundle for a portable bundle.",
] as const;
/** Complete selected records with their retained identities and direct-link closure. */
export const evidenceRecordSelectionSchema = z.strictObject({
kind: z.literal("evidence-record-selection"),
filters: z.record(z.string(), z.string()),
total_retained_records: countSchema,
matching_records: countSchema,
offset: countSchema,
limit: z.int().positive(),
next_offset: countSchema.nullable(),
records: z.array(evidenceSchema),
missing_evidence_links: z
.array(prefixedDigestSchema("ev"))
.describe(
"Direct evidence_links of the returned records that this session or bundle does not retain; links are not traversed.",
),
limitations: z.array(z.string()),
});
export type EvidenceRecordSelection = z.infer<
typeof evidenceRecordSelectionSchema
>;
/** Select one page of complete authenticated records without copying the rest of the ledger. */
export const selectEvidenceRecords = (
bundle: EvidenceBundle,
filters: EvidenceRecordFilters = {},
page: {
readonly offset?: number | undefined;
readonly limit?: number | undefined;
} = {},
): EvidenceRecordSelection => {
const offset = page.offset ?? 0;
const limit = page.limit ?? EVIDENCE_RECORD_SELECTION_LIMIT;
const records: Evidence[] = [];
let matching = 0;
for (const { evidence } of matchingEvidenceRecords(bundle, filters)) {
if (matching >= offset && records.length < limit) records.push(evidence);
matching += 1;
}
const retained = new Set(bundle.records.map(({ evidence_id: id }) => id));
const missing = records.flatMap(({ evidence_links: links }) =>
links.filter((link) => !retained.has(link)),
);
const end = offset + records.length;
return {
kind: "evidence-record-selection",
filters: selectedFilters(filters),
total_retained_records: bundle.records.length,
matching_records: matching,
offset,
limit,
next_offset: end < matching ? end : null,
records,
missing_evidence_links: uniqueSorted(missing),
limitations: [...EVIDENCE_RECORD_SELECTION_LIMITATIONS],
};
};
+19 -11
View File
@@ -16,6 +16,7 @@ import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"
import {
MCP_TRANSPORT_EVIDENCE_DISCOVERY,
MCP_TRANSPORT_EVIDENCE_EXPORT,
MCP_TRANSPORT_EVIDENCE_RECORDS,
} from "../domain/analysisErrorPresentation.js";
import {
nodeStringCeilingRecovery,
@@ -61,10 +62,10 @@ export const mcpResponseOverflowProjection = (input: {
};
/**
* A summary projection has already used `detail: "summary"`.
* Export still writes the retained bundle outside one MCP frame.
* The bundle projection a get_evidence_bundle result already selected.
* Each projection owns a different next step when it exceeds one MCP frame.
*/
const evidenceBundleSummary = (value: JsonValue): boolean => {
const evidenceBundleProjection = (value: JsonValue): JsonValue | undefined => {
const payload =
typeof value === "object" &&
value !== null &&
@@ -72,12 +73,21 @@ const evidenceBundleSummary = (value: JsonValue): boolean => {
"result" in value
? value.result
: value;
return (
typeof payload === "object" &&
return typeof payload === "object" &&
payload !== null &&
!Array.isArray(payload) &&
payload.kind === "evidence-bundle-summary"
);
!Array.isArray(payload)
? payload.kind
: undefined;
};
const evidenceBundleOverflowStep = (value: JsonValue): string => {
const projection = evidenceBundleProjection(value);
// A summary already used detail: summary; export writes outside one frame.
if (projection === "evidence-bundle-summary")
return MCP_TRANSPORT_EVIDENCE_EXPORT;
return projection === "evidence-record-selection"
? MCP_TRANSPORT_EVIDENCE_RECORDS
: MCP_TRANSPORT_EVIDENCE_DISCOVERY;
};
/** Immutable encoding and recovery policy shared by one MCP server. */
@@ -253,9 +263,7 @@ export class ToolResultDelivery {
: contract.name === "list_unknowns"
? "Call list_unknowns on this same connection with the same filters and offset, using a smaller limit, then follow next_offset. If a single unknown cannot fit, export the session with export_evidence_bundle to recover it."
: contract.name === "get_evidence_bundle"
? evidenceBundleSummary(value)
? MCP_TRANSPORT_EVIDENCE_EXPORT
: MCP_TRANSPORT_EVIDENCE_DISCOVERY
? evidenceBundleOverflowStep(value)
: "Use complete CLI JSON output to consume the analysis.";
const projected = mcpResponseOverflowProjection({
completed: true,
@@ -622,6 +622,24 @@ cliTest(
const expected = inspectEvidenceBundle(bundle, options);
if (!expected.ok) throw expected.error;
expect(result.json).toEqual(expected.value);
// The same shared owner selects the complete record from the portable file.
const records = await cli.run({
arguments: [
"inspect-evidence-bundle",
JSON.stringify({ path, ...options, detail: "records", limit: 1 }),
"--json",
],
environment: { HOME: root, XDG_CONFIG_HOME: root, XDG_CACHE_HOME: root },
});
expect(records.exitCode).toBe(0);
expect(records.json).toMatchObject({
kind: "evidence-record-selection",
matching_records: 1,
limit: 1,
next_offset: null,
records: [parent],
missing_evidence_links: [],
});
const complete = await cli.run({
arguments: [
"inspect-evidence-bundle",
@@ -0,0 +1,286 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterAll, expect, it } from "vitest";
import { z } from "zod";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { MCP_TRANSPORT_EVIDENCE_RECORDS } from "../../../src/domain/analysisErrorPresentation.js";
import {
EVIDENCE_RECORD_SELECTION_LIMIT,
EVIDENCE_RECORD_SELECTION_LIMITATIONS,
evidenceRecordSelectionSchema,
} from "../../../src/domain/evidenceRecordSummary.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const schemaValidation = createMcpSchemaValidation();
afterAll(() => schemaValidation.clear());
const GHIDRA = { id: "ghidra", name: "Ghidra", version: "12.1.4" };
const target = (letter: string) => ({
path: `/fixtures/${letter}.macho`,
format: "mach-o" as const,
sha256: letter.repeat(64),
});
// Synthetic records with the shapes the issue observed; no provider is run.
const annotation = (letter: string, name: string) =>
createEvidence(target(letter), GHIDRA, {
operation: "annotate_native_function",
parameters: { procedure: "0x100000430", name },
result: { procedure: { address: "0x100000430", name }, comment: "audit" },
});
const annotatedA = annotation("a", "audit_entry");
const annotatedB = annotation("b", "other_entry");
const pseudocode = createEvidence(target("a"), GHIDRA, {
operation: "procedure_pseudo_code",
parameters: { procedure: "0x100000430" },
result: "int audit_entry(void) { return 0; }",
});
const unretainedSource = createEvidence(target("a"), GHIDRA, {
operation: "xrefs",
parameters: { address: "0x100000430" },
result: [],
});
const trace = createEvidence(target("a"), GHIDRA, {
operation: "trace_feature",
parameters: { feature: "audit" },
result: { references: 1 },
evidenceLinks: [pseudocode.evidence_id, unretainedSource.evidence_id],
});
const LEDGER = [annotatedA, pseudocode, annotatedB, trace];
type Record_ = (typeof LEDGER)[number];
/** Canonical bundle order: exact Evidence ID code-point order. */
const canonical = (records: readonly Record_[]) =>
[...records].sort((left, right) =>
left.evidence_id < right.evidence_id ? -1 : 1,
);
const connect = async (
options: {
readonly profile?: "full" | "compact";
readonly maximumResponseBytes?: number;
readonly records?: readonly ReturnType<typeof createEvidence>[];
} = {},
) => {
const session = createTestBinarySession(() => {
throw new Error("Record selection must not open a provider");
});
const { client } = await createApplicationMcpHarness({
session,
clientIdentity: { name: "evidence-record-selection", version: "1" },
schemaValidator: schemaValidation,
serverOptions: {
environment: {
...process.env,
REA_MCP_OUTPUT_SCHEMA_PROFILE: options.profile ?? "full",
},
...(options.maximumResponseBytes === undefined
? {}
: { delivery: createToolResultDelivery(options.maximumResponseBytes) }),
},
});
for (const record of options.records ?? LEDGER)
expect(session.recordEvidence(record).ok).toBe(true);
const reply = (args: Record<string, unknown>) =>
client.callTool({ name: "get_evidence_bundle", arguments: args });
const select = async (args: Record<string, unknown>) => {
const result = await reply({ detail: "records", ...args });
expect(result.isError, JSON.stringify(result)).not.toBe(true);
return {
result,
selection: evidenceRecordSelectionSchema.parse(
z.object({ result: z.unknown() }).parse(result.structuredContent)
.result,
),
};
};
return { client, session, reply, select };
};
it.each(["full", "compact"] as const)(
"returns selected complete records with retained identities (%s)",
async (profile) => {
const { client, select } = await connect({ profile });
const exact = await select({
filters: { evidence_id: annotatedA.evidence_id },
});
expect(exact.selection).toMatchObject({
kind: "evidence-record-selection",
filters: { evidence_id: annotatedA.evidence_id },
total_retained_records: LEDGER.length,
matching_records: 1,
offset: 0,
limit: EVIDENCE_RECORD_SELECTION_LIMIT,
next_offset: null,
missing_evidence_links: [],
});
// The readback is the retained record itself, authenticated under its original ID.
expect(exact.selection.records).toEqual([annotatedA]);
expect(parseEvidence(exact.selection.records[0]).evidence_id).toBe(
annotatedA.evidence_id,
);
// Historical and non-bundle labels come from their owning constant.
expect(exact.selection.limitations).toEqual([
...EVIDENCE_RECORD_SELECTION_LIMITATIONS,
]);
const { tools } = await client.listTools();
const tool = tools.find(({ name }) => name === "get_evidence_bundle")!;
const ajv = schemaValidation.assertions;
expect(ajv.validateSchema(tool.inputSchema)).toBe(true);
expect(
ajv.validate(tool.inputSchema, {
detail: "records",
filters: { operation: "trace_feature" },
offset: 0,
limit: 1,
}),
).toBe(true);
const outputSchema: Record<string, unknown> = tool.outputSchema!;
expect(ajv.validate(outputSchema, exact.result.structuredContent)).toBe(
true,
);
// Pages follow canonical bundle order across distinct targets.
const annotations = canonical([annotatedA, annotatedB]);
const first = await select({
filters: { operation: "annotate_native_function" },
limit: 1,
});
expect(first.selection).toMatchObject({
matching_records: 2,
next_offset: 1,
records: [annotations[0]],
});
const second = await select({
filters: { operation: "annotate_native_function" },
offset: 1,
limit: 1,
});
expect(second.selection).toMatchObject({
next_offset: null,
records: [annotations[1]],
});
expect(
(await select({ filters: { target_sha256: "a".repeat(64) } })).selection
.records,
).toEqual(canonical([annotatedA, pseudocode, trace]));
expect(
(await select({ filters: { operation: "absent" } })).selection,
).toMatchObject({ matching_records: 0, records: [], next_offset: null });
expect((await select({ offset: LEDGER.length })).selection).toMatchObject({
matching_records: LEDGER.length,
records: [],
});
},
);
it("reports only the selected records' unretained direct links", async () => {
const { session, select } = await connect();
const selected = await select({
filters: { evidence_id: trace.evidence_id },
});
expect(selected.selection.records).toEqual([trace]);
expect(selected.selection.missing_evidence_links).toEqual([
unretainedSource.evidence_id,
]);
// Unselected records never contribute closure facts.
expect(
(await select({ filters: { operation: "annotate_native_function" } }))
.selection.missing_evidence_links,
).toEqual([]);
expect(session.recordEvidence(unretainedSource).ok).toBe(true);
expect(
(await select({ filters: { evidence_id: trace.evidence_id } })).selection,
).toMatchObject({ records: [trace], missing_evidence_links: [] });
});
it("selects the same records after importing an exported bundle", async () => {
const exporter = await connect();
const root = await createTestTempDirectory("rea-record-selection-");
const path = join(root, "evidence.json");
await writeFile(
path,
JSON.stringify(exporter.session.exportEvidenceBundle()),
);
const importer = await connect({ records: [] });
const imported = await importer.client.callTool({
name: "import_evidence_bundle",
arguments: { path },
});
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
expect(
(
await importer.select({
filters: { evidence_id: annotatedB.evidence_id },
})
).selection.records,
).toEqual([annotatedB]);
});
it("keeps canonical bundle and summary contracts distinct from record pages", async () => {
const { reply } = await connect();
for (const [args, path] of [
[
{ detail: "complete", filters: { operation: "trace_feature" } },
"filters",
],
[{ detail: "summary", limit: 1 }, "limit"],
[{ detail: "complete", offset: 0 }, "offset"],
[{ limit: 1 }, "limit"],
] as const)
expect(parseMcpToolError(await reply(args))).toMatchObject({
error: {
code: "invalid_request",
details: { issues: [expect.objectContaining({ path: [path] })] },
},
});
expect(
parseMcpToolError(await reply({ detail: "records", limit: 0 })),
).toMatchObject({ error: { code: "invalid_request" } });
const summary = await reply({
detail: "summary",
filters: { evidence_id: trace.evidence_id },
});
expect(summary.structuredContent).toMatchObject({
result: { kind: "evidence-bundle-summary", matching_records: 1 },
});
});
it("guides an oversized record page toward a smaller selection", async () => {
const large = createEvidence(target("c"), GHIDRA, {
operation: "procedure_pseudo_code",
parameters: { procedure: "0x1000" },
result: "x".repeat(32 * 1024),
});
const { reply, select } = await connect({
maximumResponseBytes: 16 * 1024,
records: [...LEDGER, large],
});
const failed = parseMcpToolError(await reply({ detail: "records" })).error;
expect(failed.code).toBe("resource_constraint");
// Guidance comes from its owning value; the limit facts are checked separately.
expect(failed.remediation?.action).toContain(MCP_TRANSPORT_EVIDENCE_RECORDS);
const limits = z
.object({
details: z.object({
reported_limits: z.object({
result_budget_bytes: z.number(),
response_bytes_at_least: z.number(),
}),
}),
})
.parse(failed).details.reported_limits;
expect(limits.response_bytes_at_least).toBeGreaterThan(
limits.result_budget_bytes,
);
// An exact selection that excludes the large record still fits.
expect(
(await select({ filters: { evidence_id: annotatedA.evidence_id } }))
.selection.records,
).toEqual([annotatedA]);
});