P1.3.2 — κ Built-in Functions — Execution Packet

Step 3 of the 5-step executor chain. Builds on the audit and the contract. Gates Step 4 (implement) — implementation may not begin until the packet is approved (CLAUDE.md §6).

§P1. Implementation order

P1.1  Bootstrap file scaffold + imports + error class.
P1.2  Trivial arithmetic builtins (min, max, abs, cap, clamp).
P1.3  Newton's-method isqrt + ilog2.
P1.4  decay (delegate) + diminishing.
P1.5  bps_mul + bps_div (delegate).
P1.6  hash (Node crypto SHA-256).
P1.7  vrf_verify stub (per contract §4 + §5.5).
P1.8  Build BUILTINS map + BUILTIN_COSTS map + bindBuiltins.
P1.9  Type validation helpers (asBigint, asString, requireArity).

The order is bottom-up: smallest, lowest-risk pieces first; the table assembly only happens after all 13 functions are implemented and tested.

§P2. File structure plan for builtins.ts

// Section 1 — File header doc-block (per shipped κ pattern in
//             integer-math.ts / engine.ts)
// Section 2 — Imports: integer-math, node:crypto.
// Section 3 — BuiltinTypeError class.
// Section 4 — Type aliases: BuiltinValue, BuiltinFn, BuiltinTable.
// Section 5 — Internal helpers: requireArity, asBigint, asString.
// Section 6 — Trivial arithmetic builtins (min … clamp).
// Section 7 — Newton isqrt + ilog2.
// Section 8 — decay + diminishing.
// Section 9 — bps_mul + bps_div (delegators).
// Section 10 — hash (SHA-256 wrapper).
// Section 11 — vrf_verify stub.
// Section 12 — BUILTINS Map (frozen via Object.freeze on the wrapper /
//              Map is read by ReadonlyMap, the consumer can't .set).
// Section 13 — BUILTIN_COSTS Map.
// Section 14 — bindBuiltins re-export.

Approximate line budget: ~280 lines of code + comments. Within the κ module-size envelope (engine.ts is ~730; integer-math is ~220).

§P3. Test matrix for builtins.test.ts

13 describe-blocks plus a global “surface” block. Test-count expectations (each it is one test):

Describe block it count Notes
BUILTINS surface (AC-1, AC-2) 4 size, key set, cost-table parity, frozen behavior
bindBuiltins (AC-19) 1 returns BUILTINS reference-equal
min 4 a<b, b<a, a==b, type mismatch
max 4 symmetric to min
abs 4 positive, negative, zero, type mismatch
cap 3 v<m, v>m, type mismatch
clamp 5 inside, below lo, above hi, lo>hi throws, type mismatch
isqrt 6 known values [100,101,0], negative throws, large bigint (2^100), arity
ilog2 5 known values [8,1024,2,1], <=0 returns 0n, type mismatch
decay 3 known value (matches integer-math.decay 1-epoch), arity, type mismatch
diminishing 4 known value 333, k+v=0 throws, arity, type mismatch
bps_mul / bps_div 4 delegation correctness, divide-by-zero, arity, type mismatch
hash (AC-11..13) 4 known SHA-256 of “hello world”, digest length 64, determinism, type mismatch
vrf_verify (AC-14, AC-15) 7 canonical accept, short pk, non-hex pk, short proof, non-hex proof, empty input, wrong arity, wrong types
arity validation cross-cut (AC-16) 13 one per builtin
forbidden-op self-scan (AC-20) 1 inspectFunctionForbidden over the file source
determinism harness (AC-18) 1 assertDeterministic over a representative builtin

Approx total: ~70+ tests. The exact count will be reported in the verification doc (§5).

§P4. Specific test vectors (compiled before implementation)

  • hash("hello world") === "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" — verified via Node’s REPL during contract drafting.
  • hash("") === "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" — the SHA-256 of the empty string, cross-confirmed.
  • isqrt(100n) === 10n, isqrt(101n) === 10n, isqrt(99n) === 9n, isqrt(0n) === 0n.
  • isqrt(2n**100n) === 1125899906842624n (= 2^50).
  • ilog2(8n) === 3n, ilog2(1024n) === 10n, ilog2(1n) === 0n, ilog2(0n) === 0n, ilog2(-1n) === 0n.
  • decay(1000n, 500n) === 950n (matches integer-math.decay(1000n, 500n, 1n)).
  • diminishing(500n, 1000n) === 333n (extraction §3 example).
  • bps_mul(5000n, 2000n) === 1000n (matches integer-math).
  • bps_div(1000n, 2000n) === 5000n (matches integer-math).
  • vrf_verify canonical fixture:
    • pk = "00".repeat(32) → 64 hex chars of zeros.
    • input = "test".
    • The stub computes expected = SHA-256(pk + ":" + input) (a 64-char hex string). To exceed length 160, the test fixture for “valid” cases will use proof = expected + "00".repeat(48) and the stub will be specified to compare via prefix… BUT that adds complexity to the stub.

    Decision: Simplify by changing the spec slightly — the stub accepts a proof of exactly 160 hex chars where the last 64 chars equal SHA-256(pk + ":" + input). The first 96 chars are unused. This keeps the 160-char interface (matching ECVRF proof shape) and gives a reproducible test vector without the stub having to ship a fake “real” ECVRF computation. Update contract §5.5 accordingly during implementation.

§P5. Risks and mitigations (from audit §9 + new)

Risk Mitigation
R1 — bigint Newton bootstrap (no Number()) Bit-length via BigInt(n.toString(2).length); right-shift for halve. Tested with 2n**100n.
R2 — SHA-256 length drift Fixture asserts hash("hello world").length === 64 and /^[0-9a-f]{64}$/.test(...).
R3 — min(5n, "foo") BuiltinTypeError thrown by asBigint helper before the comparison.
R4 — decay 3-arg vs 2-arg Builtin is 2-arg, delegates to integer-math.decay(v, rate, 1n).
R5 — diminishing divide-by-zero Explicit denom === 0n check throws DivisionByZeroError.
R6 — VRF stub accepting all input 64/160-char hex prefix gate before any compare; stub returns false on any malformed input.
R7 — Insertion order in BUILTINS Test asserts the exact key sequence so future edits are diff-stable.
R8 — Frozen Map semantics ReadonlyMap is a TS type; runtime freeze is via not exposing the underlying Map’s .set. The exported value is typed BuiltinTable (ReadonlyMap); a misbehaving consumer who casts can still mutate, but that’s a TS contract violation, not a runtime invariant.

§P6. Determinism guardrails

  • The file source must scan clean against FORBIDDEN_PATTERNS from determinism.ts. The test suite will run inspectFunctionForbidden over the source of every exported function.
  • crypto.createHash is explicitly permitted despite being in the Node crypto module — the FORBIDDEN_PATTERNS regex list targets crypto.randomBytes, crypto.randomUUID, crypto.getRandomValues (RNG calls). createHash is not in any forbidden pattern. The determinism harness verifies the same input → same output.

§P7. Wiring plan into engine.ts (NOT IN SCOPE — for future readers)

When P1.3.3 lands (state-access layer), engine.ts §3 case 'FuncCall' will look up expr.name in a builtin registry, charge BUILTIN_COSTS.get(name) against the budget, then call the function with the evaluated args. The current P1.3.1 engine throws 'undefined_function:<name>' for every FuncCall — that line stays as the fallback for un-registered names.

P1.3.2 does not touch engine.ts. The wiring slice is a separate task.

§P8. Build / test gate

cd .worktrees/claude/p1-3-2-builtins
npm run build && npm run lint && npm test

Pre-existing flake: startup — subprocess smoke is known to flap under full-suite load (memory note); single rerun on isolated failure. New suite is src/__tests__/domains/rules/builtins.test.ts.

§P9. Approval to proceed

This packet closes Step 3. Step 4 is implementation — feat(p1-3-2-builtins). Verify lands in docs/verification/p1-3-2-builtins-verification.md with the actual test count and CI output.


Back to top

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

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