Both were cases where the NORMATIVE TEXT was wrong or under-specified, which is
worse than a code bug: an implementor reading §4.9 would have diverged from the
reference and believed they were conforming.
1. The spec said caveats "MUST all hold for a decision to be allowed", full stop,
while the implementation deliberately skipped MaxCostUj in the two-phase
`authorize` path (the cost is genuinely unknowable there). So the spec promised
something the reference did not do, and a `MaxCostUj` caveat was in practice
unenforced for any host that authorized, ran, then charged — `charge()` never
re-checked.
§4.9 now states the rule explicitly: a cost-dependent caveat MUST be enforced
at the DEBIT POINT, an implementation MUST NOT report a grant as authorized in
a way implying it passed, and every cost-independent caveat MUST bind in both
phases. `Runtime::charge` enforces it.
The doctrine question that mattered: on a breach the work has ALREADY run, so
the joules are real. `charge` still debits them — energy that was spent is
always accounted — and records the breach in the verdict, so the sealed receipt
is evidence. Suppressing the debit to make the record look clean would be the
dishonest move; the spec now names both failure modes as non-conforming. This
follows the existing budget-overrun precedent ("records the overshoot rather
than hiding it") rather than inventing a new one.
2. The spec asserted that "weakening" an inherited caveat is an amplification, but
never defined weakening — while the code compares by exact structural equality.
So `MaxCostUj(400)` under a parent's `MaxCostUj(500)` was refused as *dropped*
despite being strictly tighter, with nothing saying so.
§4.9 now defines comparison as EXACT IDENTITY and says why: a per-variant
ordering would have to be defined for each predicate, and a verifier that got
it wrong would silently widen authority. A delegator wanting a tighter bound
adds it alongside the inherited one — since all caveats must hold, the tighter
governs. Conservative on purpose: it can never widen, and it can be relaxed
later without breaking existing grants, whereas the reverse is not true.
Also adds `Caveat::is_cost_dependent()` / `cost_breached()` to the schema so the
authorize-skip and the charge-enforce share one definition instead of two
`matches!` arms that could drift.
Tests pin both phases: authorize allows what it cannot evaluate and never debits;
charge denies the breach AND still accounts the 900 µJ. C24 extended to cover the
debit-point rule.
42 suites green, clippy clean, all conformance checks pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>