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

+2
View File
@@ -8,6 +8,7 @@ import noSharedModuleMock from "./rules/no-shared-module-mock.mjs";
import noLocaleCompare from "./rules/no-locale-compare.mjs";
import noBabelVisitorKeys from "./rules/no-babel-visitor-keys.mjs";
import projectWaitDeadline from "./rules/project-wait-deadline.mjs";
import typedResourceRefusal from "./rules/typed-resource-refusal.mjs";
export default {
meta: { name: "rea" },
@@ -21,5 +22,6 @@ export default {
"no-locale-compare": noLocaleCompare,
"no-babel-visitor-keys": noBabelVisitorKeys,
"project-wait-deadline": projectWaitDeadline,
"typed-resource-refusal": typedResourceRefusal,
},
};
@@ -0,0 +1,21 @@
{
"src/application/JsonAuthenticationHeadroom.ts": 1,
"src/application/desktop/DesktopBuildSolves.ts": 1,
"src/application/ntfs/NtfsJournalService.ts": 1,
"src/browser/WebBundleAnalysisWorker.ts": 4,
"src/browser/history/playwrightTrace/TraceStream.ts": 2,
"src/composition/javascriptAnalysisWorkerProcess.ts": 2,
"src/domain/apiContract/parseGraphqlContract.ts": 1,
"src/domain/native/nativeAotPe.ts": 1,
"src/ghidra/GhidraExecutableHeader.ts": 1,
"src/ghidra/GhidraFileArtifacts.ts": 1,
"src/il2cpp/Il2cppGhidraSeedWriter.ts": 1,
"src/model/ModelArtifactProvider.ts": 1,
"src/native/NativeMacOSProvider.ts": 1,
"src/native/NativePlistSource.ts": 1,
"src/native/SignatureTargetBinding.ts": 1,
"src/native/parsers/lipo.ts": 1,
"src/process/ArtifactAnalysisResources.ts": 1,
"src/server/http/NodeHttpRequestOwner.ts": 1,
"src/sqlite/SqliteDatabaseWorkerExecution.ts": 1
}
@@ -0,0 +1,4 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -0,0 +1,92 @@
import { readFileSync } from "node:fs";
import { relative } from "node:path";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../../../", import.meta.url));
/**
* Files that predate the rule, with their count of untyped constructions. A
* count may only shrink: the rule reports a file above or below its entry.
*/
const baseline = JSON.parse(
readFileSync(
new URL("./typed-resource-refusal-baseline.json", import.meta.url),
"utf8",
),
);
/** Option keys that type a resource constraint's recovery. */
const TYPED_KEYS = new Set([
"fixedAllowance",
"limit",
"remediationAction",
"measuredExhaustion",
]);
const keyName = (property) =>
property.key?.type === "Identifier"
? property.key.name
: property.key?.type === "Literal"
? String(property.key.value)
: undefined;
/**
* Options typed elsewhere (a helper call, a variable or a spread) are trusted;
* the rule flags only constructions that visibly name no recovery.
*/
const isTyped = (options) => {
if (options === undefined) return false;
if (options.type !== "ObjectExpression") return true;
return options.properties.some(
(property) =>
property.type === "SpreadElement" || TYPED_KEYS.has(keyName(property)),
);
};
export default {
meta: {
type: "problem",
docs: {
description:
"Type each resource refusal so recovery never falls back to generic worker-limit text.",
},
schema: [],
messages: {
untyped:
"Type this resource constraint: pass fixedAllowanceOptions(...) for a fixed ceiling, an AnalysisSettingLimit `limit` for an adjustable budget, an authored remediationAction, or measuredExhaustion: true for an observed exhaustion or occupied admission.",
stale:
"typed-resource-refusal-baseline.json allows {{allowed}} untyped resource constraints in this file but {{actual}} remain; lower the entry to {{actual}}.",
},
},
create(context) {
const filename = relative(
root,
context.physicalFilename ?? context.filename,
).replaceAll("\\", "/");
const untyped = [];
return {
NewExpression(node) {
if (
node.callee.type !== "Identifier" ||
node.callee.name !== "AnalysisResourceConstraintError"
)
return;
if (!isTyped(node.arguments[4])) untyped.push(node);
},
"Program:exit"(program) {
const allowed = baseline[filename] ?? 0;
if (untyped.length > allowed)
for (const node of untyped)
context.report({ node, messageId: "untyped" });
else if (untyped.length < allowed)
context.report({
node: program,
messageId: "stale",
data: {
allowed: String(allowed),
actual: String(untyped.length),
},
});
},
};
},
};