Audit — P1.1.1 Basis Point Arithmetic (R81.A)
Surface inventory
Target directory: src/domains/rules/
Target state: greenfield. The directory does not exist on origin/main @ 77e579b8.
$ ls src/domains/
integrations proof router skills tasks trail
No rules/ subdirectory. No integer-math.ts. No pre-existing κ code surface.
This is the first Phase 1 κ Rule Engine task to touch src/; every file this task
writes is net-new.
Related existing surfaces that must NOT be touched:
src/domains/tasks/(β) — task pipeline; unrelated.src/domains/router/(δ) — Phase 0 stubs; unrelated (router/scoring.ts,router/fallback.ts).src/domains/proof/(η) — Merkle surface; consumes κ version hashes in Phase 3 but not Phase 1. Do not edit.src/db/— κ state is in-memory for P1.1.1; no schema change.src/server.ts— no new MCP tool is registered by P1.1.1; it is a library-only module consumed by later P1 sub-tasks (P1.1.2, P1.1.3, P1.3.2).
Source of authority (reading list)
docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md§P1.1.1 (lines 62–196) — the pre-authored canonical prompt, authoritative spec for files, API, acceptance criteria, gotchas, writeback.docs/reference/extractions/kappa-rule-engine-extraction.md§3 + §4 — Phoenix-donor algorithmic source: BPS constants, signature table,safe_mul/safe_divpseudocode, int64 bounds (-2^63 .. 2^63 - 1 = -9,223,372,036,854,775,808 .. 9,223,372,036,854,775,807).docs/3-world/physics/laws/rule-engine.md§Integer-only arithmetic + §Basis-point arithmetic examples + §Forbidden operations — concept doc; confirms floor rounding is required for compounding monotonicity and lists forbidden ops (no Math, no Date, no RNG, no async, no I/O).CLAUDE.md§3 worktree, §5 triple gate, §6 5-step chain, §7 writeback.docs/guides/implementation/task-breakdown.md§P1.1.1 — roadmap entry.
File plan
Two files. Both net-new.
| Path | Role | Est. LOC |
|---|---|---|
src/domains/rules/integer-math.ts |
Public library — bigint bps helpers + typed errors | ~170 |
src/__tests__/domains/rules/integer-math.test.ts |
100% branch coverage suite | ~300 |
Test-file location note: the pre-authored prompt (line 126) writes
src/domains/rules/__tests__/integer-math.test.ts, but the shipped Phase 0
convention per CLAUDE.md §9.1 is a single root at src/__tests__/ with the
source tree mirrored under it (e.g. src/__tests__/domains/router/scoring.test.ts
matches src/domains/router/scoring.ts). Every one of the 1085 existing tests
follows this layout. jest.config.ts has roots: ['<rootDir>/src'] and picks
up **/__tests__/**/*.test.ts, so both layouts would run — but only the
centralized src/__tests__/ layout matches the established Phase 0 corpus and
CLAUDE.md §9.1 “shipped (Wave A, P0.1.2); 1001 tests passing at 09d462f8”.
This audit follows the shipped convention. Gotcha flagged in the packet.
API surface (confirmed against extraction §3 + task-prompt §P1.1.1)
All functions take and return bigint. No number, no Math.*, no Date.*,
no async, no RNG.
// Core BPS ops
bps_mul(value: bigint, bps: bigint): bigint // (value * bps) / 10_000n, floor
bps_div(value: bigint, bps: bigint): bigint // (value * 10_000n) / bps, floor; throws on bps === 0n
apply_bps(value: bigint, bps: bigint): bigint // value - bps_mul(value, bps)
decay(value: bigint, rate_bps: bigint, epochs: bigint): bigint
// per-epoch apply_bps; floors each step; throws on epochs < 0n
// Safe arithmetic primitives
safe_mul(a: bigint, b: bigint): bigint // throws OverflowError when |a*b| > 2^63 - 1
safe_div(a: bigint, b: bigint): bigint // truncate-toward-zero; throws on b === 0n
// Typed errors (exported)
export class OverflowError extends Error {}
export class DivisionByZeroError extends Error {}
export class UnderflowError extends Error {}
Constants (internal, not re-exported)
const BPS_DENOMINATOR = 10_000n; // 100% in basis points
const INT64_MAX = 9_223_372_036_854_775_807n; // 2^63 - 1
const INT64_MIN = -9_223_372_036_854_775_808n; // -2^63
BPS_100_PERCENT / BPS_1_PERCENT / BPS_50_PERCENT from the extraction are
P1.1.3 scope (constants + overflow protection), not P1.1.1. Per gotcha #3 in
the prompt: do not introduce branded Bps types here — P1.1.3 concern.
Test strategy
Harness: Jest 29 (ts-jest ESM), per the existing 1085-test suite. No new
dev deps. No fast-check/property libs — hand-rolled cases are sufficient for
100% branch coverage on six pure functions.
Coverage target: 100% branches on integer-math.ts. Jest already runs with
collectCoverage: true per jest.config.ts, so npm test emits an lcov
report the verification step reads.
Test-case matrix (expanded in packet):
| Group | Cases | Purpose |
|---|---|---|
bps_mul |
identity (10000, 10000), (1000, 500)=50, (0, x), (x, 0), negative value, negative bps, large value × safe bps | Every arg sign + zero branch |
bps_div |
inverse of bps_mul, (5000, 2500)=20000, (1000, 2000)=5000, divisor 0 throws, negative divisor | Floor / truncate semantics + error branch |
apply_bps |
(1000, 150)=985, (1000, 10000)=0 (full decay), (1000, 0)=1000 (no-op), underflow never goes below 0 for non-negative | Invariant check |
decay |
(1000, 150, 1)=985, (1000, 150, 2)=970 (compound floor), (v, r, 0)=v, epochs<0 throws, 100-epoch compounding stability | Multi-step flooring |
safe_mul |
0 × anything, 1 × anything, boundary (INT64_MAX, 1), overflow (2^62 × 2^62), negative × negative (large positive overflow), INT64_MIN × -1 (sign overflow) | Overflow + sign branches |
safe_div |
1 / 1, truncate toward zero, divide-by-zero throws, negative numerator, negative denominator, -7/2 = -3 (not -4 — truncate, not floor) | Both signs + zero branch |
| Error classes | instanceof OverflowError/DivisionByZeroError/UnderflowError, .name set, .message present |
Typed-error surface |
| Purity | same input → structurally identical output; no this-binding required | Determinism guardrail (lighter than P1.1.2’s full harness) |
Expected test count: ~30 it blocks → ~30 new tests. Brings suite from
1085 → ~1115.
Integration / side-effects
- No DB writes. Pure library; no import from
src/db/*. - No MCP tool registration.
src/server.tsis untouched. The κ surface ships with zero MCP tools in Phase 0/Phase 1 until P1.4.1 (admission evaluator wiring) — and even then MCP exposure is TBD. Tool-surface count stays at 14. - No ADR change. P1.1.1 implements the code counterpart of an existing extraction; no new architecture decision needed. ADR-006 (DSL grammar) is already on record.
- No docs outside the 5-step-chain quartet. Audits / contracts / packets /
verification live under
docs/{audits,contracts,packets,verification}/; no concept-doc edit is required becausedocs/3-world/physics/laws/rule-engine.mdalready describes the BPS arithmetic at spec granularity. A frontmatter graduation (colibri_code: none → partial) is out of scope for P1.1.1 — the κ concept only graduates once the evaluator loop ships (P1.3.1 at earliest, likely not until the P1.4.1 admission wiring).
Gate expectations
npm run build— clean. NodeNext strict mode is already configured; bigint literals are ES2020 and thetarget: ES2022tsconfig accepts them natively.npm run lint— clean.eqeqeq: errormeans always use===.curly: allmeans always brace single-line ifs.npm test— pre-existing 1085 tests + ~30 new = ~1115. One known flake:startup — subprocess smokemay fail under load on first run.
Rollback
Single-commit revert is trivial because this task creates net-new files and imports from nowhere else:
git revert <final-sha>
Nothing downstream can regress because nothing else yet imports
src/domains/rules/*.
Unblocks
P1.1.2 (Determinism Harness) consumes this module.
P1.1.3 (BPS Constants + branded Bps type) wraps this module.
P1.3.2 (Built-in functions) delegates to this module.
Done-criteria
- Target surface confirmed greenfield (no
src/domains/rules/onorigin/main). - Authority documents read end-to-end.
- File plan fixed: 2 files, ~470 LOC total.
- Test strategy + case matrix drafted (expanded in packet).
- Test-file location divergence from prompt flagged (follows CLAUDE.md §9.1 + shipped convention).
- Integration surface confirmed (zero wiring; pure lib).
- Rollback plan defined.