Repository navigation
feat(budget): name a budget set after its consumer, and make check enforce it - #263
Conversation
…force it Refs: CLOUD-50
CLOUD-50 Token-budget enforcement for the instruction-file set (`[budget]` + `policy budget`)
Why The always-loaded context budget is live policy today as The threshold is a convention-level bound, not a literature-backed performance claim: pinned deliberately in a test, moved by a config edit, never tuned on a paper. Rejected alternatives
Definition of done
Acceptance
Refinement — Ready (a declared file-set token budget: config table, Refinement gate: Definition of Ready & Done. This body carries only specializations.
Stated assumptions
|
|
|
/fast-forward |



Closes the two clauses the 2026-08-11 DoD-conformance audit demoted CLOUD-50 from
Done for. PR #237 landed
budget.rsandpolicy budget; these are the two placesthe landed code disagreed with its own Definition of Done.
1. The consumer's set name was baked into the engine
Budgetwas a struct with aninstructionsfield. "instructions" is thisrepository's name for its always-loaded context — a consumer-specific identifier
living in
crates/batten, which non-negotiable rule 1 forbids. A second consumerbudgeting a different surface needed an engine change to do it.
[budget.<name>]is now a map, and the key isfilesrather thanpathsasthe DoD specifies. Any number of sets, under any names the engine has never heard
of, with no code change.
2. The gate never fired
The DoD: "
batten checkevaluates every declared budget as a non-spawning readgate: over either bound → a deny finding, exit 2." It didn't.
policy budgetwasthe only surface, so an over-budget set was visible only to whoever thought to run
the report — and a budget that reports when asked is not a gate.
Budgets are now evaluated in
run_rules, the one funnelcheckandenforceshare, and an over-budget set produces an ordinary
Findingrather than aprivate verdict path. That choice is the point: budgets inherit waivers, the
-Jshape, the exit contract and the findings store for free, all of which a bespoke
channel would have had to re-implement.
FindingKind::Scopeis the honest kind —a budget is a whole-repo condition — and the identity is over the set, so a
bigger overrun is the same finding rather than a new one each time the count moves.
Counting reads files and sums them, so
check's declaredreadeffect ispreserved.
The absent
[budget]table reads two ways, on purposecheckmeasures nothing (a repository that declares no budget has no budget tofail).
policy budgetis exit 1 (a report that measured nothing must not print0— the false green the engine exists to catch). Two callers asking differentquestions, and both readings are honest. Asserted in both directions.
Blast radius worth reviewing
Wiring budgets into
checkmeans CLOUD-298's per-entry dead-glob refusal nowreaches the main gate: a budget entry matching no file is exit 1, where before it
was only reachable through
policy budget. That is the stronger and intendedreading — a gate that could not run must say so — but it did surface in three
fixture suites (
tests/cli.rs,tests/config-lint.bats,tests/prebuilt-lint.bats) which copy the committedbatten.tomlinto a repowith no
AGENTS.md. Each now supplies the file its budget names.One detail found on the way: the fixtures symlink most of the tree, but
rules::tree_filescounts regular files only, so a symlinkedAGENTS.mdisinvisible to the walk and the entry reads as dead. The bats fixtures copy it.
Tests
Unit (
budget.rs): a set name is the consumer's and any name validates; anover-budget report is a finding naming its set, with identity stable across
overrun size and distinct per set; a set within budget produces none;
measure_all(None)is empty rather than an error.E2E (
tests/cli.rs): an over-budget set denies throughcheckand rides thenormal
-Jfindings channel; a set within budget leaveschecksilent; two namedsets are each measured, only the over one is named, and
policy budget -Jreportsan array in name order so the shape does not change as a consumer adds a budget.
Consumer #1's
batten.tomladoptsfiles, with both thresholds unchanged andstill pinned by the existing test.
Refs: CLOUD-50