Compare commits

...
Author SHA1 Message Date
morluto c2077c9f3b fix(native): reconcile current main formatting 2026-11-07 23:17:06 +08:00
morluto 1693e074b0 fix(native): reconcile procedure reference views with main 2026-11-07 22:40:05 +08:00
Cursor Agent 9975a60381 ci: rerun after the binary-layout toolchain timeout
The required check failed because the toolchain install exceeded 15 minutes.
The same job completed on the previous head, and this commit does not change
the procedure-reference behavior.
2026-11-04 22:50:57 +00:00
Cursor Agent e57e4c72e7 test(native): catch the string-length endpoint comparison
Use a 200,000-character multibyte selected procedure and a shorter ASCII
endpoint so the old JavaScript length check drops the selected row.
2026-11-04 22:29:15 +00:00
Cursor Agent b436f44b58 test(cli): count procedure-references as a dedicated command
The MCP tool has a CLI command, so it is no longer an MCP-only exception.
2026-11-04 22:06:40 +00:00
Cursor Agent 025f10a848 fix(native): compare endpoint size with UTF-8 identity length
Keep one running maximum of stored identity bytes. A multibyte selected
procedure is recognized when its JSON no longer fits the page.
2026-11-04 21:45:13 +00:00
Cursor Agent 9ebef9c8d1 fix(native): describe procedure-reference direction and coverage
Avoid execution wording in the tool description, and give the CLI direction
option the help text the catalog requires.
2026-11-04 21:39:04 +00:00
Cursor Agent 255865c727 fix(native): reject an oversized procedure endpoint before canonicalizing it
When the JSON size already exceeds every stored identity, skip the canonical
string and keep the row out of the page.
2026-11-04 21:22:45 +00:00
Cursor Agent acad91bb54 fix(native): measure a procedure endpoint once before its comparison key
Size the candidate before building one canonical identity, and reuse that
key for the selected-procedure and shared-endpoint checks.
2026-11-04 21:00:44 +00:00
Cursor Agent 5833bb7c9b Merge origin/main into codex/procedure-reference-stacked
Keep procedure-reference pages beside the landed instruction-reference view
and the value-flow parameter description.
2026-11-04 20:38:25 +00:00
Cursor Agent b7ebd60341 fix(native): count procedure-reference endpoints before keeping a row
Include the shared endpoint table in the page budget, with JSON punctuation,
and store a new procedure only when its row is accepted.
2026-11-04 20:12:32 +00:00
Cursor Agent 42e8fa871b fix(native): document shared procedure endpoints and the required selector
Describe kind shared and items.endpoints instead of inline procedure copies.
State that procedure-references requires a positional selector or --procedure.
2026-11-04 20:08:07 +00:00
Cursor Agent 1a4986718c merge: page procedure references without repeated scans
Bring the branch onto main. Unfiltered pages use the collection length
and only the requested window. Repeated procedure identities are shared
once per page. The CLI accepts --procedure, and procedure_references
uses the shared analysis Evidence predicate.
2026-11-04 19:57:10 +00:00
Cursor Agent 5c283971bc Merge origin/main into codex/procedure-reference-stacked
Retarget onto main after the feature-trace squash. Keep procedure-reference
pages with the landed feature-trace and batch-decompile view text.
2026-11-04 19:29:08 +00:00
Cursor Agent 7b0f6299bf Merge codex/native-feature-trace-views into codex/procedure-reference-stacked
Keep the procedure-reference description with the updated feature-trace text
and the batch-decompile unavailable status example from main.
2026-11-04 18:11:15 +00:00
Cursor Agent 6ffa15c4d6 Merge origin/main into codex/native-feature-trace-views
Keep the native feature-trace view description and main's batch-decompile
status example that includes unavailable rows.
2026-11-04 18:10:11 +00:00
Cursor Agent 27b90d5d59 ci: rerun pull request checks
The merge of origin/main did not start the CI workflow for this head.
2026-11-04 18:06:30 +00:00
Cursor Agent 090dacd526 Merge remote-tracking branch 'origin/main' into codex/native-feature-trace-views 2026-11-04 17:52:17 +00:00
Cursor Agent b403205338 Merge remote-tracking branch 'origin/main' into codex/procedure-reference-stacked 2026-11-04 17:51:21 +00:00
Cursor Agent 7d956fa9bf Merge branch 'codex/native-feature-trace-views' into codex/procedure-reference-stacked 2026-11-04 17:50:42 +00:00
Cursor Agent 51cfd08d45 fix(cli): report result sources when Evidence publication fails
A trace keeps linked sources on the result. Use those records for the
undelivered source closure as well as the bundle, so a refused output
does not report them as not captured.
2026-11-04 17:50:36 +00:00
Cursor Agent fb5f1ec805 Merge codex/native-feature-trace-views into codex/procedure-reference-stacked
Update the stacked branch with the base that now contains origin/main. Keep
procedure-reference pages together with the landed feature-trace, value-trace,
and batch-decompile views.
2026-11-04 17:11:23 +00:00
Cursor Agent bb73c66baf Merge origin/main into codex/native-feature-trace-views
Keep one-shot Evidence bundle capture and linked source closure together,
including snapshot replay and immutable bundle publication.
2026-11-04 17:08:31 +00:00
morluto 1fc86c45aa docs(mcp): guide trace reference classification 2026-11-04 10:15:18 +08:00
morluto de9d468211 docs(mcp): clarify imported residual questions 2026-11-04 10:02:56 +08:00
morluto ec05b9de27 merge: integrate main into native trace views 2026-11-03 19:31:43 +08:00
morluto 0fabb2f7b8 feat(native): page retained procedure references 2026-11-02 03:19:29 +08:00
morluto 2a0415cd5b test(snapshot): verify linked foreign evidence stays scoped 2026-11-02 02:56:23 +08:00
morluto 673d971381 fix(snapshot): retain linked source evidence for replay 2026-11-02 02:54:41 +08:00
morluto 4809afeef5 feat(native): page retained feature traces 2026-11-02 02:43:25 +08:00
16 changed files with 1148 additions and 16 deletions

No files matched your search

+30
View File
@@ -97,6 +97,36 @@ body are excluded. `procedure_references` returns internal and external edges;
The reference collector's semantics participate in the analysis profile, so
older snapshots cannot replay the former incomplete results under this profile.
For a bounded handoff, request `procedure_references` with `detail: "summary"`.
The summary retains the complete observation and reports the selected procedure,
direction and counts. Page it with `inspect_analysis_view`, using the summary's
`normalized_result.parent_evidence_id` and a view such as
`{"kind":"native-procedure-references","collection":"references","source_address":"0x1234","offset":0,"limit":64}`.
The optional exact source and target filters apply before paging. A row's
`source_index` identifies it in the original retained collection, including
duplicates. Endpoint `kind: "selected-procedure"` refers to the full procedure
in the page summary; `kind: "null"` preserves the original null without asserting
why it was unavailable. `kind: "shared"` carries an index into `items.endpoints`,
where each other procedure object is stored once per page. Use collection
`unresolved_calls` for paged unresolved call sites and their original reasons.
Page coverage describes retained rows, not complete provider discovery or runtime
execution. A 1 MiB projection allowance, also used by other bounded analysis views, can
shorten a requested page; `byte_limited: true` and `coverage.next_offset` identify the
continuation. A single row or summary that cannot fit returns a resource error;
retrieve or export the complete parent Evidence. This allowance has no runtime
override. Omit detail to receive the complete observation inline.
For a one-shot CLI handoff, save the complete Evidence and its linked sources:
```bash
rea procedure-references /absolute/path/to/program my_function --provider ghidra \
--direction incoming --detail summary --evidence_output /absolute/path/to/references.json
```
Use `rea inspect-analysis-view` with that bundle as an `evidence-file` source to
page it without reopening the target. This command requires a provider supporting
`procedure_references`; Kuna currently reports that capability unavailable.
Ghidra regex searches use Java Pattern semantics. If compiling or matching a
pattern exhausts the engine's stack, REA returns a resource constraint and
preserves the active database and annotations. Retry in literal mode or simplify
+1
View File
@@ -97,6 +97,7 @@ type DirectAnalysisTool =
| "address_to_file_offset"
| "binary_overview"
| "procedure_pseudo_code"
| "procedure_references"
| "read_function_instructions"
| "inspect_native_instruction"
| "inspect_native_data_type"
+62
View File
@@ -140,6 +140,68 @@ export const registerCoreNativeCommands = (
}),
),
});
cli.command(CLI_COMMANDS.procedureReferences, {
description: `Inspect incoming or outgoing references for one native procedure. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: localPathStringSchema.describe("Local executable path"),
procedure: z
.string()
.min(1)
.optional()
.describe(
`Procedure name or provider address. ${addressOrProcedureGuidance}`,
),
}),
options: z.object({
procedure: z
.string()
.min(1)
.optional()
.describe(
`Procedure name or provider address; use --procedure=<value> for leading dashes. ${addressOrProcedureGuidance}`,
),
direction: z
.enum(["incoming", "outgoing"])
.default("outgoing")
.describe("Reference direction. Defaults to outgoing."),
snapshot: snapshotSelectionOption,
...analysisEvidenceDeliveryOptions,
...nativeOpenOptionFields,
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.procedureReferences, async () => {
const delivery = {
providerId: "auto",
operation: "procedure_references",
signal,
detail: options.detail,
evidenceOutput: options.evidence_output,
};
const invalid = cliEvidenceDeliveryInputError(delivery);
if (invalid !== undefined) return projectAnalysisError(invalid);
const result = await runDirectAnalysis(
args.path,
"procedure_references",
{
procedure: resolveCliAnalysisSelector(
args.procedure,
options.procedure,
"procedure",
"procedure_references",
),
direction: options.direction,
},
{
...directAnalysisOptionsFromCli(logger, options),
signal,
includeEvidenceSources: options.evidence_output !== undefined,
},
);
return publishCliAnalysisEvidence(result, delivery);
}),
),
});
cli.command(CLI_COMMANDS.readBytes, {
description:
"Read provider memory bytes; Ghidra memory may include loader fixups and differ from file bytes",
+1
View File
@@ -35,6 +35,7 @@ export const CLI_COMMANDS = Object.freeze({
annotateNativeFunction: "annotate-native-function",
inspectNativeApi: "inspect-native-api",
instructions: "instructions",
procedureReferences: "procedure-references",
search: "search",
importReferenceSource: "import-reference-source",
inspectArtifact: "inspect-artifact",
+1 -1
View File
@@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_analysis_view"),
kind: "application",
description:
'For inspect_api_contract parents, graphql-contract pages retained SDL types/fields, operation definitions, field references, fragment-spreads or diagnostics with exact filters and selected columns. Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. For procedure_pseudo_code Evidence, summary reports text availability and the original selection; kind native with facet pseudocode pages the original UTF-16 text. Other native facets require analyze_function. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent also supports summary and bLine truncated
'Use native-procedure-references to page retained procedure reference or unresolved-call rows. Endpoint kind selected-procedure refers to the summary procedure, null preserves the original null, and shared carries an index into items.endpoints, where each other procedure is stored once per page. Exact source_address and target_address filters apply to reference rows. For inspect_api_contract parents, graphql-contract pages retained SDL types/fields, operation definitions, field references, fragment-spreads or diagnostics with exact filters and selected columns. Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Use web-bundle collections endpoints or inferences to page endpoint observations and inferences from retained analyze_web_bundle Evidence; summary returns target URL, origin, and counts. For retained trace_feature Evidence, kind native-feature-trace pages matches, references or unknowns; matches filter exact address, references filter exact source_address and target_address, source_index identifies each original row, and list_unknowns pages durable question heads. Inspect selected views of supported completed Evidence without repeating analysis. For procedure_pseudo_code Evidence, summary reports text availability and the original selection; kind native with facet pseudocode pages the original UTF-16 text. Other native facets require analyze_function. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, statuLine truncated
inputSchema: inspectAnalysisViewInputSchema,
outputSchema: evidenceResultOf(analysisViewResultSchema),
examples: [
+3 -1
View File
@@ -198,12 +198,14 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"procedure_references",
"Return every raw incoming or outgoing reference edge for one procedure. Endpoint procedures are resolved only from provider containment. Ghidra preserves detailed reference flags. Hopper preserves native CallReference classifications and exact endpoints while leaving unavailable detailed flags explicit.",
"Return incoming or outgoing reference edges for one procedure. Ghidra preserves detailed reference flags; Hopper preserves native CallReference classifications and exact endpoints. Select detail summary to retain complete Evidence and page references or unresolved calls using inspect_analysis_view kind native-procedure-references. Page coverage describes retained observations, not complete discovery or proof the target ran.",
z.object({
procedure,
direction: z.enum(["incoming", "outgoing"]).default("outgoing"),
document,
detail: analysisDetailSchema,
}),
{ evidenceParameters: "omit-detail" },
),
official(
"procedure_pseudo_code",
+5 -13
View File
@@ -1,3 +1,4 @@
import { procedureReferencesSchema } from "../domain/native/procedureReferences.js";
import {
nativeAddressListSchema,
nativeObservationSourceSchemas,
@@ -11,6 +12,7 @@ import {
nativeFunctionSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeFeatureTraceSummaryViewResultSchema,
nativeProcedureReferencesSummaryViewResultSchema,
batchDecompileSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
artifactInventorySummaryViewResultSchema,
@@ -33,11 +35,6 @@ import {
analysisBookmarkSchema,
nativeInventoryRowsSchemas,
} from "../domain/native/nativeInventory.js";
import {
referenceEdgeSchema,
unresolvedCallSchema,
} from "../domain/native/nativeReference.js";
import { procedureIdentitySchema } from "../domain/native/procedureInfo.js";
import { z } from "zod";
import {
evidenceBundleExportReceiptSchema,
@@ -229,14 +226,9 @@ export const officialOutputSchemas = {
),
}),
),
procedure_references: evidenceResultOf(
z.object({
procedure: procedureIdentitySchema,
direction: z.enum(["incoming", "outgoing"]),
reference_kinds_available: z.boolean(),
unresolved_calls: z.array(unresolvedCallSchema),
references: z.array(referenceEdgeSchema),
}),
procedure_references: analysisDetailOutputSchemaOf(
procedureReferencesSchema,
nativeProcedureReferencesSummaryViewResultSchema,
),
procedure_pseudo_code: analysisDetailOutputSchemaOf(
nullableText,
+5
View File
@@ -1,3 +1,4 @@
import { projectNativeProcedureReferencesView } from "./nativeProcedureReferencesView.js";
import { projectPlaywrightTraceView } from "./playwrightTraceView.js";
import { projectApiContractView } from "./apiContractView.js";
import {
@@ -153,6 +154,7 @@ import {
il2cppMetadataSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeFeatureTraceSummaryViewResultSchema,
nativeProcedureReferencesSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
nativeFunctionMatchingSummaryViewResultSchema,
nativeFunctionSummaryViewResultSchema,
@@ -229,6 +231,7 @@ export {
modelArtifactPageViewResultSchema,
il2cppMetadataSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeProcedureReferencesSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
nativeFunctionMatchingSummaryViewResultSchema,
nativeFunctionSummaryViewResultSchema,
@@ -335,6 +338,7 @@ const retainedViewProjectors = new Map<string, RetainedViewProjector>([
...NATIVE_OBSERVATION_OPERATIONS.map((operation) =>
retain(operation, projectNativeObservationView),
),
retain("procedure_references", projectNativeProcedureReferencesView),
retain("trace_native_values", projectNativeValueTraceView),
retain("inspect_erlang_beam", projectErlangBeamView),
retain("inspect_devicetree", projectDevicetreeView),
@@ -475,6 +479,7 @@ export const projectAnalysisView = (
parent.operation === "match_native_functions"
? NATIVE_FUNCTION_MATCHING_PREDICATE
: parent.operation === "analyze_function" ||
parent.operation === "procedure_references" ||
parent.operation === "procedure_pseudo_code" ||
parent.operation === "inspect_native_instruction" ||
parent.operation === "trace_feature" ||
@@ -1,3 +1,4 @@
import { nativeProcedureReferencesViewRequestSchema } from "./nativeProcedureReferencesView.js";
import { graphqlContractViewRequestSchema } from "../apiContract/graphqlContractQuery.js";
import { playwrightTraceViewRequestSchema } from "../playwrightTrace/playwrightTraceQuery.js";
import { apiContractViewRequestSchema } from "../apiContract/apiContractQuery.js";
@@ -94,6 +95,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
javascriptFactsViewRequestSchema,
nativeViewSchema,
nativeInventoryViewRequestSchema,
nativeProcedureReferencesViewRequestSchema,
nativeLoadImageViewRequestSchema,
artifactInventoryViewRequestSchema,
wasmViewRequestSchema,
@@ -14,6 +14,11 @@ import {
apiContractViewContextSchema,
apiContractViewItemsSchema,
} from "./apiContractView.js";
import {
procedureReferenceViewRequests,
procedureReferenceViewItems,
procedureReferenceSummarySchema,
} from "./nativeProcedureReferencesView.js";
import {
analysisViewCoverageSchema,
nullableCollectionViewCoverageSchema,
@@ -387,6 +392,7 @@ const viewResultBase = {
...NATIVE_OBSERVATION_OPERATIONS,
"trace_native_values",
"match_native_functions",
"procedure_references",
]),
parent_digest: digestSchema,
view_digest: digestSchema,
@@ -921,6 +927,39 @@ const nativeFeatureTracePageViewResultSchema = z
nativeFeatureTracePage("unknowns"),
]),
);
export const nativeProcedureReferencesSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
...nativeViewSourceContext,
parent_operation: z.literal("procedure_references"),
artifact: artifactSchema.nullable(),
summary: procedureReferenceSummarySchema,
});
const procedureReferencePage = <
Collection extends keyof typeof procedureReferenceViewRequests,
>(
collection: Collection,
) =>
z.strictObject({
...viewResultBase,
...nativeViewSourceContext,
kind: z.literal("native-procedure-references"),
parent_operation: z.literal("procedure_references"),
artifact: artifactSchema.nullable(),
view: procedureReferenceViewRequests[collection],
summary: procedureReferenceSummarySchema,
items: procedureReferenceViewItems[collection],
byte_limited: z.boolean(),
});
const procedureReferencePageViewResultSchema = z
.object({ kind: z.literal("native-procedure-references") })
.passthrough()
.pipe(
z.union([
procedureReferencePage("references"),
procedureReferencePage("unresolved_calls"),
]),
);
/** Compact provider load observations retain the independent verification state. */
export const nativeLoadImageSummaryViewResultSchema = z.strictObject({
@@ -1314,6 +1353,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
gpuCodeObjectsSummaryViewResultSchema,
nativeInventorySummaryViewResultSchema,
nativeFeatureTraceSummaryViewResultSchema,
nativeProcedureReferencesSummaryViewResultSchema,
nativeLoadImageSummaryViewResultSchema,
nativeFunctionMatchingSummaryViewResultSchema,
nativeFunctionSummaryViewResultSchema,
@@ -1353,6 +1393,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
rows: z.array(nativeInventoryViewRowSchema),
}),
nativeFeatureTracePageViewResultSchema,
procedureReferencePageViewResultSchema,
z.strictObject({
...viewResultBase,
kind: z.literal("native-load-image"),
@@ -0,0 +1,141 @@
import { expect, it } from "vitest";
import type { AnalysisViewParent } from "./analysisViewParent.js";
import {
procedureReferenceViewRequests,
projectNativeProcedureReferencesView,
} from "./nativeProcedureReferencesView.js";
const PAGE_BYTES = 1024 * 1024;
const selected = {
address: "0x1000",
name: "selected",
classification: null,
body: { available: false as const, reason: "not reported" },
};
const parentFor = (
references: readonly Record<string, unknown>[],
): AnalysisViewParent => ({
evidenceId: `ev_${"a".repeat(64)}`,
operation: "procedure_references",
predicateType: "rea.analysis",
limitations: [],
parameters: {},
normalizedResult: {
procedure: selected,
direction: "outgoing",
reference_kinds_available: false,
unresolved_calls: [],
references,
},
});
const edge = (
sourceAddress: string,
target: Record<string, unknown> | null,
) => ({
source_address: sourceAddress,
target_address: "0x2000",
source_procedure: selected,
target_procedure: target,
kind: { available: false as const, reason: "not reported" },
});
const viewFor = (limit: number) =>
procedureReferenceViewRequests.references.parse({
kind: "native-procedure-references",
collection: "references",
offset: 0,
limit,
});
const pageBytes = (value: unknown) => Buffer.byteLength(JSON.stringify(value));
it("omits an endpoint that no accepted row uses", () => {
const projected = projectNativeProcedureReferencesView(
parentFor([
edge("0x1001", null),
edge("0x1002", {
...selected,
address: "0x3000",
name: "x".repeat(1_048_000),
}),
]),
viewFor(8),
);
if (!projected.ok) throw projected.error;
if (projected.value.kind !== "native-procedure-references")
throw new Error("Expected a procedure-reference page");
if (projected.value.items.collection !== "references")
throw new Error("Expected reference rows");
expect(projected.value.byte_limited).toBe(true);
expect(projected.value.items.rows).toHaveLength(1);
expect(projected.value.items.endpoints).toEqual([]);
expect(pageBytes(projected.value)).toBeLessThanOrEqual(PAGE_BYTES);
});
it("recognizes a multibyte selected procedure after the page is partly full", () => {
const multibyte = {
...selected,
name: "雪".repeat(200_000),
};
const filler = {
...selected,
address: "0x3000",
name: "y".repeat(150_000),
};
const projected = projectNativeProcedureReferencesView(
{
...parentFor([edge("0x1001", filler), edge("0x1002", multibyte)]),
normalizedResult: {
procedure: multibyte,
direction: "outgoing",
reference_kinds_available: false,
unresolved_calls: [],
references: [
{
...edge("0x1001", filler),
source_procedure: null,
},
{
...edge("0x1002", multibyte),
source_procedure: null,
},
],
},
},
viewFor(8),
);
if (!projected.ok) throw projected.error;
if (projected.value.kind !== "native-procedure-references")
throw new Error("Expected a procedure-reference page");
if (projected.value.items.collection !== "references")
throw new Error("Expected reference rows");
expect(projected.value.items.rows).toHaveLength(2);
expect(projected.value.items.rows[1]?.target_procedure).toEqual({
kind: "selected-procedure",
});
expect(projected.value.items.endpoints).toHaveLength(1);
});
it("counts distinct shared endpoints inside the page allowance", () => {
const projected = projectNativeProcedureReferencesView(
parentFor(
Array.from({ length: 40 }, (_, index) =>
edge(`0x${(0x2000 + index).toString(16)}`, {
...selected,
address: `0x${(0x8000 + index).toString(16)}`,
name: "endpoint".repeat(4_000),
}),
),
),
viewFor(40),
);
if (!projected.ok) throw projected.error;
if (projected.value.kind !== "native-procedure-references")
throw new Error("Expected a procedure-reference page");
if (projected.value.items.collection !== "references")
throw new Error("Expected reference rows");
expect(projected.value.byte_limited).toBe(true);
expect(projected.value.items.endpoints).toHaveLength(
projected.value.items.rows.length,
);
expect(pageBytes(projected.value)).toBeLessThanOrEqual(PAGE_BYTES);
});
@@ -0,0 +1,391 @@
import { z } from "zod";
import { canonicalJson } from "../comparisonSemantics.js";
import { jsonValueSchema } from "../jsonValue.js";
import { procedureIdentitySchema } from "../native/procedureInfo.js";
import {
referenceEdgeSchema,
unresolvedCallSchema,
} from "../native/nativeReference.js";
import { procedureReferencesSchema } from "../native/procedureReferences.js";
import { err, ok } from "../result.js";
import {
createAnalysisViewResultReader,
type AnalysisViewParent,
} from "./analysisViewParent.js";
import {
assertOwnedView,
type AdmittedView,
type SummaryAnalysisView,
} from "./analysisViewShape.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
import type { JsonValue } from "../jsonValue.js";
import { jsonValueByteLength } from "../jsonValueByteLength.js";
import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
const page = {
kind: z.literal("native-procedure-references"),
offset: z.int().nonnegative().default(0),
limit: z.int().positive().default(64),
};
export const procedureReferenceViewRequests = {
references: z.strictObject({
...page,
collection: z.literal("references"),
source_address: z
.string()
.min(1)
.optional()
.describe("Match an exact retained source address."),
target_address: z
.string()
.min(1)
.optional()
.describe("Match an exact retained target address."),
}),
unresolved_calls: z.strictObject({
...page,
collection: z.literal("unresolved_calls"),
}),
} as const;
export const nativeProcedureReferencesViewRequestSchema = z.discriminatedUnion(
"collection",
[
procedureReferenceViewRequests.references,
procedureReferenceViewRequests.unresolved_calls,
],
);
const endpointSchema = z.discriminatedUnion("kind", [
z.strictObject({ kind: z.literal("null") }),
z.strictObject({ kind: z.literal("selected-procedure") }),
z.strictObject({
kind: z.literal("shared"),
index: z.int().nonnegative(),
}),
]);
const rowSchema = referenceEdgeSchema.extend({
source_procedure: endpointSchema,
target_procedure: endpointSchema,
source_index: z.int().nonnegative(),
});
export const procedureReferenceViewItems = {
references: z.strictObject({
collection: z.literal("references"),
endpoints: z.array(procedureIdentitySchema),
rows: z.array(rowSchema),
}),
unresolved_calls: z.strictObject({
collection: z.literal("unresolved_calls"),
rows: z.array(
z.strictObject({
source_index: z.int().nonnegative(),
value: unresolvedCallSchema,
}),
),
}),
} as const;
export const procedureReferenceSummarySchema = z.strictObject({
procedure: procedureIdentitySchema,
direction: z.enum(["incoming", "outgoing"]),
reference_kinds_available: z.boolean(),
reference_count: z.int().nonnegative(),
unresolved_call_count: z.int().nonnegative(),
});
const readSource = createAnalysisViewResultReader(procedureReferencesSchema);
// The analysis-view projection allowance leaves room for Evidence and MCP
// encoding expansion. Count JSON without serializing the full page.
const PAGE_BYTES = 1024 * 1024;
const DIGEST_BYTES = jsonValueByteLength({ view_digest: "0".repeat(64) }) - 1;
/** Page the retained result without re-querying the provider or changing source coverage. */
export const projectNativeProcedureReferencesView = (
parent: AnalysisViewParent,
view: AdmittedView,
) => {
if (view.kind !== "summary" && view.kind !== "native-procedure-references")
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message:
"Procedure-reference Evidence supports summary and native-procedure-references views.",
},
]),
);
assertOwnedView<
| SummaryAnalysisView
| z.output<typeof nativeProcedureReferencesViewRequestSchema>
>(view, "summary", "native-procedure-references");
const parsed = readSource(parent);
if (!parsed.ok) return parsed;
const source = parsed.value;
const summary = {
procedure: source.procedure,
direction: source.direction,
reference_kinds_available: source.reference_kinds_available,
reference_count: source.references.length,
unresolved_call_count: source.unresolved_calls.length,
};
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: "procedure_references" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: parent.artifact ?? null,
source_provider: parent.provider ?? null,
source_analysis_profile_digest: parent.analysisProfile?.digest ?? null,
source_parameters: jsonValueSchema.parse(parent.parameters ?? null),
limitations: boundedViewMessages(
parent.limitations,
parent.evidenceId,
"limitations",
),
unknowns: [
"Page coverage describes retained references, not provider discovery completeness or runtime execution.",
],
};
const tooLarge = () =>
err(
new AnalysisResourceConstraintError(
"inspect_analysis_view",
"transport",
"The selected procedure-reference view cannot fit the analysis-view byte budget.",
{ parent_evidence_id: parent.evidenceId, limit_bytes: PAGE_BYTES },
{
retryable: false,
remediationAction:
"An individual row or the page context exceeds the fixed 1,048,576-byte projection allowance. Use the complete input Evidence or its local bundle, or retrieve or export the retained parent by the reported ID. If a CLI failure includes a partial observation, that observation is the completed parent.",
},
),
);
if (view.kind === "summary") {
const result = {
...shared,
kind: "summary" as const,
view,
summary,
coverage: completeWithinViewCoverage(1, 1),
};
return jsonValueByteLength(result as JsonValue, PAGE_BYTES - DIGEST_BYTES) >
PAGE_BYTES - DIGEST_BYTES
? tooLarge()
: ok(result);
}
const common = {
...shared,
kind: "native-procedure-references" as const,
view,
summary,
};
const header = {
...common,
items:
view.collection === "references"
? { collection: view.collection, endpoints: [], rows: [] }
: { collection: view.collection, rows: [] },
byte_limited: false,
coverage: {
status: "complete-within-view",
examined: Number.MAX_SAFE_INTEGER,
total: Number.MAX_SAFE_INTEGER,
next_offset: Number.MAX_SAFE_INTEGER,
exhausted: false,
},
};
let bytes =
DIGEST_BYTES +
jsonValueByteLength(header as JsonValue, PAGE_BYTES - DIGEST_BYTES);
if (bytes > PAGE_BYTES) return tooLarge();
const referenceRows: z.infer<typeof rowSchema>[] = [];
const unresolvedRows: {
source_index: number;
value: z.infer<typeof unresolvedCallSchema>;
}[] = [];
let byteLimited = false;
const offer = (row: JsonValue, accepted: number, extra = 0) => {
const size =
jsonValueByteLength(row, PAGE_BYTES - bytes) +
(accepted > 0 ? 1 : 0) +
extra;
if (bytes + size > PAGE_BYTES) {
byteLimited = true;
return false;
}
bytes += size;
return true;
};
if (view.collection === "unresolved_calls") {
const calls = source.unresolved_calls;
const end = Math.min(calls.length, view.offset + view.limit);
for (let index = view.offset; index < end; index += 1) {
const value = calls[index];
if (value === undefined) break;
const row = { source_index: index, value };
if (!offer(row, unresolvedRows.length)) break;
unresolvedRows.push(row);
}
if (byteLimited && unresolvedRows.length === 0) return tooLarge();
return ok({
...common,
view,
items: { collection: "unresolved_calls" as const, rows: unresolvedRows },
byte_limited: byteLimited,
coverage: pageViewCoverage(
view.offset,
unresolvedRows.length,
calls.length,
),
});
}
const selected = canonicalJson(source.procedure);
const endpoints: z.infer<typeof procedureIdentitySchema>[] = [];
const endpointIndexes = new Map<string, number>();
let longestIdentityBytes = Buffer.byteLength(selected);
const planEndpoints = (
sourceProcedure: z.infer<typeof procedureIdentitySchema> | null,
targetProcedure: z.infer<typeof procedureIdentitySchema> | null,
) => {
const pending: z.infer<typeof procedureIdentitySchema>[] = [];
const pendingKeys: string[] = [];
let extra = 0;
let overflow = false;
const resolve = (value: z.infer<typeof procedureIdentitySchema> | null) => {
if (value === null) return { kind: "null" as const };
const comma = endpoints.length + pending.length > 0 ? 1 : 0;
const measured = jsonValueByteLength(
value as JsonValue,
Math.max(0, PAGE_BYTES - bytes - extra),
);
const size = measured + comma;
// `measured` is a UTF-8 lower bound. It can skip canonicalization only
// when it already exceeds every stored identity's UTF-8 length.
if (
bytes + extra + size > PAGE_BYTES &&
measured > longestIdentityBytes
) {
overflow = true;
return undefined;
}
const key = canonicalJson(value);
if (key === selected) return { kind: "selected-procedure" as const };
const existing = endpointIndexes.get(key);
if (existing !== undefined)
return { kind: "shared" as const, index: existing };
const pendingIndex = pendingKeys.indexOf(key);
if (pendingIndex !== -1)
return {
kind: "shared" as const,
index: endpoints.length + pendingIndex,
};
if (bytes + extra + size > PAGE_BYTES) {
overflow = true;
return undefined;
}
extra += size;
pending.push(value);
pendingKeys.push(key);
const keyBytes = Buffer.byteLength(key);
if (keyBytes > longestIdentityBytes) longestIdentityBytes = keyBytes;
return {
kind: "shared" as const,
index: endpoints.length + pending.length - 1,
};
};
const resolvedSource = resolve(sourceProcedure);
const resolvedTarget = resolve(targetProcedure);
if (
overflow ||
resolvedSource === undefined ||
resolvedTarget === undefined
)
return { overflow: true as const };
return {
overflow: false as const,
resolvedSource,
resolvedTarget,
pending,
pendingKeys,
extra,
};
};
const filtered =
view.source_address !== undefined || view.target_address !== undefined;
const references = source.references;
const visit = function* () {
if (!filtered) {
const end = Math.min(references.length, view.offset + view.limit);
for (let index = view.offset; index < end; index += 1) {
const reference = references[index];
if (reference !== undefined) yield [index, reference] as const;
}
return;
}
let seen = 0;
for (const [index, reference] of references.entries()) {
if (
(view.source_address !== undefined &&
reference.source_address !== view.source_address) ||
(view.target_address !== undefined &&
reference.target_address !== view.target_address)
)
continue;
if (seen >= view.offset && seen < view.offset + view.limit)
yield [index, reference] as const;
seen += 1;
}
};
let filteredTotal = 0;
if (filtered) {
for (const reference of references) {
if (
(view.source_address === undefined ||
reference.source_address === view.source_address) &&
(view.target_address === undefined ||
reference.target_address === view.target_address)
)
filteredTotal += 1;
}
}
for (const [index, reference] of visit()) {
const planned = planEndpoints(
reference.source_procedure,
reference.target_procedure,
);
if (planned.overflow) {
byteLimited = true;
break;
}
const row = {
...reference,
source_index: index,
source_procedure: planned.resolvedSource,
target_procedure: planned.resolvedTarget,
};
if (!offer(row as JsonValue, referenceRows.length, planned.extra)) break;
for (const [offset, value] of planned.pending.entries()) {
endpointIndexes.set(planned.pendingKeys[offset]!, endpoints.length);
endpoints.push(value);
}
referenceRows.push(row);
}
if (byteLimited && referenceRows.length === 0) return tooLarge();
return ok({
...common,
view,
items: {
collection: "references" as const,
endpoints,
rows: referenceRows,
},
byte_limited: byteLimited,
coverage: pageViewCoverage(
view.offset,
referenceRows.length,
filtered ? filteredTotal : references.length,
),
});
};
+15
View File
@@ -0,0 +1,15 @@
import { z } from "zod";
import { procedureIdentitySchema } from "./procedureInfo.js";
import {
referenceEdgeSchema,
unresolvedCallSchema,
} from "./nativeReference.js";
/** Complete provider-observed references and unresolved calls for one procedure. */
export const procedureReferencesSchema = z.object({
procedure: procedureIdentitySchema,
direction: z.enum(["incoming", "outgoing"]),
reference_kinds_available: z.boolean(),
unresolved_calls: z.array(unresolvedCallSchema),
references: z.array(referenceEdgeSchema),
});
@@ -50,7 +50,6 @@ const MCP_TOOLS_WITHOUT_DEDICATED_CLI = Object.freeze([
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_references",
"resolve_containing_procedure",
"set_address_name",
"set_addresses_names",
@@ -0,0 +1,153 @@
import { readFile, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Cli } from "incur";
import pino from "pino";
import { expect, it } from "vitest";
import {
createAnalysisExecution,
type AnalysisProvider,
} from "../../../src/application/AnalysisProvider.js";
import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
import type { DirectAnalysisDependencies } from "../../../src/application/DirectAnalysisDependencies.js";
import { registerCoreNativeCommands } from "../../../src/cli/coreNativeCommands.js";
import { parseConfig } from "../../../src/config/parseConfig.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { ok } from "../../../src/domain/result.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const identity = { id: "fixture", name: "Fixture", version: "1" } as const;
const effects = {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
};
const procedure = {
address: "0x1000",
name: "selected",
classification: null,
body: { available: false as const, reason: "unavailable" },
};
const result = {
procedure,
direction: "incoming" as const,
reference_kinds_available: false,
references: [],
unresolved_calls: [{ address: "0x1010", reason: "unknown target" }],
};
it.each([false, true])(
"exports linked source Evidence or refuses missing closure (missing=%s)",
async (missing) => {
const root = await createTestTempDirectory("rea-procedure-references-cli-");
const path = join(root, "fixture.hop"),
output = join(root, "evidence.json");
await writeFile(path, "synthetic analysis database");
const history = createEvidence(undefined, identity, {
operation: "health",
parameters: {},
result: "project history",
...(missing ? { evidenceLinks: ["ev_" + "a".repeat(64)] } : {}),
});
const provider: AnalysisProvider = {
identity: () => identity,
capabilities: () => [
{
provider: identity,
operation: "procedure_references",
available: true,
reason: null,
cachePolicy: "live",
limitations: [],
effects,
},
],
resolveAnalysisProfile: async () =>
ok({ profile: createAnalysisProfile(identity, { fixture: true }) }),
createClient: () => ({
execute: async () =>
ok(
createAnalysisExecution(result, identity, {
sourceEvidence: [history],
}),
),
close: async () => ok(null),
}),
};
const createSession = () => createTestBinarySession(provider);
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: createSession,
createNativeDispatchBinarySession: createSession,
createArtifactBinarySession: createSession,
createNativeBinarySession: createSession,
createManagedBinarySession: createSession,
};
const cli = Cli.create("procedure-references", { sync: false });
registerCoreNativeCommands(
cli,
pino({ enabled: false }),
(target, tool, args, options) =>
runDirectAnalysis(dependencies, target, tool, args, options),
);
let stdout = "";
await cli.serve(
[
"procedure-references",
path,
"selected",
"--provider",
"fixture",
"--direction",
"incoming",
"--detail",
"summary",
"--evidence_output",
output,
"--json",
],
{
env: {},
stdout: (text) => {
stdout += text;
},
exit: () => undefined,
},
);
const response = JSON.parse(stdout);
if (missing) {
expect(response).toMatchObject({
code: "evidence_integrity_mismatch",
details: { reason: "missing", evidence_id: "ev_" + "a".repeat(64) },
});
await expect(stat(output)).rejects.toMatchObject({ code: "ENOENT" });
} else {
const bundle = parseEvidenceBundle(
JSON.parse(await readFile(output, "utf8")),
);
const parent = bundle.records.find(
(record) => record.operation === "procedure_references",
);
expect(response.summary.normalized_result).toMatchObject({
parent_evidence_id: parent?.evidence_id,
summary: {
reference_count: 0,
unresolved_call_count: 1,
direction: "incoming",
},
});
expect(parent?.evidence_links).toEqual([history.evidence_id]);
expect(
bundle.records.some(
(record) => record.evidence_id === history.evidence_id,
),
).toBe(true);
}
},
);
@@ -0,0 +1,297 @@
import { join } from "node:path";
import { expect, it } from "vitest";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
const selected = {
address: "0x1000",
name: "selected",
classification: null,
body: { available: false as const, reason: "not reported" },
};
const other = { ...selected, address: "0x2000", name: "other" };
const edge = {
source_address: "0x1001",
target_address: "0x2000",
source_procedure: selected,
target_procedure: other,
kind: { available: false as const, reason: "not reported" },
};
const fixture = {
procedure: selected,
direction: "outgoing" as const,
reference_kinds_available: false,
references: [
edge,
{ ...edge, source_address: "0x1002", target_procedure: null },
edge,
],
unresolved_calls: [{ address: "0x1003", reason: "indirect destination" }],
};
const setup = async (profile: "full" | "compact") => {
const session = createTestBinarySession(() => {
throw new Error("View opened provider");
});
return {
...(await createApplicationMcpHarness({
session,
clientIdentity: { name: "procedure-reference-view", version: "1" },
serverOptions: {
environment: {
...process.env,
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
},
},
})),
session,
};
};
it.each(["full", "compact"] as const)(
"pages procedure reference Evidence across a %s offline handoff",
async (profile) => {
const parent = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "procedure_references",
parameters: { procedure: "selected", direction: "outgoing" },
result: fixture,
limitations: ["fixture coverage"],
},
);
const first = await setup(profile);
const source = {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
};
const invoke = async (
client: typeof first.client,
name: string,
args: Record<string, unknown>,
) => {
const reply = await client.callTool({ name, arguments: args });
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
return reply.structuredContent;
};
const view = async (
client: typeof first.client,
request: Record<string, unknown>,
) =>
analysisViewResultSchema.parse(
parseEvidence(
await invoke(client, "inspect_analysis_view", {
source,
view: { kind: "native-procedure-references", ...request },
}),
).normalized_result,
);
const path = join(
await createTestTempDirectory("rea-procedure-reference-view-"),
"bundle.json",
);
try {
expect(first.session.recordEvidence(parent).ok).toBe(true);
const firstPage = await view(first.client, {
collection: "references",
source_address: "0x1001",
target_address: "0x2000",
offset: 0,
limit: 1,
});
const secondPage = await view(first.client, {
collection: "references",
source_address: "0x1001",
target_address: "0x2000",
offset: 1,
limit: 1,
});
expect(firstPage).toMatchObject({
parent_evidence_id: parent.evidence_id,
summary: {
procedure: selected,
direction: "outgoing",
reference_count: 3,
unresolved_call_count: 1,
},
coverage: { total: 2, next_offset: 1 },
items: {
rows: [
{
source_index: 0,
source_procedure: { kind: "selected-procedure" },
target_procedure: { kind: "shared", index: 0 },
kind: edge.kind,
},
],
},
});
expect(secondPage).toMatchObject({
coverage: { total: 2, next_offset: null },
items: { rows: [{ source_index: 2 }] },
});
expect(
analysisViewResultSchema.safeParse({
...firstPage,
items: { collection: "unresolved_calls", rows: [] },
}).success,
).toBe(false);
expect(
await view(first.client, {
collection: "unresolved_calls",
offset: 0,
limit: 1,
}),
).toMatchObject({
items: {
rows: [{ source_index: 0, value: fixture.unresolved_calls[0] }],
},
});
expect(
await view(first.client, {
collection: "references",
source_address: "0xmissing",
}),
).toMatchObject({ coverage: { status: "empty", total: 0 } });
const invalid = await first.client.callTool({
name: "inspect_analysis_view",
arguments: {
source,
view: {
kind: "native-procedure-references",
collection: "unresolved_calls",
source_address: "0x1000",
},
},
});
expect(invalid.isError).toBe(true);
await invoke(first.client, "export_evidence_bundle", { path });
} finally {
await first.close();
}
const second = await setup(profile);
try {
await invoke(second.client, "import_evidence_bundle", { path });
expect(
await view(second.client, {
collection: "references",
offset: 1,
limit: 1,
}),
).toMatchObject({
items: {
rows: [{ source_index: 1, target_procedure: { kind: "null" } }],
},
});
} finally {
await second.close();
}
},
);
it("bounds a requested page by bytes and keeps oversized-row recovery available", async () => {
const largeReason = "observed reference limitation ".repeat(90);
const rows = Array.from({ length: 900 }, (_, index) => ({
...edge,
source_address: `0x${(0x1000 + index).toString(16)}`,
kind: { available: false as const, reason: largeReason },
}));
const provider = { id: "fixture", name: "Fixture", version: "1" };
const parent = createEvidence(undefined, provider, {
operation: "procedure_references",
parameters: {},
result: { ...fixture, references: rows },
});
const oversize = createEvidence(undefined, provider, {
operation: "procedure_references",
parameters: {},
result: {
...fixture,
references: [
{
...edge,
target_procedure: { ...other, name: "x".repeat(1_100_000) },
},
],
},
});
const { session, client, close } = await setup("compact");
const call = (
id: string,
collection: "references" | "unresolved_calls",
offset: number,
) =>
client.callTool({
name: "inspect_analysis_view",
arguments: {
source: { kind: "retained-evidence", evidence_id: id },
view: {
kind: "native-procedure-references",
collection,
offset,
limit: 900,
},
},
});
try {
expect(session.recordEvidence(parent).ok).toBe(true);
expect(session.recordEvidence(oversize).ok).toBe(true);
const first = await call(parent.evidence_id, "references", 0);
expect(first.isError, JSON.stringify(first)).not.toBe(true);
const page = analysisViewResultSchema.parse(
parseEvidence(first.structuredContent).normalized_result,
);
if (
page.kind !== "native-procedure-references" ||
page.items.collection !== "references"
)
throw new Error("Expected references page");
expect(page.byte_limited).toBe(true);
expect(page.coverage.total).toBe(rows.length);
const nextOffset = page.coverage.next_offset;
expect(nextOffset).toBe(page.items.rows.length);
if (nextOffset === null) throw new Error("Expected continuation");
const next = await call(parent.evidence_id, "references", nextOffset);
expect(next.isError, JSON.stringify(next)).not.toBe(true);
const nextPage = analysisViewResultSchema.parse(
parseEvidence(next.structuredContent).normalized_result,
);
if (
nextPage.kind !== "native-procedure-references" ||
nextPage.items.collection !== "references"
)
throw new Error("Expected references continuation");
expect(nextPage.items.rows[0]?.source_index).toBe(nextOffset);
const refused = await call(oversize.evidence_id, "references", 0);
expect(refused.isError).toBe(true);
expect(parseMcpToolError(refused)).toMatchObject({
error: {
code: "resource_constraint",
remediation: {
action: expect.stringContaining("export the retained parent"),
},
details: {
reported_limits: {
parent_evidence_id: oversize.evidence_id,
limit_bytes: 1_048_576,
},
},
},
});
const otherPage = await call(oversize.evidence_id, "unresolved_calls", 0);
expect(otherPage.isError, JSON.stringify(otherPage)).not.toBe(true);
expect(
parseEvidence(otherPage.structuredContent).normalized_result,
).toMatchObject({
items: { rows: [{ value: fixture.unresolved_calls[0] }] },
});
} finally {
await close();
}
});