Audit — P1.1.2 Determinism Verification Harness (R83.A)

Surface inventory

Target directory: src/domains/rules/ Target state: P1.1.1 landed on main via R81.A (PR #173). Directory now contains exactly one file.

$ ls src/domains/rules/
integer-math.ts

P1.1.1 live surface (the module this harness wraps):

$ wc -l src/domains/rules/integer-math.ts
183 src/domains/rules/integer-math.ts

Exported symbols (cited for the contract’s AC#2):

Line Symbol Kind
37 OverflowError exported class extends Error
45 DivisionByZeroError exported class extends Error
53 UnderflowError exported class extends Error
85 bps_mul(value, bps) exported function, (bigint, bigint) => bigint
96 bps_div(value, bps) exported function, throws on bps === 0n
110 apply_bps(value, bps) exported function
126 decay(value, rate_bps, epochs) exported function, throws on epochs < 0n
153 safe_mul(a, b) exported function, throws OverflowError
178 safe_div(a, b) exported function, throws DivisionByZeroError

All six helpers accept bigint and return bigint. None take options objects. None have side effects. This is the total surface P1.1.2 must prove deterministic.

Test location convention (same as R81.A §Test-file location note):

The pre-authored prompt at docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md §P1.1.2 line 246 names src/domains/rules/__tests__/determinism.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 1123 existing tests on main follows this layout. R81.A audit already locked this convention for κ tests at src/__tests__/domains/rules/integer-math.test.ts. jest.config.ts line 16 has testMatch: ['**/__tests__/**/*.test.ts', …] so either layout would run; we follow R81.A’s convention.

Related existing surfaces that must NOT be touched:

  • src/domains/tasks/ (β) — task pipeline; unrelated.
  • src/domains/router/ (δ) — Phase 0 stubs; unrelated.
  • src/domains/proof/ (η) — Merkle surface; unrelated.
  • src/domains/skills/ (ε), src/domains/trail/ (ζ), src/domains/integrations/ (ν) — none touched.
  • src/db/ — harness is pure in-memory; no schema change.
  • src/server.ts — no new MCP tool is registered by P1.1.2; library-only surface.

Source of authority (reading list)

  1. docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md §P1.1.2 (lines 199–318) — the pre-authored canonical prompt, authoritative spec for files, acceptance, gotchas, writeback.
  2. docs/guides/implementation/task-breakdown.md §P1.1.2 (lines 482–492) — roadmap entry: “property test · no Math.random/Date.now · no async · fuzz test 10k tuples · static grep rejects Math.*/Date.* outside tests · suite <10s”.
  3. docs/3-world/physics/laws/rule-engine.md §Forbidden operations (lines 45–50, 137) — concept doc: “Clock reads forbidden; Randomness forbidden; Floating-point forbidden in rule bodies.”
  4. src/domains/rules/integer-math.ts — the P1.1.1 helpers under test.
  5. src/__tests__/domains/rules/integer-math.test.ts — prior-art test layout.
  6. CLAUDE.md — §3 worktree, §5 triple gate, §6 5-step chain, §7 writeback (hard-block at src/domains/tasks/writeback.ts:97).
  7. Orchestrator dispatch prompt — adds a harness library file (src/domains/rules/determinism.ts) beyond what the canonical p1.1-… file specifies test-only. The library surface is the orchestrator’s expansion.

Scope divergence vs. task-prompts doc (recorded for reviewer)

The original p1.1-… file’s “Files to create” lists one file (src/domains/rules/__tests__/determinism.test.ts), with the harness logic inline inside the test. The R83 orchestrator dispatch prompt (the authoritative spec for THIS execution per §7 of the orchestrator guidance) splits that into two files:

# Path Role
1 src/domains/rules/determinism.ts Reusable harness library (runN + code-inspection)
2 src/__tests__/domains/rules/determinism.test.ts Tests exercising the harness

The reusable-library shape is strictly more capable than the inline-only shape: P1.1.3, P1.3.1, P1.3.2 can all import determinism.ts to re-assert their own helpers rather than re-rolling the harness. This aligns with the orchestrator prompt’s framing: “This is a guardrail for all of Phase 1.” Convention-wise, src/domains/rules/determinism.ts is production code so it must itself honor the κ determinism invariants (no floats, no RNG, no I/O).

File plan

Two files. Both net-new.

Path Role Est. LOC
src/domains/rules/determinism.ts Pure harness library (runN + function-source inspector) ~140
src/__tests__/domains/rules/determinism.test.ts Tests exercising harness + fs-side source scanner ~270

API surface (for src/domains/rules/determinism.ts)

All functions are synchronous, pure, and operate on bigint or structurally comparable values. No Math.*, no Date.*, no async, no RNG.

// Error class for determinism-violation reports
export class DeterminismError extends Error {}

// Run a function `n` times with the same args; throw DeterminismError if
// any pair of outputs (or thrown errors) differs from the first call.
// `n` defaults to 10.
export function assertDeterministic<A extends readonly unknown[], R>(
  fn: (...args: A) => R,
  args: A,
  opts?: { iterations?: number; label?: string },
): R;

// Regex-scan a function body for forbidden-op tokens (Math., Date., random,
// setTimeout/setInterval, fetch, import 'fs', require('fs'), await, async,
// crypto.random, .hrtime, .nextTick, float literals like 3.14).
// Returns the list of offending tokens; empty array means clean.
export function inspectFunctionForbidden(fn: Function): readonly string[];

// Convenience wrapper: throws DeterminismError if inspectFunctionForbidden
// returns a non-empty list.
export function assertNoForbiddenOps(
  fn: Function,
  opts?: { label?: string },
): void;

// Deep-equality helper that understands bigint, array, plain-object, typed
// error (`.name` + `.message`), and primitives. Exported so tests can reuse it.
export function deepEqualDeterministic(a: unknown, b: unknown): boolean;

Forbidden-ops regex patterns (spec’d in contract §4)

The inspector compiles a fixed readonly list of regexes applied against fn.toString():

Pattern Rejects
/\bMath\.[A-Za-z_]\w*/ Math.random, Math.floor, any Math.*
/\bDate\.[A-Za-z_]\w*/ Date.now, Date.parse, any Date.*
/\bnew\s+Date\b/ new Date()
/\bsetTimeout\b\|\bsetInterval\b\|\bsetImmediate\b/ timers
/\bfetch\b\|\bXMLHttpRequest\b/ network
/\brequire\s*\(\s*['"]fs['"]/ CJS fs
/\bfrom\s+['"]fs['"]/ ESM fs
/\bcrypto\.[A-Za-z_]\w*/ crypto randomness
/\bprocess\.hrtime\b\|\bprocess\.nextTick\b/ process clock/queue
/\bawait\b/ async
/\basync\s+(function\|\()/ async function
/\b\d+\.\d+\b/ (but not 1n) float literal

Matched tokens are deduped. Regex semantics are intentionally token-level: false positives are acceptable (tests can inline-ignore when reviewing), false negatives are not (a missed Date.now breaks consensus).

Test strategy

Harness: Jest 29 (ts-jest ESM), same as R81.A.

Coverage target: 100% branches on determinism.ts. Jest 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
assertDeterministic happy path All six P1.1.1 helpers wrapped; 10 iterations each AC#2 — real deterministic code passes
assertDeterministic throw-path wrap a function that throws; assert harness treats consistent throws as deterministic Error-equality branch
assertDeterministic FAIL detection Deliberately non-deterministic mock () => Date.now() fails with DeterminismError mentioning the two differing outputs AC#3 — detects drift
assertDeterministic label propagation Custom label flows into error message Readability branch
assertDeterministic iterations override iterations=2 exits after 2 calls; iterations=1 no-ops Option-branch
inspectFunctionForbidden happy path integer-math helpers return empty array AC#4 — clean code reports clean
inspectFunctionForbidden Math.* () => Math.random() returns ["Math.random"] RNG detection
inspectFunctionForbidden Date.* () => Date.now() returns ["Date.now"] Clock detection
inspectFunctionForbidden new Date () => new Date() returns ["new Date"] Constructor form
inspectFunctionForbidden timers () => { setTimeout(()=>{},0); } returns ["setTimeout"] Timer detection
inspectFunctionForbidden fetch () => fetch('x') returns ["fetch"] Network
inspectFunctionForbidden crypto () => crypto.randomBytes(4) returns ["crypto.randomBytes"] Crypto
inspectFunctionForbidden process.hrtime () => process.hrtime() returns ["process.hrtime"] hrtime
inspectFunctionForbidden await async () => await x returns both ["async function", "await"] Async
inspectFunctionForbidden float literal () => 3.14 returns ["3.14"] Float leakage
assertNoForbiddenOps throws on any non-empty inspect result; no-op on clean; label-propagation check AC#5 — wrapper branch
deepEqualDeterministic equal bigints, unequal bigints, arrays, objects, nested, primitives, null/undefined, mixed-type, different-key-length Equality helper branches
DeterminismError instanceof DeterminismError, .name === 'DeterminismError', message contains label Typed-error surface
FS-side full-corpus scan Read src/domains/rules/**/*.ts (exclude __tests__); assert NO forbidden tokens in any shipped κ file AC#4 prod-guard

Expected test count: ~30 it blocks → ~30 new tests. Brings suite from 1123 → ~1153.

Integration / side-effects

  • No DB writes. Pure library; no import from src/db/*.
  • No MCP tool registration. src/server.ts is untouched. Tool-surface count stays at 14.
  • No ADR change. P1.1.2 is a harness around P1.1.1; no new architecture decision needed.
  • No docs outside the 5-step-chain quartet. No concept-doc edit; the κ concept doc already describes “Forbidden operations” at spec granularity.
  • No frontmatter graduation. κ stays colibri_code: none until the evaluator loop ships (P1.3.1 earliest; P1.4.1 most likely).

Gate expectations

  • npm run build — clean. NodeNext strict mode; bigint literals OK at target: ES2022. No any unless explicitly Function (the introspection target type has no better choice).
  • npm run lint — clean. eqeqeq: error → always ===. curly: all → brace single-line ifs. @typescript-eslint/no-explicit-any: warn may require a single // eslint-disable-next-line on the Function-typed inspector argument (this is intentional: we accept any function shape).
  • npm test — pre-existing 1123 + ~30 new ≈ 1153. startup — subprocess smoke pre-existing flake may hit once; rerun-once policy.

Rollback

Single-commit revert is trivial because this task creates net-new files and imports from nowhere outside src/domains/rules/integer-math.ts:

git revert <impl-sha>

Nothing downstream can regress because nothing else yet imports src/domains/rules/determinism.ts.

Unblocks

  • Guardrail for every compute sub-task in P1.2–P1.5 per task-prompts.md line 207.
  • P1.1.3 (BPS Constants) can use assertDeterministic to re-validate its constants-with-safe-arith composition.
  • P1.3.1 (Core evaluation loop) can use the FS-side scanner as a CI guard.

Done-criteria

  • Target surface confirmed (P1.1.1 shipped; only integer-math.ts in src/domains/rules/).
  • Authority documents read end-to-end.
  • File plan fixed: 2 files, ~410 LOC total.
  • Test strategy + case matrix drafted (expanded in packet).
  • Test-file location convention aligned with R81.A (follows CLAUDE.md §9.1 + shipped layout).
  • Scope divergence from p1.1-… doc (2-file vs 1-file) recorded.
  • Integration surface confirmed (zero wiring; pure lib).
  • Rollback plan defined.

Back to top

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

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