Files
reverse-engineering-anything/src/domain/analysisView/nativeProcedureReferencesView.ts
T
morlutoandCursor Agent b81da90354 feat(native): page retained procedure references (#8588)
* feat(native): page retained feature traces

* fix(snapshot): retain linked source evidence for replay

* test(snapshot): verify linked foreign evidence stays scoped

* feat(native): page retained procedure references

* docs(mcp): clarify imported residual questions

* docs(mcp): guide trace reference classification

* 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.

* ci: rerun pull request checks

The merge of origin/main did not start the CI workflow for this head.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* 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.

* test: stabilize capture decoder exhaustion coverage

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-08 02:16:24 +00:00

392 lines
13 KiB
TypeScript

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,
),
});
};