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:

β 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, or MAX_ARG_COUNT in src/domains/rules/budget.ts MUST be accompanied by a bump of ENGINE_VERSION in src/domains/rules/versioning.ts in the same commit.

Mechanism (recap from budget.ts:54–63 and audit §6):

  1. The three constants are LITERAL VALUES baked into the engine binary.
  2. They are NOT direct inputs to computeVersionHash(ruleset); that hash mixes the canonical ruleset bytes with ENGINE_VERSION.
  3. Therefore the dependency on the limits is mediated by ENGINE_VERSION.
  4. Without the bump, two arbiters running different binaries (one with the old cap, one with the new) would compute the same rule_version_hash for 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 §6
  • docs/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.
  • FuncCall arg-count check at engine.ts:594 — unchanged.
  • executeRuleset freshBudget() allocation at engine.ts:712 — now imported from ./budget.js instead of declared locally; semantics identical.
  • Context.budget typing — was BudgetTracker interface declared in engine.ts; is now BudgetTracker type alias of BudgetCounters re-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 RuleBudgetExceeded with correct counter (F2.1–F2.3, F5.4, F6.3, F6.4)
  • budget.tick events 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-lock stage). 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 frozen DEFAULT_LIMITS aggregate.
  • The RuleBudgetExceeded error class with which/limit/observed fields and the locked message format.
  • The BudgetCounters interface (the plain mutable struct that engine.ts’s evaluator hot path uses).
  • The freshBudget() factory.
  • A new class BudgetTracker with tickIntegerOp / pushCall / popCall / reset / snapshot / subscribe methods, emitting frozen BudgetTick events ('integer_op' 'call_push' 'call_pop') with a logical (bigint, non-wall-clock) monotonic at counter 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.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.