Files
reverse-engineering-anything/src/application/CallPathTracing.ts
T
Kaoru0822andClaude Opus 5.5 0ecf3f2438 perf(mcp): keep Playwright and type-only imports off MCP startup (#1691)
* fix(build): emit no runtime imports for type-only bindings

Under verbatimModuleSyntax, an import whose specifiers all carry inline
`type` modifiers compiles to `import {} from "..."`, a side-effect import
that still resolves and evaluates the module. 78 compiled modules carried
one, including `import {} from "playwright-core"` in the browser scenario
session, which loaded Playwright's module graph at MCP startup.

Use top-level `import type` for these bindings and enable oxlint's
typescript/no-import-type-side-effects rule so the pattern cannot return.

Refs #1685

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(mcp): load Playwright when a scenario launches, not at startup

The browser scenario and Electron active providers are constructed before
the stdio server serves, and both bound Playwright's launchers at module
load. Resolve `chromium` and `_electron` from playwright-core when a
capture opens instead, as the web module resolver already does.

MCP startup now resolves 1,312 modules (4,902 resolutions) instead of
1,598 (6,076) before reading the first request, and no longer loads
playwright-core. Provider construction, the tool catalog, and capability
inventory are unchanged.

A boundary test spawns the stdio server, initializes, lists tools, and
asserts playwright-core and isomorphic-git were not resolved; the CLI
probe now exercises the real launch path to prove detection.

Closes #1685

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:04:05 -07:00

222 lines
6.7 KiB
TypeScript

import type { AnalysisOperation } from "./AnalysisProvider.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { parseRelatedAddresses } from "../domain/hopperValues.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { ok, type Result } from "../domain/result.js";
import type { EnhancedResult } from "./EnhancedToolTypes.js";
import { resolveProcedureAddress } from "./ProcedureAddressResolution.js";
type AnalysisCall = (
name: AnalysisOperation,
arguments_: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal,
) => Promise<Result<JsonValue, AnalysisError>>;
export interface CallPathTraceInput {
readonly start: string;
readonly goal?: string | undefined;
readonly direction: "forward" | "backward";
}
interface TraceState {
readonly visited: Map<
string,
{ readonly depth: number; readonly path: string[] }
>;
readonly queue: string[];
readonly edges: Map<
string,
{
readonly source_address: string;
readonly target_address: string;
readonly discovery_depth: number;
}
>;
readonly failures: Array<{
readonly address: string;
readonly error: ReturnType<typeof projectAnalysisError>;
}>;
readonly residual: Set<string>;
queueIndex: number;
traversalPath: string[];
}
interface AddressExpansion {
readonly input: CallPathTraceInput;
readonly state: TraceState;
readonly address: string;
readonly current: { readonly depth: number; readonly path: string[] };
readonly signal?: AbortSignal;
}
interface DiscoveredEdge {
readonly address: string;
readonly relatedAddress: string;
readonly discoveryDepth: number;
}
/** Trace direct caller/callee relationships until the graph is exhausted or the goal is reached. */
export const traceCallPath = async (
call: AnalysisCall,
input: CallPathTraceInput,
signal?: AbortSignal,
): EnhancedResult => {
const start = await resolveProcedureAddress(call, input.start, signal);
if (!start.ok) return start;
let goal: string | undefined;
if (input.goal !== undefined) {
const resolved = await resolveProcedureAddress(call, input.goal, signal);
if (!resolved.ok) return resolved;
goal = resolved.value;
}
const resolvedInput = {
start: start.value,
...(goal === undefined ? {} : { goal }),
direction: input.direction,
};
const state = createTraceState(resolvedInput);
while (
state.queueIndex < state.queue.length &&
state.traversalPath.length === 0
) {
const address = state.queue[state.queueIndex++];
if (address === undefined) break;
const current = state.visited.get(address);
if (current === undefined) continue;
const found = await expandAddress(call, {
input: resolvedInput,
state,
address,
current,
...(signal === undefined ? {} : { signal }),
});
if (found) break;
}
return ok(projectTrace(resolvedInput, state));
};
const createTraceState = (input: CallPathTraceInput): TraceState => ({
visited: new Map([[input.start, { depth: 0, path: [input.start] }]]),
queue: [input.start],
edges: new Map(),
failures: [],
residual: new Set(),
queueIndex: 0,
traversalPath: input.goal === input.start ? [input.start] : [],
});
const expandAddress = async (
call: AnalysisCall,
expansion: AddressExpansion,
): Promise<boolean> => {
const { input, state, address, current, signal } = expansion;
const relation = input.direction === "forward" ? "callees" : "callers";
const tool =
input.direction === "forward" ? "procedure_callees" : "procedure_callers";
const result = await call(tool, { procedure: address }, signal);
if (!result.ok) {
state.failures.push({
address,
error: projectAnalysisError(result.error),
});
state.residual.add(`Call relationships were unavailable for ${address}.`);
return false;
}
const related = parseRelatedAddresses(result.value, relation);
if (!related.ok) {
state.failures.push({
address,
error: projectAnalysisError(related.error),
});
state.residual.add(`Call relationships were unreadable for ${address}.`);
return false;
}
for (const relatedAddress of sortedUniqueAddresses(related.value)) {
recordEdge(input, state, {
address,
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 true;
}
continue;
}
state.visited.set(relatedAddress, {
depth: current.depth + 1,
path,
});
if (relatedAddress === input.goal) {
state.traversalPath = path;
return true;
}
state.queue.push(relatedAddress);
}
return false;
};
const recordEdge = (
input: CallPathTraceInput,
state: TraceState,
edge: DiscoveredEdge,
): void => {
const sourceAddress =
input.direction === "forward" ? edge.address : edge.relatedAddress;
const targetAddress =
input.direction === "forward" ? edge.relatedAddress : edge.address;
const edgeKey = `${sourceAddress}\u0000${targetAddress}`;
if (state.edges.has(edgeKey)) return;
state.edges.set(edgeKey, {
source_address: sourceAddress,
target_address: targetAddress,
discovery_depth: edge.discoveryDepth,
});
};
const projectTrace = (input: CallPathTraceInput, state: TraceState) => ({
start: input.start,
goal: input.goal ?? null,
direction: input.direction,
goal_status:
input.goal === undefined
? "not_requested"
: state.traversalPath.length > 0
? "reached"
: "not_reached",
nodes: [...state.visited.entries()]
.map(([address, { depth }]) => ({ address, depth }))
.sort(
(left, right) =>
left.depth - right.depth || compareAddress(left.address, right.address),
),
edges: [...state.edges.values()].sort(
(left, right) =>
left.discovery_depth - right.discovery_depth ||
compareAddress(left.source_address, right.source_address) ||
compareAddress(left.target_address, right.target_address),
),
traversal_path: state.traversalPath,
failures: state.failures,
traversal: {
nodes_visited: state.visited.size,
},
truncated: state.residual.size > 0,
residual_unknowns: [...state.residual],
limitations: [
"Direct provider call relationships may omit unresolved indirect calls.",
],
});
const compareAddress = (left: string, right: string): number =>
left < right ? -1 : left > right ? 1 : 0;
const sortedUniqueAddresses = (addresses: readonly string[]): string[] =>
[...new Set(addresses)].sort(compareAddress);