mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 09:52:27 +00:00
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>
99 lines
3.2 KiB
TypeScript
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 };
|
|
},
|
|
};
|
|
};
|