Verification — P1.4.3 Admission Budgets
Module: src/domains/rules/budget.ts
Round: R87 κ Wave 7
Branch: feature/p1-4-3-budget
Step: 5 of 5 — test evidence
Predecessors:
docs/audits/p1-4-3-budget-audit.mddocs/contracts/p1-4-3-budget-contract.mddocs/packets/p1-4-3-budget-packet.md
β task ID: 6f474dec-885f-433b-90f9-4bafb45cd725
Base SHA: 0b124c17
1. Step-1-through-4 commits
| Step | Output | SHA |
|---|---|---|
| 1 — Audit | docs/audits/p1-4-3-budget-audit.md |
0a7faef3 |
| 2 — Contract | docs/contracts/p1-4-3-budget-contract.md |
d051e158 |
| 3 — Packet | docs/packets/p1-4-3-budget-packet.md |
ecbff4a5 |
| 4 — Implement | src/domains/rules/budget.ts + tests + engine.ts edit |
fc68083a |
This file (Step 5) is the final commit before push.
2. Gate run — npm run build && npm run lint && npm test
> colibri@0.0.1 build
> tsc
> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 6 migration(s) E:\AMS\.worktrees\claude\p1-4-3-budget\src\db\migrations -> E:\AMS\.worktrees\claude\p1-4-3-budget\dist\db\migrations
> colibri@0.0.1 lint
> eslint src
(no output — clean)
> colibri@0.0.1 test
> node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage
Test Suites: 43 passed, 43 total
Tests: 2154 passed, 2154 total
Snapshots: 0 total
Time: 66.19 s
All three gates green.
2.1. Test count delta
- Baseline at
0b124c17(origin/main, post Wave 6): 2075 tests across 42 suites. - After P1.4.3: 2154 tests across 43 suites.
- Delta: +79 tests, +1 suite (
src/__tests__/domains/rules/budget.test.ts).
2.2. New tests by section
| Section | Count | Coverage |
|---|---|---|
| F1 — Constants | 5 | MAX_* literal values, DEFAULT_LIMITS shape, frozen |
| F2 — RuleBudgetExceeded | 7 | All 3 which values, .name, message format, instanceof Error/Self |
| F3 — freshBudget | 3 | zero struct, distinct allocations, mutability |
| F4 — Class construction | 6 | default + partial limits + zero values, frozen limits, mutation rejection |
| F5 — tickIntegerOp | 5 | increment, emit, MAX boundary, throw with fields, post-increment value |
| F6 — pushCall | 7 | normal, == limit OK, arg_count throw, depth throw at 17th, emit, no-mutation on arg fail, normal emit |
| F7 — popCall | 4 | decrement, safe at 0, always emit, emit at depth 0 |
| F8 — reset | 5 | zero counters, zero depth + arg, zero at, preserve limits, preserve listeners |
| F9 — subscribe | 5 | one listener, multiple listeners, unsubscribe, idempotent unsubscribe, re-subscribe |
| F10 — Listener errors | 3 | swallow, do not abort others, do not affect tracker |
| F11 — Frozen events | 4 | event frozen, snapshot frozen, limits frozen, mutation no-op |
F12 — at is logical |
4 | first === 0n, monotonic +1n, bigint type, advances without listener |
| F13 — Snapshot | 4 | frozen return, current values, snapshot stability post-mutation, limits embedded |
| F14 — Determinism scan | 8 | corpus self-scan over class methods + constructor + freshBudget |
| F15 — engine.ts re-export identity | 5 | RuleBudgetExceeded ===, MAX_* equal, instanceof on engine alias |
| F16 — Interface compat | 4 | class instance ↔ BudgetCounters, engine alias, mutation, type usability |
Total: 79 tests.
2.3. Pre-existing tests unchanged
src/__tests__/domains/rules/engine.test.ts (59 tests) — passes WITHOUT modification:
- F4.1, F4.2 — integer-ops cap
- F4.3, F4.4 — call-depth cap
- F4.5, F4.6 — arg-count cap (==MAX OK, MAX+1 throws)
- F8.x — per-rule reset (executeRuleset’s fresh-instance behavior)
- F9.5 — RuleBudgetExceeded.{which,limit,observed,name,message} contract
This is the engine.ts re-export contract honored at runtime. The four imports RuleBudgetExceeded, MAX_INTEGER_OPS, MAX_CALL_DEPTH, MAX_ARG_COUNT from '../../../domains/rules/engine.js' continue to resolve to the same class identities and constant values as before — F15 in budget.test.ts asserts this directly.
src/__tests__/domains/rules/admission.test.ts, policy-gate.test.ts, registry.test.ts, versioning.test.ts, etc — all green, unchanged.
3. Acceptance criteria — verdict
From the dispatch prompt:
| Criterion | Verdict | Evidence |
|---|---|---|
Budget tracker class with counters: integer_ops, call_depth, current_arg_count |
✓ PASS | class BudgetTracker in src/domains/rules/budget.ts:225–404 with all three public mutable fields. F4.1 verifies. |
Limits exported as constants: MAX_INTEGER_OPS=10_000, MAX_CALL_DEPTH=16, MAX_ARG_COUNT=8 |
✓ PASS | src/domains/rules/budget.ts:96, :106, :115. F1.1, F1.2, F1.3 assert literal values. |
On exceed: throw RuleBudgetExceeded carrying which-counter-fired field |
✓ PASS | Class at budget.ts:215–230. Three throw paths in tickIntegerOp (line 296), pushCall (lines 318 + 333), with which ∈ {integer_ops, call_depth, arg_count}. F2.1–F2.7, F5.4, F6.3, F6.4. |
Instrumentation hooks: emit budget.tick events for α audit (count only) |
✓ PASS | BudgetTick event interface (budget.ts:185–192), subscribe(listener) returning unsubscribe (budget.ts:374–388), private emit() (budget.ts:392–404). Events frozen, snapshot count-only. F9, F11. |
| Budget state resets per-rule (no leaking across rules) | ✓ PASS | Two paths: freshBudget() (budget.ts:265) — fresh struct per rule; reset() method (budget.ts:344–349) — recycle existing instance. F3, F8. |
| Limits documented as participating in rule version hash via P1.5.1 | ✓ PASS | budget.ts:54–63 (preamble §”Limits ↔ rule_version_hash dependency”) + per-constant docstrings at lines 90, 104, 113 explicitly call out the lockstep obligation with ENGINE_VERSION in versioning.ts. See also §4 of this file (verification checklist). |
| Determinism scanner clean | ✓ PASS | F14.1–F14.8 — inspectFunctionForbidden over every class method + constructor + freshBudget returns []. |
| If integrating with engine.ts inline tracker: existing engine.test.ts continues to pass | ✓ PASS | engine.ts re-exports MAX_INTEGER_OPS, MAX_CALL_DEPTH, MAX_ARG_COUNT, RuleBudgetExceeded, and BudgetTracker (as a type alias of BudgetCounters). 59/59 engine.test.ts assertions green; F15.1–F15.5 in budget.test.ts directly asserts identity equality of the re-exports. |
8 of 8 acceptance criteria pass.
4. Rule version hash dependency — checklist for future maintainers
OBLIGATION (P1.4.3 → P1.5.1): any change to
MAX_INTEGER_OPS,MAX_CALL_DEPTH, orMAX_ARG_COUNTinsrc/domains/rules/budget.tsMUST be accompanied by a bump ofENGINE_VERSIONinsrc/domains/rules/versioning.tsin the same commit.
Mechanism (recap from budget.ts:54–63 and audit §6):
- The three constants are LITERAL VALUES baked into the engine binary.
- They are NOT direct inputs to
computeVersionHash(ruleset); that hash mixes the canonical ruleset bytes withENGINE_VERSION. - Therefore the dependency on the limits is mediated by
ENGINE_VERSION. - Without the bump, two arbiters running different binaries (one with the old cap, one with the new) would compute the same
rule_version_hashfor a given ruleset and yet return inconsistent admission decisions, breaking θ consensus.
This obligation is documented in:
src/domains/rules/budget.ts:54–63(module preamble §”Limits ↔ rule_version_hash dependency”)src/domains/rules/budget.ts:90, 104, 113(per-constant docstrings)docs/audits/p1-4-3-budget-audit.md§6docs/contracts/p1-4-3-budget-contract.md§3 invariant I1- This verification doc (§4)
No automated guard. The discipline is human; reviewers must check the obligation when touching the literals. Future κ phase (e.g. when consensus tooling lands) may add an automated check.
5. Determinism corpus self-scan — full output
Per F14.1–F14.8, inspectFunctionForbidden over every observable surface returns []:
F14.1 BudgetTracker.prototype.tickIntegerOp []
F14.2 BudgetTracker.prototype.pushCall []
F14.3 BudgetTracker.prototype.popCall []
F14.4 BudgetTracker.prototype.reset []
F14.5 BudgetTracker.prototype.snapshot []
F14.6 BudgetTracker.prototype.subscribe []
F14.7 BudgetTracker (constructor) []
F14.8 freshBudget []
No Math.*, Date.*, crypto.*, setTimeout, setInterval, setImmediate, fetch, XMLHttpRequest, await, async function, float literal, [native code], process.hrtime, process.nextTick, or filesystem import patterns.
The _at field is bigint, advanced by += 1n. No wall-clock anywhere.
6. Engine integration verdict
The pre-existing engine evaluator’s behavior is byte-identical before vs. after P1.4.3:
bumpIntegerOps(engine.ts) — unchanged body.bumpCallDepth(engine.ts) — unchanged body.FuncCallarg-count check at engine.ts:594 — unchanged.executeRulesetfreshBudget()allocation at engine.ts:712 — now imported from./budget.jsinstead of declared locally; semantics identical.Context.budgettyping — wasBudgetTrackerinterface declared in engine.ts; is nowBudgetTrackertype alias ofBudgetCountersre-exported from./budget.js. Same shape, same usage.
The new class BudgetTracker in ./budget.js is a parallel, observable forward API; the engine evaluator does NOT consume it this round. Future engine refactor can switch the evaluator to use the class methods (and emit α audit events through them); that’s out-of-scope per the contract §7.
7. Test plan checklist (from PR template)
npm run build(green)npm run lint(green)npm test(green; 2154 tests; +79 from baseline 2075)- limit-exceed throws
RuleBudgetExceededwith correct counter (F2.1–F2.3, F5.4, F6.3, F6.4) budget.tickevents emitted (count only; frozen) (F11.1–F11.3, F9.1, F9.2)- per-rule reset (F8.1–F8.5)
- determinism scanner clean (F14.1–F14.8)
- engine.ts re-export identity preserved (F15.1–F15.5)
- engine.test.ts F4.x and F9.5 unchanged and green (no edits to that file; 59/59 pass)
8. Worktree + remote cleanup plan
After PR squash-merge:
cd E:/AMS
git worktree remove --force .worktrees/claude/p1-4-3-budget
gh pr merge <num> --squash --delete-branch
git -C E:/AMS fetch --prune origin
The git worktree remove --force BEFORE the gh pr merge --delete-branch avoids the R76 quirk where a pinned worktree silently skips the remote delete (memory: feedback_gh_pr_merge_delete_branch_quirk.md).
9. Flake watchlist
startup — subprocess smoke— pre-existing under full-suite load (carry-over from R76+). Did NOT fire during this gate run; not a new regression. Memory has a watchlist entry.- No new flakes introduced by P1.4.3.
10. Risk register — final
All risks from the packet (§4) are mitigated:
| Risk | Mitigation | Verdict |
|---|---|---|
Identity equality of re-exported RuleBudgetExceeded broken |
F15.1 asserts === |
✓ PASS |
engine.ts’s freshBudget() shadowed |
engine.ts now imports from ./budget.js; single source of truth |
✓ PASS |
Class instance does not structurally satisfy BudgetCounters |
F16.1, F16.2 | ✓ PASS |
BudgetTick.at chokes JSON serialization |
bigint, documented; consumers convert via String(event.at) |
✓ DOCUMENTED |
| Listener throws cascade into evaluator | F10.1, F10.2, F10.3 | ✓ PASS |
| Determinism scanner misses tokens | F14.* covers all observable surfaces | ✓ PASS |
Test ESM .js extension on imports |
uses pattern from existing test files | ✓ PASS |
| TypeScript declaration merging conflict (interface + class same name) | resolved via rename: interface → BudgetCounters; class kept as BudgetTracker; engine.ts type alias bridges |
✓ PASS |
11. Unblocks
- P1.4.4 — Tool-Lock Integration Spec (registers admission evaluator at α’s
tool-lockstage). The class’s observable surface gives α a clean attach point for budget telemetry.
12. Summary
P1.4.3 ships a dedicated src/domains/rules/budget.ts module hosting:
- The 3 canonical limit constants (
MAX_INTEGER_OPS=10000,MAX_CALL_DEPTH=16,MAX_ARG_COUNT=8) and a frozenDEFAULT_LIMITSaggregate. - The
RuleBudgetExceedederror class withwhich/limit/observedfields and the locked message format. - The
BudgetCountersinterface (the plain mutable struct that engine.ts’s evaluator hot path uses). - The
freshBudget()factory. -
A new class BudgetTrackerwithtickIntegerOp/pushCall/popCall/reset/snapshot/subscribemethods, emitting frozenBudgetTickevents ('integer_op''call_push''call_pop') with a logical (bigint, non-wall-clock) monotonicatcounter for α audit observability. Listeners are observers, not gates — their throws are swallowed and do not affect tracker state.
src/domains/rules/engine.ts is converted to RE-EXPORT the four canonical symbols (MAX_INTEGER_OPS, MAX_CALL_DEPTH, MAX_ARG_COUNT, RuleBudgetExceeded) and to type-alias the old BudgetTracker interface name to BudgetCounters. Identity equality is preserved across the re-export boundary; existing tests (engine.test.ts F4.x, F9.5) and consumers (policy-gate.ts, admission.ts) continue to work without modification.
The limits’ participation in the rule version hash is documented via the lockstep obligation on ENGINE_VERSION in versioning.ts (per audit §6 + this verification §4) — the mechanism for θ consensus integrity is unchanged.
All 8 acceptance criteria pass. All 2154 tests green. Build + lint + test gates all clean.
Verification complete — 2026-05-07. Step 5 of 5 in the executor chain. Ready for push + PR.