Files
reverse-engineering-anything/src/application/native/NativeValueTraceBudget.ts
T
Kaoru0822andClaude Opus 5.5 73ff6ca0c8 fix(native): separate value-trace page omission from traversal truncation (#8309)
trace_native_values set one truncated flag for two different facts: the
returned page omits retained nodes, and a budget stopped or narrowed the
traversal. A last page of a complete trace and a node- or edge-capped trace
both reported truncated: true with next_offset null and no unknown naming the
cap, so a caller could not tell whether to follow pages, raise a budget, or
treat the examined graph as exhausted.

Results now carry page_truncated for page omission and a traversal object
that is complete, or truncated with the observed reasons in schema order:
max_depth, max_functions, max_call_sites, max_nodes, max_edges,
node_payload_bytes, provider_pcode_truncated and ambiguous_call_candidates.
truncated keeps its previous meaning as the union of both.

A new budget owner admits nodes and edges and records every cause together
with a located unknown, only after a budget rejects work; an exactly filled
budget is no longer truncation. Depth and ambiguous-candidate limits are
resolved after traversal, so a destination analyzed or queued through a
shallower path is no longer reported as depth-limited. Once an edge is
rejected, remaining call sites in that procedure are not resolved.

Fixes #8283

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:59:58 -07:00

99 lines
3.2 KiB
TypeScript

import type { z } from "zod";
import { jsonParts } from "../../domain/jsonSerialization.js";
import {
nativeValueTraceTruncationReasonSchema,
type nativeValueTraceSchema,
type NativeValueTraceTruncationReason,
} from "../../domain/native/nativeValueTrace.js";
type Trace = z.output<typeof nativeValueTraceSchema>;
type TraceNode = Trace["nodes"][number];
type TraceEdge = Trace["edges"][number];
type TraceUnknown = Trace["unknowns"][number];
/** Retained node JSON allowance; this release has no runtime override. */
const NODE_PAYLOAD_BYTES = 8 * 1024 * 1024;
const rowBudgetReasons = {
max_nodes: "Node budget reached",
node_payload_bytes: "Node payload budget reached",
max_edges: "Edge budget reached",
} as const;
/**
* Own row admission and every observed cause of a narrowed traversal. Each
* cause is recorded with a located unknown, and a row budget is recorded only
* after it rejects a row, so an exactly filled budget is not truncation.
*/
export const createValueTraceBudget = (
limits: { readonly max_nodes: number; readonly max_edges: number },
unknowns: TraceUnknown[],
) => {
const nodes: TraceNode[] = [];
const edges: TraceEdge[] = [];
const causes = new Set<NativeValueTraceTruncationReason>();
let bytes = 0;
let nodesExhausted = false;
let edgesExhausted = false;
const narrow = (
reason: NativeValueTraceTruncationReason,
unknown: TraceUnknown,
): false => {
causes.add(reason);
unknowns.push(unknown);
return false;
};
const reject = (procedure: string, reason: keyof typeof rowBudgetReasons) =>
narrow(reason, {
procedure,
address: null,
reason: rowBudgetReasons[reason],
});
return {
nodes,
edges,
narrow,
retainNode(value: TraceNode): boolean {
if (nodesExhausted) return false;
if (nodes.length >= limits.max_nodes) {
nodesExhausted = true;
return reject(value.procedure, "max_nodes");
}
let size = 0;
// Reuse bounded JSON encoding to count escapes and UTF-8 expansion
// before allocating a complete serialized node that might not fit.
for (const part of jsonParts(value)) {
size += Buffer.byteLength(part);
if (size > NODE_PAYLOAD_BYTES - bytes) {
nodesExhausted = true;
return reject(value.procedure, "node_payload_bytes");
}
}
bytes += size;
nodes.push(value);
return true;
},
retainEdge(value: TraceEdge, procedure: string): boolean {
if (edgesExhausted) return false;
if (edges.length >= limits.max_edges) {
edgesExhausted = true;
return reject(procedure, "max_edges");
}
edges.push(value);
return true;
},
/** A rejected edge means no later call edge or expansion can be retained. */
edgesExhausted: () => edgesExhausted,
/** Traversal stops after the procedure in which a row budget rejected work. */
exhausted: () => nodesExhausted || edgesExhausted,
traversal: (): Trace["traversal"] => {
const reasons = nativeValueTraceTruncationReasonSchema.options.filter(
(reason) => causes.has(reason),
);
return reasons.length === 0
? { status: "complete" }
: { status: "truncated", reasons };
},
};
};