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.ts and 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 owns builtins.ts only — 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 under src/domains/rules/ ships its tests in src/__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:

  1. Input-shape validation always runs — wrong arity → BuiltinTypeError; wrong type per arg → BuiltinTypeError; non-hex / wrong-length pk / proof / input → false return (not throw). This is the load-bearing correctness property: a stub that returns true for malformed input is a security hole on the day someone replaces it with a real VRF.
  2. Hex format check — pk and proof must be hex strings of canonical lengths matching the eventual ECVRF-EDWARDS25519 shape (32-byte / 80-byte).
  3. Determinism — the stub returns the same value for identical (pk, proof, input) triples. Concretely: it returns true only when proof === 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.* — no Math.sqrt, no Math.log2, no Math.abs, no Math.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 via safe_mul for 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 because Number(n) overflows for n > 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.createHash digest 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 throw BuiltinTypeError before 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 — decay 3-arg form lives in integer-math.ts. P1.3.2 narrows to 2-arg for the DSL (single-epoch). I delegate to integer_math.decay(v, rate, 1n) — the inner check that 1n <= MAX_DECAY_EPOCHS is trivially satisfied.
  • R5 — diminishing divide-by-zero. (v * k) / (k + v): if k + 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 guard k + v === 0n with DivisionByZeroError.
  • R6 — BUILTIN_COSTS consumed by engine. The engine at R85 already has bumpIntegerOps for AST node visits. P1.3.2’s costs are not yet wired in (the engine doesn’t yet read BUILTIN_COSTS). The export is staged so a follow-up engine slice can sum these costs into the budget tracker without touching builtins.ts.

§10. Out-of-scope

  • Wiring the BuiltinTable into Context.bindings (engine concern, P1.3.3+).
  • Charging BUILTIN_COSTS against 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.


Back to top

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

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