feat(errors): type refusals at the producer with a total recovery (#10577)

* feat(errors): type refusals at the producer with a total recovery

A resource refusal without authored text fell back to resource-kind advice:
a fixed parser ceiling reported as memory told the caller to add host
memory, an upstream IDA reply ceiling pointed at REA's MCP transport, and a
fixed Kuna text ceiling reported as capacity was marked retryable. An
ambiguous selector was reported as invalid input that pointed at the schema.

Add AnalysisRefusal with three variants built from facts producers already
own: fixed_ceiling (FixedAllowance, now with limit, unit and narrowing),
adjustable_budget (AnalysisSettingLimit) and ambiguous_selector (new
AnalysisInputError option). Each variant is attributed to the error's
operation and projected under details.refusal. Presentation is a total
switch over the variants, and authored text still takes precedence.

Migrate the first producers. GraphQL nesting, Playwright trace input, the
Kuna function text and the IDA upstream reply become fixed ceilings. jadx
overloads and Kuna ambiguous names become ambiguous selectors, replacing the
two Kuna-specific ambiguity texts. Seven occupied admissions declare
measuredExhaustion. The rea/typed-resource-refusal lint rule rejects new
untyped resource constraints, and the remaining 24 are counted in a baseline
that can only shrink.

* fix(errors): honor refusal recovery ownership
This commit is contained in:
morluto authored and GitHub committed 2026-11-14 14:36:14 -05:00
1 parent a0d9025202
commit e83e3fa8a0
31 files changed
+896 -57

No files matched your search

+27 -9
View File
@@ -374,11 +374,33 @@ const matchingMethods = (
return candidates;
};
const overloadChoices = (
/** The method name matched several overloads and no overload_index chose one. */
const ambiguousOverload = (
request: AndroidRequest,
candidates: ReturnType<typeof matchingMethods>,
external = false,
): string =>
`Select one of ${candidates.length} overloads: ${candidates.map((method) => `${method.overload_index}: ${method.reported_signature}${external ? ` [${method.dex_descriptor}]` : ""}`).join("; ")}`;
): never => {
throw new AnalysisInputError(
request.operation,
{
ambiguousSelector: {
field: ["overload_index"],
candidates: candidates.map((method) => ({
overload_index: method.overload_index,
reported_signature: method.reported_signature,
dex_descriptor: method.dex_descriptor,
})),
},
},
[
{
path: ["overload_index"],
reason: "invalid_value",
message: `Select one of ${candidates.length} overloads: ${candidates.map((method) => `${method.overload_index}: ${method.reported_signature}${external ? ` [${method.dex_descriptor}]` : ""}`).join("; ")}`,
},
],
);
};
const inspectMethod = async (
tools: JadxToolPort,
@@ -388,7 +410,7 @@ const inspectMethod = async (
): Promise<JsonValue> => {
const candidates = matchingMethods(summary, request);
if (candidates.length > 1 && request.input.overload_index === undefined)
inputFailure(request, "overload_index", overloadChoices(candidates));
ambiguousOverload(request, candidates);
const index = request.input.overload_index ?? 0;
const selected =
candidates[index] ??
@@ -531,11 +553,7 @@ const traceReferences = async (
"Method name is required for an overload index.",
);
if (candidates.length > 1 && request.input.overload_index === undefined)
inputFailure(
request,
"overload_index",
overloadChoices(candidates, external),
);
ambiguousOverload(request, candidates, external);
const index = request.input.overload_index ?? 0;
const selected = methodName === undefined ? undefined : candidates[index];
if (methodName !== undefined && selected === undefined)