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(matchesinteger-math.decay(1000n, 500n, 1n)).diminishing(500n, 1000n) === 333n(extraction §3 example).bps_mul(5000n, 2000n) === 1000n(matchesinteger-math).bps_div(1000n, 2000n) === 5000n(matchesinteger-math).vrf_verifycanonical 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 useproof = 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
proofof exactly 160 hex chars where the last 64 chars equalSHA-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_PATTERNSfromdeterminism.ts. The test suite will runinspectFunctionForbiddenover the source of every exported function. crypto.createHashis explicitly permitted despite being in the Nodecryptomodule — theFORBIDDEN_PATTERNSregex list targetscrypto.randomBytes,crypto.randomUUID,crypto.getRandomValues(RNG calls).createHashis 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.