ADR-0003 — Budget comparisons are precomputed, and "cost unknown" is a field¶
Status: accepted · week 1 Fills: SPEC §8 and the AG-BUD-002/003/004 descriptions in SPEC §4.6.
Context¶
Three baseline rules in SPEC §4.6 cannot be written in the language SPEC §4.4 defines.
AG-BUD-002, "≥80% consumed." The predicate set has no arithmetic and no way
to compare two fields. Only steps_remaining is available, so the closest
expressible rule is "two steps or fewer left", which is a different rule and
wrong for a 500-step budget.
AG-BUD-003, "money budget hit." Written as
money_remaining_cents <= 0 it is dead code: BudgetState.exhausted()
returns true at money_used >= money_cents, and SPEC §4.5.6 short-circuits
AG-BUD-001 before any rule runs. The rule can never fire. The useful rule —
"this action costs more than the token has left" — compares
context.cost_estimate.money_cents against derived.budget.money_remaining_cents,
which the language cannot express.
AG-BUD-004, "unknown cost." SPEC §8 says "missing → 0 and AG-BUD-004
unknown_cost shadow hit". But SPEC §3.1 gives cost_estimate a default of
CostEstimate(), so "missing" and "genuinely zero" are the same value. An
adapter that cannot estimate is indistinguishable from a free action.
Decision¶
Add four things rather than weaken the language.
derived.budget.{steps,tokens,money,seconds}_pct_used— integers 0–100, truncated toward zero,nullwhen that axis is uncapped. AG-BUD-002 becomesgte: 80on any of them.derived.budget.money_overrun— boolean,nullwhen there is no money cap. True whencost_estimate.money_cents > money_remaining_cents. AG-BUD-003 uses it and becomes reachable.CostEstimate.known: bool = True. An adapter that cannot estimate sets it false. AG-BUD-004 reads it directly.- A cap of
0on any budget axis means uncapped, and the corresponding_remaining/_pct_usedfield isnull. This matches the mandate example in SPEC §3.4, which writesmoney_cents: 0to mean "no money budget".
All four are computed in compute_derived, which is pure, so none of this moves
I/O into the decision path.
Consequences¶
- The predicate language stays arithmetic-free: no expressions, no two-field comparisons, no plugins (ADR-002.5 holds).
- Every comparison a baseline rule needs is a named, documented, individually testable field. "80 percent" appears once in code rather than once per rule.
CostEstimate.knownis an addition to the SPEC §3.1 schema. It is additive and defaults to the previous behaviour, soschema_versionstays1.0.- Adapters that cannot estimate cost become visible instead of silently scoring zero. AG-BUD-004 ships in shadow mode so the gap is measured rather than enforced.
- Adding a derived field means adding a predicate path. The lint path set is generated from the models, so no separate registration is needed.
Alternatives rejected¶
- Add arithmetic to the predicate language. Reopens ADR-002.5, and every expression is a new way for two installations to disagree about what a rule means.
- Make
cost_estimateoptional (CostEstimate | None). Expressible viaexists, but it breaks every rule that readscontext.cost_estimate.money_centswhenever an adapter omits the block. - Leave AG-BUD-003 as written and accept dead code. A rule nobody can ever trigger in a compliance pack is worse than no rule: it shows up in a control mapping as coverage that does not exist.