Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/mcp-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,26 @@ that revision. If a list response overflows, retry on the same connection with
the same filters and offset and a smaller limit; export if one head still cannot
fit.

To find missing sources before a downstream analysis refuses them, read
`missing_direct_links` from the summary, or call `get_evidence_bundle` with
`detail: "links"`. The result (`kind: "evidence-link-inspection"`) transfers no
record payload. `filters` select the starting records, while membership is
checked against every record in the session or file, so a filtered-out source is
`retained`, not missing. Rows (`evidence_id`, `linked_evidence_id`, `status`) are
in canonical order, default to `link_status: "missing"` and page with `offset` and
`limit` (default 64); `links` counts every inspected link regardless of that
selection. `traversal: "direct"` (the default) reads the selected records' own
links and sets `retained_closure` to null, because it makes no closure claim.
`traversal: "retained-closure"` also follows retained links, as composed workflows
such as `build_call_path` do, and reports `complete` or `incomplete`; anything
beyond a missing link remains unknown. Missing means absent from this session or
file, not nonexistent, and neither status describes analysis coverage. Importing
a missing source changes this membership without changing any Evidence ID.

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.
`"detail":"records"` with an `evidence_id` filter returns that complete record,
and `"detail":"links"` reads link membership.
The CLI validates and materializes that local bundle under its existing memory
budget; summary delivery does not make arbitrary large files cheap to import.

Expand Down
4 changes: 3 additions & 1 deletion src/application/EvidenceBundleCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,12 @@ const projectWrite = (
export const inspectEvidenceBundleCommand = async (
input: {
readonly path: string;
readonly detail: "complete" | "summary" | "records";
readonly detail: "complete" | "summary" | "records" | "links";
readonly filters?: EvidenceRecordFilters | undefined;
readonly offset?: number | undefined;
readonly limit?: number | undefined;
readonly traversal?: "direct" | "retained-closure" | undefined;
readonly link_status?: "missing" | "retained" | "all" | undefined;
},
signal?: AbortSignal,
) => {
Expand Down
73 changes: 52 additions & 21 deletions src/application/investigation/InspectEvidenceBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,41 @@ import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
inspectEvidenceLinks,
selectEvidenceRecords,
summarizeEvidenceBundle,
type EvidenceBundleSummary,
type EvidenceLinkInspection,
type EvidenceRecordFilters,
type EvidenceRecordSelection,
} from "../../domain/evidenceRecordSummary.js";

/** Select complete bundle delivery, discovery metadata, or a page of complete selected records. */
type InspectionDetail = "complete" | "summary" | "records" | "links";

/** Fields owned by particular deliveries, refused elsewhere rather than ignored. */
const DELIVERY_FIELDS = [
["offset", ["records", "links"]],
["limit", ["records", "links"]],
["traversal", ["links"]],
["link_status", ["links"]],
] as const;

/** Select complete delivery, discovery metadata, complete records, or link membership. */
export const inspectEvidenceBundle = (
bundle: EvidenceBundle,
options: {
readonly detail: "complete" | "summary" | "records";
readonly detail: InspectionDetail;
readonly filters?: EvidenceRecordFilters | undefined;
readonly offset?: number | undefined;
readonly limit?: number | undefined;
readonly traversal?: "direct" | "retained-closure" | undefined;
readonly link_status?: "missing" | "retained" | "all" | undefined;
},
): Result<
EvidenceBundle | EvidenceBundleSummary | EvidenceRecordSelection,
| EvidenceBundle
| EvidenceBundleSummary
| EvidenceRecordSelection
| EvidenceLinkInspection,
AnalysisInputError
> => {
if (options.detail === "complete" && options.filters !== undefined)
Expand All @@ -29,36 +46,50 @@ export const inspectEvidenceBundle = (
path: ["filters"],
reason: "invalid_value",
message:
"Record filters require detail: summary or records; complete returns the canonical bundle.",
expected: "detail: summary or records",
"Record filters require detail: summary, records or links; complete returns the canonical bundle.",
expected: "detail: summary, records or links",
},
]),
);
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) => ({
const misplaced = DELIVERY_FIELDS.filter(
([field, details]) =>
options[field] !== undefined &&
!(details as readonly InspectionDetail[]).includes(options.detail),
);
if (misplaced.length > 0)
return err(
new AnalysisInputError(
"get_evidence_bundle",
undefined,
misplaced.map(([field, details]) => {
const expected = details
.map((detail) => `detail: ${detail}`)
.join(" or ");
return {
path: [field],
reason: "invalid_value" as const,
message: `${field} pages complete records and requires detail: records.`,
expected: "detail: records",
})),
),
);
}
message: `${field} requires ${expected}.`,
expected,
};
}),
),
);
if (options.detail === "records")
return ok(
selectEvidenceRecords(bundle, options.filters, {
offset: options.offset,
limit: options.limit,
}),
);
if (options.detail === "links")
return ok(
inspectEvidenceLinks(bundle, options.filters, {
traversal: options.traversal,
linkStatus: options.link_status,
offset: options.offset,
limit: options.limit,
}),
);
return ok(
options.detail === "summary"
? summarizeEvidenceBundle(bundle, options.filters)
Expand Down
4 changes: 2 additions & 2 deletions src/cli/analysisViewCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export const registerAnalysisViewCommands = (
): void => {
cli.command(CLI_COMMANDS.inspectEvidenceBundle, {
description:
"Inspect a local Evidence bundle completely, as discovery metadata, or as a page of selected complete records",
"Inspect a local Evidence bundle completely, as discovery metadata, as a page of selected complete records, or as direct-link and retained-closure membership.",
args: z.object({
inputJson: z
.string()
.describe(
"JSON or JSON file with absolute path, detail, optional record filters, and records offset/limit",
"JSON or JSON file with absolute path, detail, optional record filters, records or links offset/limit, and links traversal/link_status",
),
}),
run: ({ args }) =>
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/sessionToolContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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.",
"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. detail=links reads which evidence_links of the selected records this session or bundle retains, without their payloads: traversal retained-closure also follows retained links, the walk composed workflows require, and rows default to missing links. Use a discovered evidence_id with inspect_analysis_view without restarting a provider. Filters apply to summary, records and links delivery.",
getEvidenceBundleInputSchema,
),
{
Expand Down
23 changes: 19 additions & 4 deletions src/contracts/sessionToolSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,41 @@ import { bundleComparisonInputSchema } from "../domain/bundleComparison.js";

import { prefixedDigestSchema } from "./../domain/digests.js";
import {
EVIDENCE_LINK_INSPECTION_LIMIT,
EVIDENCE_RECORD_SELECTION_LIMIT,
evidenceLinkStatusFilterSchema,
evidenceLinkTraversalSchema,
evidenceRecordFiltersSchema,
} from "../domain/evidenceRecordSummary.js";
import { residualUnknownSchema } from "../domain/residualUnknown.js";

/** Complete retained Evidence, selected discovery metadata, or a page of selected complete records. */
/** Complete retained Evidence, discovery metadata, a page of complete records, or link membership. */
export const getEvidenceBundleInputSchema = z.strictObject({
detail: z.enum(["complete", "summary", "records"]).default("complete"),
detail: z
.enum(["complete", "summary", "records", "links"])
.default("complete"),
filters: evidenceRecordFiltersSchema.optional(),
offset: z
.int()
.nonnegative()
.optional()
.describe("records only: first matching record to return; default 0."),
.describe("records or links: first matching row to return; default 0."),
limit: z
.int()
.positive()
.optional()
.describe(
`records only: maximum complete records to return; default ${String(EVIDENCE_RECORD_SELECTION_LIMIT)}.`,
`records: maximum complete records, default ${String(EVIDENCE_RECORD_SELECTION_LIMIT)}. links: maximum link rows, default ${String(EVIDENCE_LINK_INSPECTION_LIMIT)}.`,
),
traversal: evidenceLinkTraversalSchema
.optional()
.describe(
"links only: direct (default) reads the selected records' own links; retained-closure also follows retained links.",
),
link_status: evidenceLinkStatusFilterSchema
.optional()
.describe(
"links only: rows to return, default missing. Counts always cover every inspected link.",
),
});

Expand Down
2 changes: 2 additions & 0 deletions src/contracts/toolOutputSchemaGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { jsonRecordSchema } from "../domain/jsonRecord.js";
import { byteSearchResultSchema } from "../domain/native/bytePattern.js";
import {
evidenceBundleSummarySchema,
evidenceLinkInspectionSchema,
evidenceRecordSelectionSchema,
} from "../domain/evidenceRecordSummary.js";
import { liveBinaryTargetSummaryVariants } from "../domain/liveBinaryTargetSummary.js";
Expand Down Expand Up @@ -533,6 +534,7 @@ export const sessionOutputSchemas = {
evidenceBundleSchema,
evidenceBundleSummarySchema,
evidenceRecordSelectionSchema,
evidenceLinkInspectionSchema,
]),
),
get_navigation_context: lifecycleResultOf(
Expand Down
4 changes: 4 additions & 0 deletions src/domain/analysisErrorPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const MCP_TRANSPORT_EVIDENCE_DISCOVERY = `Discover retained records with
/** 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}`;

/** Next step when one MCP frame cannot carry a selected page of link rows. */
export const MCP_TRANSPORT_EVIDENCE_LINKS =
"Request fewer rows with get_evidence_bundle detail: links, a smaller limit or an exact evidence_id filter, then follow next_offset.";

/** Name the next step that is true for how long the caller keeps REA's runtime and session. */
export const analysisErrorRemediationAction = (
error: AnalysisError,
Expand Down
Loading
Loading