fix!: preserve feature traces when owner lookup is unavailable (#8368)

* fix!: preserve feature traces when owner lookup is unavailable

* test: strengthen feature trace cancellation assertions

* fix: preserve feature trace provider readiness errors

* test: verify all feature trace reference sources

* test: verify IDA guidance at its owning boundary

* style: format feature trace regression
This commit is contained in:
morluto authored and GitHub committed 2026-11-01 05:38:40 +08:00
1 parent f72638f6b1
commit 6e9ee97a69
16 files changed
+440 -37

No files matched your search

@@ -776,6 +776,23 @@ describe("trace_feature requirements", () => {
reason: null,
}));
it("remains available when owner lookup is unavailable", () => {
expect(
entry(
"trace_feature",
status({
open: true,
kind: "executable",
capabilities: capabilities([
"search_strings",
"search_procedures",
"xrefs",
]),
}),
),
).toMatchObject({ available: true, reason: "available" });
});
it("needs the search operations the workflow actually calls", () => {
expect(
entry(
+1 -6
View File
@@ -215,12 +215,7 @@ const ENHANCED_REQUIREMENTS: ReadonlyMap<string, ProviderComposition> = new Map(
],
analyze_function: ["analyze_function"],
inspect_native_api: ["analyze_function"],
trace_feature: [
"search_strings",
"search_procedures",
"xrefs",
"resolve_containing_procedure",
],
trace_feature: ["search_strings", "search_procedures", "xrefs"],
} satisfies Readonly<Record<EnhancedContractName, ProviderComposition>>),
);
+45 -11
View File
@@ -4,12 +4,16 @@ import type {
AnalysisExecution,
AnalysisOperation,
} from "./AnalysisProvider.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import {
parseContainingProcedureResolution,
type ContainingProcedureResolution,
type TraceContainingProcedure,
} from "../domain/native/nativeObservation.js";
import { err, ok, type Result } from "../domain/result.js";
import type { EnhancedResult, TraceReference } from "./EnhancedToolTypes.js";
@@ -70,7 +74,7 @@ export const traceLiteralFeature = async (
search_mode: "literal",
matches: searched.value.matches,
references: traced.value.references,
truncated: traced.value.residual.length > 0,
truncated: traced.value.truncated,
residual_unknowns: traced.value.residual,
});
};
@@ -121,14 +125,18 @@ const traceReferences = async (
observe: ObserveCall,
request: ReferenceTraceRequest,
): Promise<
Result<{ references: TraceReference[]; residual: string[] }, AnalysisError>
Result<
{ references: TraceReference[]; residual: string[]; truncated: boolean },
AnalysisError
>
> => {
const references: TraceReference[] = [];
const residual: string[] = [];
let truncated = false;
// References identify targets by address, independently of search alternatives.
const tracedTargets = new Set<string>();
// A source address is resolved once however many matches reference it.
const containing = new Map<string, ContainingProcedureResolution>();
const containing = new Map<string, TraceContainingProcedure>();
for (const match of request.matches) {
if (tracedTargets.has(match.address)) continue;
tracedTargets.add(match.address);
@@ -140,10 +148,12 @@ const traceReferences = async (
if (!observed.ok) return observed;
// Only an explicit producer report of truncation is a coverage gap; a
// provider that reports nothing keeps its previous, unqualified result.
if (observed.value.referencesTruncated === true)
if (observed.value.referencesTruncated === true) {
truncated = true;
residual.push(
`References to ${match.address} were truncated by the provider; additional references may exist.`,
);
}
const sources = observed.value.result;
if (!Array.isArray(sources))
return err(
@@ -167,10 +177,34 @@ const traceReferences = async (
{ address: source },
request.signal,
);
if (!resolved.ok) return resolved;
const parsed = parseContainingProcedureResolution(resolved.value);
if (!parsed.ok) return parsed;
containingProcedure = parsed.value;
if (!resolved.ok) {
if (
!(resolved.error instanceof AnalysisCapabilityUnavailableError) ||
resolved.error.operation !== "resolve_containing_procedure" ||
resolved.error.cleanupIncomplete ||
resolved.error.retryable
)
return resolved;
if (request.signal?.aborted === true)
return err(
new AnalysisCancelledError("trace_feature", {
cause: request.signal.reason,
}),
);
containingProcedure = {
query_address: source,
status: "unavailable",
provider_id: resolved.error.providerId,
reason: resolved.error.reason,
};
residual.push(
`Containing procedure for ${source} is unavailable: ${resolved.error.reason}`,
);
} else {
const parsed = parseContainingProcedureResolution(resolved.value);
if (!parsed.ok) return parsed;
containingProcedure = parsed.value;
}
containing.set(source, containingProcedure);
}
references.push({
@@ -180,5 +214,5 @@ const traceReferences = async (
});
}
}
return ok({ references, residual });
return ok({ references, residual, truncated });
};
+2 -2
View File
@@ -3,7 +3,7 @@ import type { z } from "zod";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { ContainingProcedureResolution } from "../domain/native/nativeObservation.js";
import type { TraceContainingProcedure } from "../domain/native/nativeObservation.js";
import { err, type Result } from "../domain/result.js";
import {
enhancedInputSchemas,
@@ -14,7 +14,7 @@ export type EnhancedResult = Promise<Result<JsonValue, AnalysisError>>;
export type TraceReference = {
readonly target_address: string;
readonly source_address: string;
readonly containing_procedure: ContainingProcedureResolution;
readonly containing_procedure: TraceContainingProcedure;
};
/** One enhanced call whose input was parsed at its owning adapter boundary. */
+5 -3
View File
@@ -17,7 +17,9 @@ export const workflowAnalysisProfile = (
? { workflow_revision: 5 }
: operation === "batch_decompile"
? { workflow_revision: 4 }
: operation === "get_call_graph" || operation === "trace_feature"
? { workflow_revision: 3 }
: { workflow_revision: 1 }),
: operation === "trace_feature"
? { workflow_revision: 4 }
: operation === "get_call_graph"
? { workflow_revision: 3 }
: { workflow_revision: 1 }),
});
@@ -1248,6 +1248,11 @@ describe("executions that report truncated references", () => {
it("revises the trace_feature workflow profile so earlier results are not replayed", () => {
const profile = ANALYSIS_SNAPSHOT_PROFILE;
expect(
workflowAnalysisProfile(profile, "trace_feature").parameters
.workflow_revision,
).toBe(4);
expect(workflowAnalysisProfile(profile, "trace_feature")).not.toEqual(
workflowAnalysisProfile(profile),
);
+1 -1
View File
@@ -80,7 +80,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
...FUNCTION_WORKFLOW_TOOL_CONTRACTS,
enhanced(
"trace_feature",
"Trace the complete literal query, preserving spaces, through every matching string and procedure, their xrefs, and truthful containing-procedure resolution. Returns observations and residual unknowns without inferring reference kinds.",
"Trace the complete literal query, preserving spaces, through every matching string and procedure and their xrefs. Preserve an explicit unavailable owner for a non-retryable containment capability refusal. Returns observations and residual unknowns without inferring reference kinds; truncated only reports an observed reference cap.",
enhancedInputSchemas.trace_feature,
),
enhanced(
+2 -2
View File
@@ -1,7 +1,7 @@
import {
nativeAddressListSchema,
nativeObservationSourceSchemas,
containingProcedureResolutionSchema,
traceContainingProcedureSchema,
} from "../domain/native/nativeObservation.js";
import { overviewInventoryCoverageSchema } from "../domain/native/inventoryCoverage.js";
import {
@@ -261,7 +261,7 @@ const literalTraceOutput = evidenceResultOf(
z.object({
target_address: z.string(),
source_address: z.string(),
containing_procedure: containingProcedureResolutionSchema,
containing_procedure: traceContainingProcedureSchema,
}),
),
truncated: z.boolean(),
+14
View File
@@ -27,6 +27,20 @@ export type ContainingProcedureResolution = z.infer<
typeof containingProcedureResolutionSchema
>;
/** A trace can retain a reference when its provider cannot resolve ownership. */
export const traceContainingProcedureSchema = z.union([
containingProcedureResolutionSchema,
z.strictObject({
query_address: z.string(),
status: z.literal("unavailable"),
provider_id: z.string(),
reason: z.string(),
}),
]);
export type TraceContainingProcedure = z.infer<
typeof traceContainingProcedureSchema
>;
/** Validate provider procedure facts before a workflow interprets absence. */
export const parseContainingProcedureResolution = (
value: JsonValue,