P1.3.2 — κ Built-in Functions — Audit
Step 1 of the 5-step executor chain. Inventory of the surface area needed by
src/domains/rules/builtins.tsand the upstream/downstream callers it must coexist with. No code yet — this is a survey.
§1. Task identity
- Colibri task ID:
f471cc30-6451-43fd-adba-543812e7ae3e - Branch:
feature/p1-3-2-builtins - Worktree:
.worktrees/claude/p1-3-2-builtins - Round: R86 — κ Phase 1 Wave 5
- Wave 5 sibling slices (file-disjoint, parallel):
P1.2.4 (
registry.ts), P1.3.3 (state-access.ts), P1.3.4 (policy-gate.ts), P1.5.1 (versioning.ts). This slice ownsbuiltins.tsonly — no overlap.
§2. Files to create
src/domains/rules/builtins.ts— implementation.src/__tests__/domains/rules/builtins.test.ts— Jest suite (matches the shipped κ test layout; the dispatch prompt and the κ task prompt agree on this co-located convention — every other κ module undersrc/domains/rules/ships its tests insrc/__tests__/domains/rules/).docs/audits/p1-3-2-builtins-audit.md(this file).docs/contracts/p1-3-2-builtins-contract.md.docs/packets/p1-3-2-builtins-packet.md.docs/verification/p1-3-2-builtins-verification.md.
§3. Spec sources walked
| Source | What it gives me |
|---|---|
docs/guides/implementation/task-prompts/p1.1-kappa-rule-engine.md §P1.3.2 |
13-function table, signature shapes, budget-cost values, FORBIDDENS, writeback shape. |
docs/reference/extractions/kappa-rule-engine-extraction.md §3 |
Original 8-function table including rep(node) (which is not in the P1.3.2 surface — see §6 below) plus the canonical isqrt / ilog2 pseudocode. |
docs/3-world/physics/laws/rule-engine.md §Built-in functions |
Concept-doc table; calls out 8 built-ins; declares decay(value, rate_bps, epochs) 3-arg form (P1.3.2 dispatch prompt narrows to a 2-arg, single-epoch form). |
docs/architecture/decisions/ADR-002-vrf-implementation.md |
VRF status is “PROPOSED” — Decision is “TBD”. P1.3.2 ships an HMAC-shaped stub with input-shape validation; full ECVRF is deferred per the ADR. |
docs/contracts/p1-3-1-engine-contract.md §1, §2 |
Engine surface — confirms the engine would call into a builtin table via FuncCall AST nodes; P1.3.1 currently rejects every FuncCall as 'undefined_function:<name>'. P1.3.2 will register the builtins so the engine can resolve them once P1.3.3+ wire it through. |
src/domains/rules/engine.ts (P1.3.1, R85) |
Live import surface of Expression, RuleNode. The engine’s FuncCall handler at engine.ts:592 is the call site that will eventually consume BuiltinTable. |
src/domains/rules/integer-math.ts (P1.1.1, R81.A) |
bps_mul, bps_div, decay, safe_mul, safe_div, OverflowError, DivisionByZeroError, UnderflowError, EpochCeilingError. P1.3.2 delegates to these — never reimplements. |
src/domains/rules/bps-constants.ts (P1.1.3, R83.B) |
BPS_MAX, BPS_MIN, MAX_INT64, MIN_INT64. Re-export aliases for safe_mul/safe_div etc. |
src/domains/rules/determinism.ts (P1.1.2, R83.A) |
FORBIDDEN_PATTERNS regex list (Math, Date, setTimeout, fetch, crypto.random, etc.). P1.3.2 must not introduce any forbidden pattern — crypto.createHash is permitted because it is a deterministic transform with no RNG. |
§4. Existing public surface in src/domains/rules/
| Module | Lines | Exports of interest |
|---|---|---|
integer-math.ts |
~220 | bps_mul, bps_div, apply_bps, decay (3-arg), safe_mul, safe_div, errors. |
bps-constants.ts |
~160 | BPS_*, DECAY_*, DAMAGE_*, MAX_INT64, MIN_INT64, bps(), error re-exports. |
determinism.ts |
~280 | assertDeterministic, inspectFunctionForbidden, FORBIDDEN_PATTERNS, DeterminismError. |
lexer.ts |
— | Chevrotain lexer (P1.2.1). |
parser.ts |
— | Chevrotain parser → AST (P1.2.2); declares Expression, RuleNode, FuncCall, Location. |
validator.ts |
— | 7-check AST walker (P1.2.3, R85). |
canonical.ts |
— | Byte-identical JSON serialization (P1.5.4, R85). |
engine.ts |
~730 | evaluate, evaluateExpr, executeRuleset, RuleBudgetExceeded, MAX_INTEGER_OPS, MAX_CALL_DEPTH, MAX_ARG_COUNT, Mutation, Context, etc. |
§5. Dependency graph
┌───────────────┐
│ integer-math │ (P1.1.1, R81.A)
│ bps_mul │
│ bps_div │
│ decay │
│ safe_mul │
│ errors │
└──────▲────────┘
│
│ (delegation)
│
┌──────┴────────┐
builtins.ts (this slice) ──┤ consumes │
│ + node:crypto│
│ + bps-constants (re-exports)
└──────▲────────┘
│
│ (consumed at FuncCall dispatch — P1.3.3+)
│
┌──────┴────────┐
│ engine.ts │ (P1.3.1, R85)
│ evaluateExpr │
│ case FuncCall│
└───────────────┘
P1.3.2 is a leaf module from the engine’s POV — it only exports functions
the engine will call. It imports from integer-math.ts and (for hash)
the Node crypto built-in. It does not import from engine.ts —
that would be a cycle. The engine consumes the builtin table via a
dependency injection (the Context will gain a bindings map populated
with BuiltinTable lookups in a follow-up wave; P1.3.2 stages the
artefact, not the wiring).
§6. The 13-function surface, reconciled across sources
The dispatch prompt names 13 built-ins. The extraction §3 table has 13
rows but includes rep(node) (a state-access stub, not a pure builtin)
and omits cap/hash/vrf_verify. The concept-doc table has 8 rows
without bps_mul/bps_div/hash/vrf_verify/diminishing (mentioned
in the prose but absent from the canonical table) and uses sqrt/log2
instead of isqrt/ilog2. The dispatch prompt is the authoritative set
because (a) it is the most recent and (b) it pins the names the engine’s
FuncCall lookup will use.
| # | Name | Arity | In types | Out type | Cost | Source |
|---|---|---|---|---|---|---|
| 1 | min |
2 | bigint, bigint |
bigint |
1 | All sources |
| 2 | max |
2 | bigint, bigint |
bigint |
1 | All sources |
| 3 | abs |
1 | bigint |
bigint |
1 | All sources |
| 4 | cap |
2 | bigint, bigint |
bigint |
1 | Concept-doc + dispatch |
| 5 | clamp |
3 | bigint, bigint, bigint |
bigint |
1 | Concept-doc + dispatch |
| 6 | isqrt |
1 | bigint |
bigint |
5 | Extraction §3 (isqrt); concept-doc spells sqrt |
| 7 | ilog2 |
1 | bigint |
bigint |
5 | Extraction §3 (ilog2); concept-doc spells log2 |
| 8 | decay |
2 | bigint, bigint |
bigint |
5 | Dispatch prompt narrows the 3-arg integer-math.decay to single-epoch (epochs = 1n) |
| 9 | diminishing |
2 | bigint, bigint |
bigint |
5 | Concept-doc + extraction |
| 10 | bps_mul |
2 | bigint, bigint |
bigint |
5 | Concept-doc + extraction |
| 11 | bps_div |
2 | bigint, bigint |
bigint |
5 | Concept-doc + extraction |
| 12 | hash |
1 | string |
string |
100 | Dispatch prompt only — for ZK-friendly DSL hashing |
| 13 | vrf_verify |
3 | string, string, string |
boolean |
500 | ADR-002 stub |
rep(node) from extraction §3 is excluded — it is a state-access
function, not a pure builtin, and belongs to P1.3.3 (state-access layer).
§7. ADR-002 VRF stub — the boundary I commit to
The ADR is in PROPOSED state with no decision. The dispatch prompt authorizes a stub. I commit to:
- Input-shape validation always runs — wrong arity →
BuiltinTypeError; wrong type per arg →BuiltinTypeError; non-hex / wrong-length pk / proof / input →falsereturn (not throw). This is the load-bearing correctness property: a stub that returnstruefor malformed input is a security hole on the day someone replaces it with a real VRF. - Hex format check — pk and proof must be hex strings of canonical lengths matching the eventual ECVRF-EDWARDS25519 shape (32-byte / 80-byte).
- Determinism — the stub returns the same value for identical
(pk, proof, input)triples. Concretely: it returnstrueonly whenproof === SHA-256(pk || ":" || input)for the hex string concatenation (a self-consistent test vector that has nothing to do with real VRF semantics but gives the test suite a reproducible “valid proof” example).
That last property is purely a test fixture — when the real VRF lands (Phase 1.5+ per ADR-002), this stub function body will be deleted and replaced. The interface (3 hex strings → bool) remains stable.
§8. Forbidden patterns I will not use
From determinism.ts §FORBIDDEN_PATTERNS and the dispatch prompt §FORBIDDENS:
- ✗
Math.*— noMath.sqrt, noMath.log2, noMath.abs, noMath.min/max. - ✗
Date.*,new Date,setTimeout,setInterval,setImmediate. - ✗
Math.random,crypto.randomBytes,crypto.randomUUID,crypto.getRandomValues. - ✗
fetch,XMLHttpRequest,process.hrtime,process.nextTick. - ✗ Float literals (e.g.
3.14,0.5);Number(n)for bigint coercion. - ✗
localeCompare, locale-sensitive collation. - ✗
console.*, file system,process.env. - ✓ Permitted:
crypto.createHash(deterministic transform, no RNG), bigint literals,BigInt(string),toString(2)for bit-length, native+,-,*,/,%,<,>,<=,>=,===,!==on bigint (with the int64 caveat handled viasafe_mulfor multiplication paths).
§9. Risks identified at audit time
- R1 — Newton’s bigint isqrt bootstrap. Naively
let x = BigInt(Math.floor(Math.sqrt(Number(n))))fails becauseNumber(n)overflows forn > 2^53. The dispatch prompt flags this explicitly. Mitigation: bootstrap with bit-length —let x = 1n << (BigInt(n.toString(2).length) / 2n + 1n)— which is always ≥ √n for n ≥ 1. Newton converges from above in O(log log n). - R2 —
crypto.createHashdigest length. SHA-256 is always 32 bytes / 64 hex chars. I will assert this with a fixture in tests so a future Node-version drift doesn’t silently change the contract. - R3 — Type coercion edge cases.
min(5n, "foo")must throwBuiltinTypeErrorbefore attempting the comparison. The compare operators on cross-type bigint/string are ill-defined in TS at runtime (string < bigint TypeErrors), but I want the typed message regardless. - R4 —
decay3-arg form lives ininteger-math.ts. P1.3.2 narrows to 2-arg for the DSL (single-epoch). I delegate tointeger_math.decay(v, rate, 1n)— the inner check that1n <= MAX_DECAY_EPOCHSis trivially satisfied. - R5 —
diminishingdivide-by-zero.(v * k) / (k + v): ifk + v === 0n(i.e.k === -v), this divides by zero. The DSL says k defaults to 1000 and v is non-negative reputation; but the builtin is called from raw arguments. I guardk + v === 0nwithDivisionByZeroError. - R6 —
BUILTIN_COSTSconsumed by engine. The engine at R85 already hasbumpIntegerOpsfor AST node visits. P1.3.2’s costs are not yet wired in (the engine doesn’t yet readBUILTIN_COSTS). The export is staged so a follow-up engine slice can sum these costs into the budget tracker without touchingbuiltins.ts.
§10. Out-of-scope
- Wiring the BuiltinTable into
Context.bindings(engine concern, P1.3.3+). - Charging
BUILTIN_COSTSagainst the budget tracker (engine concern). - Real ECVRF / RFC 9381 implementation (Phase 1.5+ per ADR-002).
- Adding
rep(node)(P1.3.3 state-access). - Rule-level FuncCall validation (P1.2.3 validator concern).
- Mutation collection / application (P1.4.1).
§11. Approval to proceed
This audit closes Step 1. Step 2 is the contract — see
docs/contracts/p1-3-2-builtins-contract.md.