Contract — P1.1.3 BPS Constants + Overflow Protection (R83.B)

1. Scope

Add a single file — src/domains/rules/bps-constants.ts — that layers named basis-point constants, a branded Bps type, a range-validating bps() helper, and overflow-guarded re-exports on top of the R81.A surface at src/domains/rules/integer-math.ts. Plus one net-new test file.

This contract does not authorize edits to integer-math.ts.

2. Modules touched

Path Direction Net LOC Source of truth
src/domains/rules/bps-constants.ts create ≈ 180 this contract
src/__tests__/domains/rules/bps-constants.test.ts create ≈ 300 this contract
src/domains/rules/integer-math.ts must not change 0 R81.A PR #173 (sealed)

3. Invariants (must all hold at merge)

  • I1 All exported numeric values are bigint literals (100n, not BigInt(100), not 100). as const narrows their types so no caller can widen them.
  • I2 No function in this module performs I/O, throws synchronously on purpose-only-for-logging, or depends on a clock, RNG, or global mutable state.
  • I3 integer-math.ts is imported via the ./integer-math.js extensioned specifier (NodeNext ESM convention already used throughout src/).
  • I4 Typed errors thrown from this module are instances of the classes re-exported from integer-math.ts. Do not define local clones.
  • I5 Branded Bps is a compile-time tag only; at runtime Bps === bigint.
  • I6 bps(n) refuses every input outside [0n, 10_000n] with a typed error; NaN / Infinity / non-integer number raise TypeError.
  • I7 100% branch coverage on bps-constants.ts.
  • I8 1123 existing tests still pass (R81.A baseline, see MEMORY.md “R81 Wave 1 SEALED PARTIAL”).
  • I9 Tool-surface count stays at 14 — no new MCP tool, no registration in src/server.ts.

4. Public surface

4.1 Named bigint constants

// 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 (spec AC-1 per task-breakdown.md §P1.1.3)
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 (spec AC-2)
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%

// Int64 boundaries (public mirrors of integer-math internals)
export const MAX_INT64 = 9_223_372_036_854_775_807n;   //  2^63 - 1
export const MIN_INT64 = -9_223_372_036_854_775_808n;  // -(2^63)

// Bps admissible range (kept separate so tooling can diff against BPS_100_PERCENT)
export const BPS_MIN = 0n;
export const BPS_MAX = 10_000n;

Every value is frozen by const + bigint literal — no callsite can reassign.

4.2 Branded type + validator

export type Bps = bigint & { readonly __brand: 'Bps' };

export function bps(n: bigint | number): Bps;

Behaviour of bps(n):

Input Result
bps(0n) returns 0n (tagged Bps)
bps(10_000n) returns 10_000n
bps(2_500n) returns 2_500n
bps(0) (number) returns 0n
bps(10_000) returns 10_000n
bps(-1n) throws UnderflowError("bps: -1 below BPS_MIN (0)")
bps(-1) throws UnderflowError("bps: -1 below BPS_MIN (0)")
bps(10_001n) throws OverflowError("bps: 10001 above BPS_MAX (10000)")
bps(10_001) throws OverflowError("bps: 10001 above BPS_MAX (10000)")
bps(3.14) throws TypeError("bps: 3.14 is not an integer")
bps(NaN) throws TypeError("bps: NaN is not an integer")
bps(Infinity) throws TypeError("bps: Infinity is not an integer")
bps(-Infinity) throws TypeError("bps: -Infinity is not an integer")

TypeError is the Node built-in (no new class). Error subtypes for the bound violations are the ones re-exported from integer-math.ts so there is exactly one OverflowError class in the κ surface.

4.3 Safe-op re-exports

export {
  safe_mul,
  safe_div,
  OverflowError,
  DivisionByZeroError,
  UnderflowError,
} from './integer-math.js';

Purpose: downstream κ modules (P1.3.2 built-ins, P1.4.3 budget) import only from bps-constants.ts. They never reach into integer-math.ts directly. That keeps the surface one hop deep and makes the safe-variant discovery path trivial.

5. Error contract

Errors thrown by bps():

  • UnderflowError when the numeric value is strictly less than BPS_MIN.
  • OverflowError when the numeric value is strictly greater than BPS_MAX.
  • TypeError when a number input is non-finite or non-integer (Number.isSafeInteger(n) === false).

Errors thrown by re-exported ops — identical to R81.A semantics:

  • safe_mul(a, b)OverflowError when the product is outside [MIN_INT64, MAX_INT64].
  • safe_div(a, b)DivisionByZeroError when b === 0n.

6. Test acceptance criteria

# Criterion How verified
AC-1 Every BPS / DECAY / DAMAGE constant equals the literal in §4.1 Equality + typeof === 'bigint'
AC-2 MAX_INT64 === 2n ** 63n - 1n and MIN_INT64 === -(2n ** 63n) Computed comparison
AC-3 BPS_MIN === 0n and BPS_MAX === BPS_100_PERCENT Cross-check
AC-4 bps() accepts every valid bigint in {0n, 2500n, 10_000n} Equality on returned value
AC-5 bps() accepts every valid number in {0, 2500, 10000} Returned value is bigint
AC-6 bps(-1n) and bps(-1) throw UnderflowError with message matching /below BPS_MIN/ toThrow
AC-7 bps(10_001n) and bps(10_001) throw OverflowError with /above BPS_MAX/ toThrow
AC-8 bps(3.14), bps(NaN), bps(Infinity), bps(-Infinity) all throw TypeError with /not an integer/ toThrow
AC-9 safe_mul re-exported from bps-constants.ts still throws on (2^62, 2^62) toThrow(OverflowError)
AC-10 safe_div re-exported still throws on (7n, 0n) toThrow(DivisionByZeroError)
AC-11 OverflowError / DivisionByZeroError / UnderflowError re-exported from bps-constants.ts are identical to their integer-math.ts originals (reference equality of class reference) const { OverflowError: A } = await import('./bps-constants.js'); const { OverflowError: B } = await import('./integer-math.js'); expect(A).toBe(B);
AC-12 bps_mul(1000n, BPS_50_PERCENT) === 500n — end-to-end wire-up Equality
AC-13 100% branch coverage on bps-constants.ts in the Jest lcov report npm test output
AC-14 1123 pre-existing tests still pass Full-suite run

7. Gates

  • npm run build — clean.
  • npm run lint — clean (eqeqeq, curly, consistent-type-imports).
  • npm test — 1123 + ≈ 45 new tests all green (except pre-existing startup — subprocess smoke flake, which is unrelated).

8. Out of scope

  • Editing src/domains/rules/integer-math.ts (R81.A sealed).
  • Registering an MCP tool for any of these constants.
  • Wiring the constants into a rule engine (P1.3.2 will).
  • Graduating the κ concept doc’s colibri_code frontmatter (stays none until the evaluator ships).
  • Generics / template-typed constants — Bps is the only branded type here.

9. Definition of done

  • Both files land on feature/r83-b-bps-constants via commits on the 5-step chain (audit → contract → packet → impl → verify).
  • All three gates green.
  • PR opened against main with writeback block referencing R83 manifest PR #187.

Back to top

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

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