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

+29
View File
@@ -371,6 +371,35 @@ JSON values stop at `MAX_JSON_DEPTH` (1000) in
"JSON value exceeds maximum nesting depth of 1000. This release has no runtime
override."
## Typed refusals
A producer that refuses a request types the refusal instead of leaving
recovery to the resource kind. The domain owner is
[`analysisRefusal.ts`](https://github.com/morluto/rea/blob/main/src/domain/analysisRefusal.ts).
It has three variants. Each one projects under `details.refusal` with the
`operation` the error is attributed to:
| Variant | Producer fact | Projected fields |
| -------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------- |
| `fixed_ceiling` | `fixedAllowanceOptions(name, kind, { limit, unit, narrowing })` | `allowance`, `allowance_kind`, `limit`, `unit`, `narrowing` |
| `adjustable_budget` | an `AnalysisSettingLimit` bound as `limit` | `control`, `setting`, `effective_value`, `unit`, `range` |
| `ambiguous_selector` | `AnalysisInputError` option `ambiguousSelector: { field, candidates }` | `field`, `candidates` |
Presentation is total over these variants. Authored remediation still wins.
Without it, the variant names the recovery: fixed ceilings and their narrowing
step, the budget's control and range for the caller's lifetime, or the selector
field and its candidates. A resource refusal never falls back to text about
worker limits, the MCP transport, or doctor. An ambiguous selector conforms to
the input contract, so it does not ask the caller to inspect the schema.
A measured exhaustion or occupied admission is not a refusal. It declares
`measuredExhaustion: true` and keeps the resource kind's recovery. The
`rea/typed-resource-refusal` lint rule rejects a new
`AnalysisResourceConstraintError` that names none of these. Older untyped
constructions are counted in
`scripts/lint/rules/typed-resource-refusal-baseline.json`, and those counts may
only shrink.
## Recovery text
The string the caller sees includes: