Files
reverse-engineering-anything/src/application/CallRelationshipTraversal.ts
T
Kaoru0822andClaude Opus 5.5 f95bdbce76 fix(native): stop value traces at unconfirmed child cleanup (#8576)
After a validated seed, trace_native_values turned a cleanup-incomplete
failure from a call-target read or a followed callee dossier into a
plain-text unknown, kept dispatching reads, and returned successful
Evidence without the cleanup_incomplete status, cleanup reason or owned
resources.

The trace now classifies child failures with the shared stopsTraversal
predicate that call graph traversals use. A cancelled child still ends
the trace with its cancellation. A child whose provider cannot confirm
cleanup stops the trace: the provider's own error is the outcome, so its
resources and bound cleanup retry stay intact, and the work admitted
before the stop is returned as the trace's normal shape, with traversal
truncated by cleanup_incomplete and a located unknown at the stopping
read. That stop stays primary over a concurrent caller abort and over
the output-size check. Ordinary child failures remain unknowns.

MCP already retains such partials as workflow Evidence. The one-shot CLI
dropped them, so runDirectAnalysis now retains the partial workflow
Evidence on the failure through the same shared owner.

Fixes #8371

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:56:02 -08:00

88 lines
3.4 KiB
TypeScript

import type { CallRelationshipDirection } from "../contracts/enhancedInputs.js";
import type {
AnalysisExecution,
AnalysisOperation,
ExecutionOptions,
} from "./AnalysisProvider.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { parseRelatedAddresses } from "../domain/native/nativeObservation.js";
import type { Result } from "../domain/result.js";
/** The provider read and response relation for each supported traversal direction. */
export const CALL_RELATIONSHIP_TRAVERSALS = {
forward: { operation: "procedure_callees", relation: "callees" },
backward: { operation: "procedure_callers", relation: "callers" },
} as const satisfies Readonly<
Record<
CallRelationshipDirection,
{
readonly operation: AnalysisOperation;
readonly relation: "callees" | "callers";
}
>
>;
/** One observed provider operation, as composed workflows dispatch it. */
export type CallRelationshipObserver = (
name: AnalysisOperation,
arguments_: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal,
selectorOrigin?: ExecutionOptions["selectorOrigin"],
) => Promise<Result<AnalysisExecution, AnalysisError>>;
/**
* One procedure's relationship read. An unavailable or unreadable read leaves
* the rest of the traversal usable; a stopped read must end it.
*/
export type CallRelationshipRead =
| {
readonly status: "observed";
readonly execution: AnalysisExecution;
readonly related: readonly string[];
}
| { readonly status: "unavailable"; readonly error: AnalysisError }
| { readonly status: "unreadable"; readonly error: AnalysisOutputError }
| { readonly status: "stopped"; readonly error: AnalysisError };
/**
* Whether a failed child read must end a traversal rather than become one of
* its unknowns. A cancelled read ends it; so does a read whose provider cannot
* confirm release of resources it owns, because that read has not finished and
* dispatching more reads would build on that state.
*/
export const stopsTraversal = (error: AnalysisError): boolean =>
error._tag === "AnalysisCancelledError" || error.cleanupIncomplete;
/** Read and parse one procedure's direct relationships for a traversal. */
export const readCallRelationships = async (
observe: CallRelationshipObserver,
direction: CallRelationshipDirection,
procedure: string,
signal?: AbortSignal,
): Promise<CallRelationshipRead> => {
const { operation, relation } = CALL_RELATIONSHIP_TRAVERSALS[direction];
const execution = await observe(operation, { procedure }, signal, "provider");
if (!execution.ok)
return {
status: stopsTraversal(execution.error) ? "stopped" : "unavailable",
error: execution.error,
};
const related = parseRelatedAddresses(
execution.value.result,
relation,
operation,
);
return related.ok
? { status: "observed", execution: execution.value, related: related.value }
: { status: "unreadable", error: related.error };
};
/** The admitted traversal's caveat when unconfirmed cleanup stopped it. */
export const cleanupStoppedTraversal = (
procedure: string,
unexpanded: number,
): string =>
`Traversal stopped at ${procedure} because the provider did not confirm cleanup of resources it owns; ${unexpanded === 1 ? "1 discovered procedure was" : `${unexpanded} discovered procedures were`} not expanded.`;