Audit — P1.1.3 BPS Constants + Overflow Protection (R83.B)
Surface inventory
Target directory: src/domains/rules/
Parent module (shipped): src/domains/rules/integer-math.ts (R81.A, PR #173, landed on main at b88d7ca0).
$ ls src/domains/rules/
integer-math.ts
Test-suite counterpart on main: src/__tests__/domains/rules/integer-math.test.ts
(100% branch coverage; +38 tests at R81.A, baseline now 1123 per MEMORY.md).
Source of authority
docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md§P1.1.3 (lines 320–449) — authoritative prompt. Lists files, constants, acceptance criteria, writeback, gotchas.docs/guides/implementation/task-breakdown.md§P1.1.3 (lines 494–505).docs/reference/extractions/kappa-rule-engine-extraction.md§3 (BPS Constants) + §4 (Overflow Protection).docs/3-world/physics/laws/rule-engine.md§Integer-only arithmetic + §Basis-point arithmetic.src/domains/rules/integer-math.ts— R81.A shipped file; must not be edited (CLAUDE.md §3 forbids re-touching shipped code outside the authorizing task).src/__tests__/domains/rules/integer-math.test.ts— style reference for the new test file.CLAUDE.md§3, §5, §6, §7, §8;docs/agents/executor-contract.md.
What is already in integer-math.ts (R81.A surface)
Exports
// classes
export class OverflowError extends Error { /* name = 'OverflowError' */ }
export class DivisionByZeroError extends Error { /* name = 'DivisionByZeroError' */ }
export class UnderflowError extends Error { /* name = 'UnderflowError' */ }
// functions
export function bps_mul(value: bigint, bps: bigint): bigint
export function bps_div(value: bigint, bps: bigint): bigint // throws DivisionByZeroError on bps === 0n
export function apply_bps(value: bigint, bps: bigint): bigint
export function decay(value: bigint, rate_bps: bigint, epochs: bigint): bigint // throws UnderflowError on epochs < 0n
export function safe_mul(a: bigint, b: bigint): bigint // throws OverflowError outside INT64 range
export function safe_div(a: bigint, b: bigint): bigint // throws DivisionByZeroError on b === 0n
Not exported (module-private constants)
const BPS_DENOMINATOR = 10_000n;
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)
What is missing (P1.1.3 scope)
- Named basis-point constants as public exports (
BPS_100_PERCENT,BPS_50_PERCENT,BPS_1_PERCENT). - Five domain decay rates (
DECAY_EXECUTION,DECAY_COMMISSIONING,DECAY_ARBITRATION,DECAY_GOVERNANCE,DECAY_SOCIAL). - Five penalty constants (
DAMAGE_MINOR,DAMAGE_MODERATE,DAMAGE_SEVERE,DAMAGE_CRITICAL,DAMAGE_FRAUD). - Public
MAX_INT64/MIN_INT64re-exports so downstream modules can compute ceilings without recomputing2n ** 63n. - Branded
Bpstype plus abps()runtime validator that refuses out-of-range inputs. - Re-exports / thin wrappers for
safe_mulandsafe_divso consumers have a single import surface for “I want the overflow-guarded variant.”
File plan
Two files. Both net-new.
| Path | Role | Est. LOC |
|---|---|---|
src/domains/rules/bps-constants.ts |
Named bigint constants + branded Bps type + bps() validator + safe-op re-exports |
~180 |
src/__tests__/domains/rules/bps-constants.test.ts |
100% branch coverage suite | ~300 |
Test-file location: follows CLAUDE.md §9.1 + the R81.A audit precedent.
The P1.1.1 audit (docs/audits/r81-a-p1-1-1-integer-math-audit.md lines 60-71)
documented that the pre-authored prompt writes src/domains/rules/__tests__/...,
but every one of the 1123 shipped tests lives at src/__tests__/<mirror>/....
This audit carries forward the R81.A decision — tests land at
src/__tests__/domains/rules/bps-constants.test.ts.
API surface (to ship)
Named bigint constants (12 total — matching §P1.1.3 AC line-by-line)
// BPS fractions
export const BPS_100_PERCENT = 10_000n;
export const BPS_50_PERCENT = 5_000n;
export const BPS_1_PERCENT = 100n;
// Domain decay rates
export const DECAY_EXECUTION = 500n; // 5.00%
export const DECAY_COMMISSIONING = 300n; // 3.00%
export const DECAY_ARBITRATION = 1_000n; // 10.00%
export const DECAY_GOVERNANCE = 200n; // 2.00%
export const DECAY_SOCIAL = 100n; // 1.00%
// Damage / penalty tiers
export const DAMAGE_MINOR = 1_500n; // 15%
export const DAMAGE_MODERATE = 3_000n; // 30%
export const DAMAGE_SEVERE = 5_000n; // 50%
export const DAMAGE_CRITICAL = 8_000n; // 80%
export const DAMAGE_FRAUD = 10_000n; // 100%
// Range ceilings (public mirrors of integer-math internal constants)
export const MAX_INT64 = 9_223_372_036_854_775_807n;
export const MIN_INT64 = -9_223_372_036_854_775_808n;
// BPS range for validator / branded type
export const BPS_MIN = 0n;
export const BPS_MAX = 10_000n;
Branded Bps type + validator
export type Bps = bigint & { readonly __brand: 'Bps' };
export function bps(n: bigint | number): Bps; // throws UnderflowError when <0, OverflowError when >BPS_MAX,
// TypeError when non-finite number
Safe-op re-exports
export { safe_mul, safe_div, OverflowError, DivisionByZeroError, UnderflowError }
from './integer-math.js';
This turns bps-constants.ts into the single import surface that P1.3.2
(built-ins) and P1.4.3 (budget module) will consume — no downstream task
has to touch integer-math.ts directly to get typed errors.
Test strategy
Harness: Jest 29 (ts-jest ESM). No new dev deps.
Coverage target: 100% branches on bps-constants.ts. jest.config.ts
already runs collectCoverage: true; the verification step reads the lcov
report.
Test-case matrix:
| Group | Cases | Purpose |
|---|---|---|
| BPS fractions | 3 constants × (value + bigint typeof) | Pin values to spec |
| Domain decay rates | 5 constants × (value + bigint typeof) | Pin values to spec |
| Damage tiers | 5 constants × (value + bigint typeof) | Pin values to spec |
| Range ceilings | MAX_INT64 === 2n**63n - 1n, MIN_INT64 === -(2n**63n), BPS_MIN, BPS_MAX |
Cross-check arithmetic |
bps() accepts bigint in range |
bps(0n), bps(10_000n), bps(5_000n) |
Happy path |
bps() accepts number in range |
bps(0), bps(10000), bps(2500) |
Number coercion |
bps() rejects negative bigint |
bps(-1n) → UnderflowError |
Lower bound |
bps() rejects bigint > 10000 |
bps(10_001n) → OverflowError |
Upper bound |
bps() rejects negative number |
bps(-1) → UnderflowError |
Lower bound (number path) |
bps() rejects number > 10000 |
bps(10_001) → OverflowError |
Upper bound (number path) |
bps() rejects NaN |
bps(NaN) → TypeError |
Non-finite guard |
bps() rejects Infinity |
bps(Infinity) → TypeError |
Non-finite guard |
bps() rejects non-integer number |
bps(3.14) → TypeError |
Integer discipline |
bps() brand flow |
bps(5000n) type-asserts to Bps; returns the same value |
Brand preserved |
| Re-exports | safe_mul, safe_div, 3 error classes re-exportable from new module |
Import surface |
Re-exported safe_mul still throws |
safe_mul(TWO_POW_62, TWO_POW_62) throws OverflowError |
Behaviour preserved |
Re-exported safe_div still throws |
safe_div(7n, 0n) throws DivisionByZeroError |
Behaviour preserved |
| Constant immutability | Re-assignment in a module-level test does not compile (documented via // @ts-expect-error) |
Narrative proof |
| Cross-product sanity | bps_mul(1000n, BPS_50_PERCENT) === 500n via re-imported bps_mul |
End-to-end wire-up |
Expected test count: ~45 it blocks → +45 tests. Brings suite from
1123 → ~1168.
Integration / side-effects
- No DB writes. Pure library.
- No MCP tool registration.
src/server.tsuntouched. Tool-surface stays at 14 (MEMORY.md frozen). - No new deps. No package.json change.
integer-math.tsis not edited. Writeback-hard-block is non-issue — no β task work; this is a pure κ library drop.- No ADR change. ADR-006 (DSL grammar) already covers the κ arithmetic discipline.
- No concept-doc graduation. κ concept stays
colibri_code: noneuntil the evaluator loop ships (P1.3.1 at earliest).
Gate expectations
npm run build— clean.target: ES2022+NodeNext;bigintliterals and branded types compile natively.npm run lint— clean.eqeqeq: error,curly: all,consistent-type-imports: error. Usetypekeyword on type-only imports.npm test— 1123 existing + ~45 new ≈ 1168. One known flake:startup — subprocess smoke(pre-existing, unrelated).
Rollback
Single-commit revert is trivial — every file is net-new and nothing yet
imports bps-constants.ts. git revert <impl-sha> leaves integer-math.ts
and its 38 R81.A tests untouched.
Unblocks
- P1.3.2 (Built-in functions) —
min,max,cap,decay,bps_mul,bps_divwrappers will import named constants from here. - P1.4.3 (Budget module) — consumes
DAMAGE_*tiers directly.
Done-criteria
- Parent module
integer-math.tsinventoried; exported + private surface mapped. - Missing surface list established (12 constants +
Bpstype +bps()+ safe re-exports). - File plan fixed: 2 files, ~480 LOC total; tests follow CLAUDE.md §9.1 layout.
- Test-case matrix drafted (18 groups, ~45 tests).
- Integration surface confirmed (zero wiring; pure lib).
- Rollback plan defined.
- No forbidden edits planned (
integer-math.tsstays read-only).