perf: retain call path ancestry once per visited node

This commit is contained in:
morluto committed 2026-10-24 01:47:30 +08:00
1 parent 8aa40e9a61
commit 5e4ec0fa1b
3 files changed
+225 -21

No files matched your search

+26 -21
View File
@@ -35,11 +35,14 @@ export interface CallPathTraceInput {
readonly direction: CallRelationshipDirection;
}
interface TraceNode {
readonly address: string;
readonly depth: number;
readonly parent: TraceNode | undefined;
}
interface TraceState {
readonly visited: Map<
string,
{ readonly depth: number; readonly path: string[] }
>;
readonly visited: Map<string, TraceNode>;
readonly queue: string[];
readonly edges: Map<
string,
@@ -64,7 +67,7 @@ interface AddressExpansion {
readonly input: CallPathTraceInput;
readonly state: TraceState;
readonly address: string;
readonly current: { readonly depth: number; readonly path: string[] };
readonly current: TraceNode;
readonly signal?: AbortSignal;
}
@@ -123,7 +126,9 @@ export const traceCallPath = async (
};
const createTraceState = (input: CallPathTraceInput): TraceState => ({
visited: new Map([[input.start, { depth: 0, path: [input.start] }]]),
visited: new Map([
[input.start, { address: input.start, depth: 0, parent: undefined }],
]),
queue: [input.start],
edges: new Map(),
failures: [],
@@ -195,23 +200,15 @@ const expandAddress = async (
relatedAddress,
discoveryDepth: current.depth + 1,
});
const path = state.visited.get(relatedAddress)?.path ?? [
...current.path,
relatedAddress,
];
if (state.visited.has(relatedAddress)) {
if (relatedAddress === input.goal) {
state.traversalPath = path;
return ok(true);
}
continue;
}
state.visited.set(relatedAddress, {
if (state.visited.has(relatedAddress)) continue;
const discovered: TraceNode = {
address: relatedAddress,
depth: current.depth + 1,
path,
});
parent: current,
};
state.visited.set(relatedAddress, discovered);
if (relatedAddress === input.goal) {
state.traversalPath = path;
state.traversalPath = reconstructPath(discovered);
return ok(true);
}
state.queue.push(relatedAddress);
@@ -219,6 +216,14 @@ const expandAddress = async (
return ok(false);
};
/** Only the selected goal needs a path; each discovered node owns one parent link. */
const reconstructPath = (goal: TraceNode): string[] => {
const path: string[] = [];
for (let node: TraceNode | undefined = goal; node; node = node.parent)
path.push(node.address);
return path.reverse();
};
const recordEdge = (
input: CallPathTraceInput,
state: TraceState,
@@ -214,6 +214,58 @@ describe("enhanced MCP graph caveats", () => {
});
});
describe("trace_call_path MCP path selection", () => {
it.each(["forward", "backward"] as const)(
"selects the first shortest %s path through a diamond with cycles",
async (direction) => {
const expanded: string[] = [];
const related: Readonly<Record<string, readonly string[]>> = {
"0x1": ["0x3", "0x2", "0x2"],
"0x2": ["0x1", "0x2", "0x4"],
"0x3": ["0x4"],
};
const client = await connect({
execute: async (name, arguments_) => {
if (name === "procedure_address")
return relationshipExecution(String(arguments_.procedure));
expect(name).toBe(
direction === "forward" ? "procedure_callees" : "procedure_callers",
);
const address = String(arguments_.procedure);
expanded.push(address);
return relationshipExecution(related[address] ?? [], [], {
status: "complete",
});
},
});
const trace = await client.callTool({
name: "trace_call_path",
arguments: { start: "0x1", goal: "0x4", direction },
});
expect(trace.isError).not.toBe(true);
expect(trace.structuredContent).toMatchObject({
normalized_result: {
goal_status: "reached",
traversal_path: ["0x1", "0x2", "0x4"],
nodes: [
{ address: "0x1", depth: 0 },
{ address: "0x2", depth: 1 },
{ address: "0x3", depth: 1 },
{ address: "0x4", depth: 2 },
],
truncated: false,
failures: [],
relationship_coverage: {
"0x1": { status: "complete" },
"0x2": { status: "complete" },
},
},
});
expect(expanded).toEqual(["0x1", "0x2"]);
},
);
});
describe("trace_call_path MCP outcome semantics", () => {
it("reports an unreachable goal as unknown when relationship coverage is partial", async () => {
const client = await connect({
@@ -0,0 +1,147 @@
import { expect, it } from "vitest";
import { z } from "zod";
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
import { traceCallPath } from "../../src/application/CallPathTracing.js";
import { ok } from "../../src/domain/result.js";
const traceSchema = z.object({
goal_status: z.string(),
traversal_path: z.array(z.string()),
nodes: z.array(z.object({ address: z.string(), depth: z.number() })),
edges: z.array(
z.object({
source_address: z.string(),
target_address: z.string(),
discovery_depth: z.number(),
}),
),
});
const addresses = ["0x1", "0x2", "0x3"];
/** Enumerate simple paths independently of the production breadth-first traversal. */
const pathsFrom = (
graph: ReadonlyMap<string, readonly string[]>,
path: string[],
): string[][] => [
path,
...(graph.get(path.at(-1)!) ?? []).flatMap((next) =>
path.includes(next) ? [] : pathsFrom(graph, [...path, next]),
),
];
it("matches a simple-path oracle for every three-node directed graph in both directions", async () => {
const possible = addresses.flatMap((from) =>
addresses.filter((to) => to !== from).map((to) => [from, to] as const),
);
for (let mask = 0; mask < 2 ** possible.length; mask++) {
const edges = possible.filter((_, index) => mask & (1 << index));
for (const direction of ["forward", "backward"] as const) {
const graph = new Map(
addresses.map((address) => [
address,
edges
.filter(
([from, to]) => (direction === "forward" ? from : to) === address,
)
.map(([from, to]) => (direction === "forward" ? to : from)),
]),
);
const paths = pathsFrom(graph, ["0x1"]).sort(
(a, b) =>
a.length - b.length ||
JSON.stringify(a).localeCompare(JSON.stringify(b)),
);
const depth = (address: string) =>
paths.find((path) => path.at(-1) === address)!.length - 1;
for (const goal of [undefined, ...addresses]) {
const expanded: string[] = [];
const result = await traceCallPath(
async (_, args) => ok(String(args.procedure)),
async (_, args) => {
const address = String(args.procedure);
expanded.push(address);
// Deliberately duplicated and reversed producer order.
const related = [...(graph.get(address) ?? [])].reverse();
return ok(
createAnalysisExecution(
[...related, ...related],
{ id: "oracle", name: "Oracle", version: null },
{ relationshipCoverage: { status: "complete" } },
),
);
},
{ start: "0x1", goal, direction },
);
if (!result.ok) throw result.error;
const trace = traceSchema.parse(result.value);
const expected =
goal === undefined
? []
: (paths.find((path) => path.at(-1) === goal) ?? []);
expect(trace.traversal_path).toEqual(expected);
expect(trace.goal_status).toBe(
goal === undefined
? "not_requested"
: expected.length > 0
? "reached"
: "not_reached",
);
expect(new Set(expanded).size).toBe(expanded.length);
if (goal !== undefined) continue;
const reachable = new Set(paths.map((path) => path.at(-1)!));
expect(trace.nodes).toEqual(
[...reachable]
.map((address) => ({ address, depth: depth(address) }))
.sort(
(a, b) => a.depth - b.depth || a.address.localeCompare(b.address),
),
);
expect(trace.edges).toEqual(
edges
.filter(([from, to]) =>
reachable.has(direction === "forward" ? from : to),
)
.map(([from, to]) => ({
source_address: from,
target_address: to,
discovery_depth: depth(direction === "forward" ? from : to) + 1,
}))
.sort(
(a, b) =>
a.discovery_depth - b.discovery_depth ||
a.source_address.localeCompare(b.source_address) ||
a.target_address.localeCompare(b.target_address),
),
);
}
}
}
});
it("reconstructs a deep chain iteratively and preserves its discovery depths", async () => {
const count = 5000;
const address = (index: number) => `0x${index.toString(16).padStart(4, "0")}`;
const result = await traceCallPath(
async (_, args) => ok(String(args.procedure)),
async (_, args) => {
const next = Number.parseInt(String(args.procedure).slice(2), 16) + 1;
return ok(
createAnalysisExecution(
next < count ? [address(next)] : [],
{ id: "chain", name: "Chain", version: null },
{ relationshipCoverage: { status: "complete" } },
),
);
},
{ start: address(0), goal: address(count - 1), direction: "forward" },
);
if (!result.ok) throw result.error;
const trace = traceSchema.parse(result.value);
expect(trace.traversal_path).toEqual(
Array.from({ length: count }, (_, index) => address(index)),
);
expect(trace.nodes.at(-1)).toEqual({
address: address(count - 1),
depth: count - 1,
});
});