{"version":3,"file":"lua.mjs","names":[],"sources":["../../../../src/batteries/orchestration/cells/lua/index.ts"],"sourcesContent":["/**\n * The Lua predicate cell — an untrusted-script evaluator for `branch` and `select` nodes.\n *\n * @module @nhtio/adk/batteries/orchestration/cells/lua\n *\n * **Node-only.** This subpath imports `node:worker_threads` and `node:process` directly and will\n * not load in a browser or Web Worker bundle — deliberately, unlike the environment-neutral\n * orchestration barrel, which carries zero `node:*` imports anywhere in its module graph so it\n * can be imported from isomorphic code. Never re-export it from that barrel.\n *\n * The predicate for this cell is a SOURCE STRING (a Lua expression or statement list) read from\n * the node's `predicate` field. `validate()` refuses a non-string and loads the chunk so a syntax\n * error surfaces at freeze rather than at run time. `evaluate()` compiles the chunk in text mode\n * only (precompiled bytecode is refused — the Lua bytecode verifier is not a security boundary),\n * injects the marshalled context, and interprets the result as a branch verdict\n * (`{kind:'branch', matched}`) or a select verdict (`{kind:'select', caseLabel}`).\n *\n * ## Three enforcement layers\n *\n * 1. **Instruction-count hook.** A Lua count hook is installed via `lua_sethook` and fires every\n *    `instructionLimit` VM instructions (default 1,000,000). On each firing it raises a Lua error,\n *    which the VM turns into an interrupted `pcall` — this stops `while true do end` from *inside*\n *    the VM, which no host-side timer can do. The hook is the only thing that can break a tight\n *    Lua loop because Lua runs synchronously on the host thread.\n * 2. **Allocator cap (enforced by wasmoon).** `engine.global.setMemoryMax(memoryCeilingBytes)` is\n *    a REAL allocator cap: wasmoon installs a custom `lua_Alloc` when the engine is built with\n *    `traceAllocations: true`, and that allocator REFUSES any growing realloc whose end size would\n *    exceed `memoryMax`, returning `NULL` so the VM raises `LUA_ERRMEM` (\"not enough memory\").\n *    `traceAllocations: true` is REQUIRED for the cap to exist — without it `setMemoryMax`/\n *    `getMemoryUsed` throw \"Memory allocations is not being traced\" — so the engine is always\n *    built with that flag. `getMemoryUsed()` (typed surface: `getMemoryUsed`; there is no\n *    `getMemoryUse`) reports bytes allocated under the same flag, and is kept as a SECONDARY\n *    signal: the count hook polls it and raises a clean, named, model-addressed\n *    `MEMORY_CEILING_EXCEEDED` error naming the ceiling BEFORE the allocator's hard failure can\n *    fire, so the common case produces a friendly named abort rather than a raw \"not enough\n *    memory\". The poll is secondary, NOT the enforcement: the cap is what actually refuses\n *    allocation; the poll only wins the race when it gets a firing between the overshoot start\n *    and the cap's refusal. An overshoot that beats the poll still hits the hard cap, and the\n *    host catches that too and converts it to the same named failure (see layer 3 of the abort\n *    handling in `evaluate`), so a raw \"not enough memory\" never propagates.\n * 3. **Worker-thread watchdog.** The outer bound. A main-thread timer cannot do this job, because\n *    a tight Lua loop starves `setTimeout`, `nextTick`, `queueMicrotask` and `setImmediate` alike\n *    — only a separate event loop keeps time. The watchdog runs in a `worker_threads` Worker and,\n *    on expiry, `process.kill(pid, 'SIGKILL')`s the SHARED process (not `process.exit()`, which\n *    ends only the worker). Five load-bearing details, all required:\n *      - a main-thread timer cannot keep time during a tight loop (above);\n *      - the worker kills the SHARED process via `process.kill(pid, 'SIGKILL')`, never\n *        `worker.terminate()`-or-`process.exit()` semantics, because only killing the host\n *        process actually breaks a synchronous VM loop that owns the main thread;\n *      - `worker.unref()` is called or a successful run keeps the worker alive and the process\n *        never exits;\n *      - the expired-deadline check is SYNCHRONOUS and runs BEFORE the first `await import()` of\n *        wasmoon, so a process that started past its deadline refuses before doing any work;\n *      - one absolute deadline epoch (`Date.now() + timeoutMs`) is shared between the host and the\n *        watchdog so the two cannot disagree about when \"expired\" is.\n *\n * ## Reduced guarantee\n *\n * At construction the hook and the allocator cap are probed against a canary. The cap probe is\n * POSITIVE: it builds a throwaway engine, calls `setMemoryMax` with a DELIBERATELY TINY ceiling,\n * runs a bounded chunk that allocates a lot, and confirms the allocator actually REFUSES the\n * allocation (throws). Only when that refusal is observed is layer 2 claimed. If the probe fails\n * (the WASM module refused `addFunction`, `setMemoryMax` did not gate allocation, `traceAllocations`\n * produced no stats, etc.), the cell falls back to watchdog-only enforcement and REPORTS the\n * reduced guarantee through the `guarantee` field rather than claiming a hook/memory bound it\n * cannot deliver. A successful run under a reduced guarantee is still correct; it is merely less\n * protected against a runaway script.\n *\n * ## Sandbox surface\n *\n * The engine is built by ALLOWLIST with `openStandardLibs: false`, so no standard library is\n * present until explicitly injected. The following are NEVER injected and are absent by\n * construction: `_G`, `getfenv`, `getmetatable`, `load`, `dofile`, `io`, `os`. No clock and no\n * randomness are injected unless a future caller extends this cell. A predicate that reaches for\n * any of these fails at runtime with an \"unknown global\" error, which the cell surfaces as a\n * `{kind:'select', caseLabel: null}` (default) or `{kind:'branch', matched: false}` verdict\n * rather than as a thrown host exception — a predicate is never allowed to crash the run.\n *\n * The test that confirms the watchdog actually kills the process on a `while true do end` loop is\n * NOT run here: it would SIGKILL this process. It is described in the module-level TSDoc on\n * `createLuaCell` and lives in a separate, opt-in test harness that spawns a child process.\n */\n\nimport { loadOnce } from '../../predicates'\nimport { isInstanceOf, isObject } from '../../../../lib/utils/guards'\nimport type {\n  BranchNodeDefinition,\n  NodeOutput,\n  OutputItem,\n  PlanNode,\n  PredicateContext,\n  PredicateEvaluator,\n  PredicateVerdict,\n  SelectNodeDefinition,\n} from '../../types'\n\n// ── wasmoon surface (typed; loaded lazily) ───────────────────────────────────\n//\n// These are declared locally rather than imported at top level because wasmoon is an OPTIONAL\n// peer and must not be required to resolve when this module is merely imported by the\n// environment-neutral barrel's type checker. `load()` is the only place `await import('wasmoon')`\n// runs, and `loadOnce` maps a failure there to `E_ORCH_CELL_UNAVAILABLE`.\n//\n// The shapes below are checked against the installed `node_modules/wasmoon/dist/*.d.ts`:\n//   · `LuaFactory().createEngine(options)` → `LuaEngine`\n//   · `engine.global` : `Global` (extends `Thread`), exposing `getMemoryUsed()`, `setMemoryMax()`,\n//     `loadString`, `run`, `runSync`, `getTop`, `pop`, `pushValue`, `getValue`, `setField`,\n//     `createtable`-via-`lua_createtable`, and (through `Thread`) `readonly address: LuaState`\n//     and `readonly lua: LuaWasm`.\n//   · `LuaWasm` exposes `lua_sethook(L, func, mask, count)` and `module.addFunction(fn, sig)` /\n//     `module.removeFunction(ptr)` — the count-hook primitive the enforcement layers depend on.\n//   · `LuaEventMasks.Count === 8`.\n//\n// Layer 2 needs `traceAllocations: true`: wasmoon installs its custom `lua_Alloc` (the cap) ONLY\n// under that flag, and both `getMemoryUsed()` and `setMemoryMax()` THROW \"Memory allocations is\n// not being traced\" without it. So the engine is ALWAYS built with `traceAllocations: true`, the\n// cap is set via `setMemoryMax(memoryCeilingBytes)`, and the canary probe positively confirms the\n// cap refuses a large allocation under a tiny ceiling before layer 2 is claimed.\ninterface WasmoonLuaWasmModule {\n  addFunction(fn: (...args: number[]) => void, signature: string): number\n  removeFunction(ptr: number): void\n}\ninterface WasmoonLuaWasm {\n  lua_sethook: (L: number, func: number | null, mask: number, count: number) => void\n  lua_error: (L: number) => number\n  readonly module: WasmoonLuaWasmModule\n}\ninterface WasmoonThread {\n  readonly address: number\n  readonly lua: WasmoonLuaWasm\n  loadString(luaCode: string, name?: string): void\n  run(argCount?: number): Promise<unknown[]>\n  runSync(argCount?: number): unknown[]\n  getTop(): number\n  pop(count?: number): void\n  pushValue(value: unknown): void\n  getValue(index: number): unknown\n  setField(index: number, name: string, value: unknown): void\n  close(): void\n}\ninterface WasmoonGlobal extends WasmoonThread {\n  get(name: string): unknown\n  set(name: string, value: unknown): void\n  getMemoryUsed(): number\n  setMemoryMax(max: number | undefined): void\n}\ninterface WasmoonLuaEngine {\n  global: WasmoonGlobal\n  close(): void\n}\ninterface WasmoonLuaFactory {\n  createEngine(options?: {\n    openStandardLibs?: boolean\n    injectObjects?: boolean\n    enableProxy?: boolean\n    traceAllocations?: boolean\n    functionTimeout?: number\n  }): Promise<WasmoonLuaEngine>\n}\ntype WasmoonModule = {\n  LuaFactory: new (customWasmUri?: string) => WasmoonLuaFactory\n  LuaEventMasks: { Count: number }\n}\n\nconst LUA_EVENT_MASK_COUNT = 8 // LuaEventMasks.Count in wasmoon; verified against dist/index.js\n\n// ── defaults ────────────────────────────────────────────────────────────────\n/** Default instruction budget before the count hook raises. */\nconst DEFAULT_INSTRUCTION_LIMIT = 1_000_000\n/** Default memory ceiling in bytes for the allocator cap (and the secondary poll). */\nconst DEFAULT_MEMORY_CEILING_BYTES = 64 * 1024 * 1024 // 64 MiB\n/** Default wall-clock timeout before the watchdog SIGKILLs the shared process. */\nconst DEFAULT_TIMEOUT_MS = 5_000\n\n// ── errors ──────────────────────────────────────────────────────────────────\n/**\n * The stable code prefix for an out-of-memory abort. The full model-addressed message is built by\n * {@link memoryCeilingMessage} and names the ceiling in bytes, e.g.\n * `\"lua-cell: memory ceiling exceeded (65536 bytes)\"`. The count hook raises this as a Lua error\n * (via the secondary `getMemoryUsed` poll) BEFORE the allocator's hard `LUA_ERRMEM` (\"not enough\n * memory\") fires whenever the poll wins the race; if the allocator wins instead, the host catch\n * converts the raw error into this same named failure so a raw \"not enough memory\" never\n * propagates out of the cell. Surfaced to the model as a clean, named abort (via the executor's\n * `node_failed`/handled-error edge), NOT as a raw wasmoon error.\n */\nconst MEMORY_CEILING_EXCEEDED = 'lua-cell: memory ceiling exceeded'\n/**\n * Build the full model-addressed out-of-memory message naming the ceiling in bytes.\n */\nconst memoryCeilingMessage = (ceilingBytes: number): string =>\n  `${MEMORY_CEILING_EXCEEDED} (${ceilingBytes} bytes)`\n/**\n * Raised inside the VM (by the count hook) when the instruction budget or the wall-clock deadline\n * is exceeded. It crosses the `pcall` boundary as a Lua error and is caught by the host, which\n * converts it to a safe verdict.\n */\nconst INSTRUCTION_BUDGET_EXCEEDED = 'lua-cell: instruction budget exceeded'\nconst DEADLINE_EXCEEDED = 'lua-cell: wall-clock deadline exceeded'\n\n/**\n * The enforcement guarantee a cell instance is actually delivering, probed at construction.\n *\n * `full` means the count hook and the allocator cap both work. `watchdog-only` means one or both\n * probes failed and only the worker-thread watchdog remains. This is REPORTED, never claimed: a\n * cell that cannot install a hook or whose `setMemoryMax` does not gate allocation does not\n * pretend it did.\n */\nexport type LuaCellGuarantee = 'full' | 'watchdog-only'\n\n/**\n * The options accepted by {@link createLuaCell}. All optional; every field has a safe default.\n */\nexport interface CreateLuaCellOptions {\n  /**\n   * The number of VM instructions between count-hook firings. Default 1,000,000. Lowering this\n   * tightens the instruction budget AND the secondary memory poll's latency (the poll runs inside\n   * the hook and raises the named abort before the allocator's hard failure), at the cost of more\n   * hook overhead. The hook is what stops `while true do end` from inside the VM.\n   */\n  readonly instructionLimit?: number\n  /**\n   * The memory ceiling in bytes, ENFORCED by wasmoon's allocator cap (`setMemoryMax`). Default\n   * 64 MiB. The allocator refuses any growing realloc whose end size would exceed this. A\n   * secondary `getMemoryUsed` poll inside the count hook raises a clean named abort before the\n   * hard failure when it gets a firing first; an overshoot that beats the poll still hits the\n   * hard cap, and the host converts that to the same named failure.\n   */\n  readonly memoryCeilingBytes?: number\n  /**\n   * The wall-clock timeout in milliseconds before the worker-thread watchdog SIGKILLs the shared\n   * process. Default 5,000. This is the OUTER bound; the count hook is the inner one.\n   */\n  readonly timeoutMs?: number\n}\n\n/**\n * Inspectable runtime status of a Lua cell instance: which enforcement layers are live and which\n * were probed away at construction. Exposed for diagnostics and for tests that need to assert the\n * guarantee without triggering a runaway script.\n */\nexport interface LuaCellStatus {\n  /** The enforcement guarantee actually being delivered. */\n  readonly guarantee: LuaCellGuarantee\n  /** The instruction budget the count hook enforces, if the hook is live; else `null`. */\n  readonly instructionLimit: number | null\n  /** The memory ceiling the allocator cap enforces, if the cap is live; else `null`. */\n  readonly memoryCeilingBytes: number | null\n  /** The wall-clock timeout the watchdog enforces. Always live. */\n  readonly timeoutMs: number\n}\n\n/**\n * The type of the cell returned by {@link createLuaCell}. It is a {@link PredicateEvaluator} with\n * one extra field, `status()`, for inspecting the enforcement guarantee.\n */\nexport interface LuaCell extends PredicateEvaluator {\n  /**\n   * Returns the enforcement guarantee this instance is delivering. See {@link LuaCellGuarantee}.\n   * A cell that could not install a count hook or whose allocator cap probe failed reports\n   * `watchdog-only` here rather than claiming a `full` guarantee.\n   */\n  status(): LuaCellStatus\n}\n\n// ── context marshalling ─────────────────────────────────────────────────────\n//\n// `ctx.outputs` is a `ReadonlyMap<string, NodeOutput>` keyed `${nodeId}:${branchKey}`. Each\n// `NodeOutput.items[i].json` is a `Record<string, EncodableValue>` whose values may include\n// `Date`, `RegExp`, `bigint`, `Map`, `Set`, typed arrays, etc. — none of which round-trip through\n// Lua's value model. We marshal the whole table to a plain-data tree (arrays, plain objects,\n// strings, numbers, booleans, null) before pushing it into the VM, so a predicate's `ctx` global\n// is always a Lua table of plain data.\n\n/**\n * Marshal an `EncodableValue` (or any value that survived `OutputItem.json`) to plain data\n * suitable for pushing into the Lua VM. Cycles are not expected (outputs are freeze-checked for\n * encodability), but this guards against them anyway by returning a placeholder rather than\n * recursing forever.\n */\nconst marshalValue = (value: unknown, seen: Set<unknown>): unknown => {\n  if (value === null || value === undefined) return null\n  const t = typeof value\n  if (t === 'string' || t === 'number' || t === 'boolean') return value\n  if (t === 'bigint') return value.toString() + 'n' // Lua has no bignum; carry a tagged string\n  if (t === 'function') return null // predicates never see live functions\n  if (isInstanceOf(value, 'Date', Date)) {\n    return (value as Date).toISOString()\n  }\n  if (isInstanceOf(value, 'RegExp', RegExp)) {\n    return (value as RegExp).toString()\n  }\n  if (isInstanceOf(value, 'ArrayBuffer', ArrayBuffer)) {\n    return Array.from(new Uint8Array(value))\n  }\n  if (Array.isArray(value)) {\n    if (seen.has(value)) return null\n    seen.add(value)\n    return value.map((v) => marshalValue(v, seen))\n  }\n  // Typed arrays: present as plain arrays of numbers.\n  if (ArrayBuffer.isView(value) && !isInstanceOf(value, 'DataView', DataView)) {\n    const view = value as unknown as { length: number; [i: number]: number }\n    const out: number[] = []\n    for (let i = 0; i < view.length; i++) out.push(view[i])\n    return out\n  }\n  if (isInstanceOf(value, 'DataView', DataView)) {\n    return Array.from(new Uint8Array((value as DataView).buffer))\n  }\n  if (isInstanceOf(value, 'Map', Map)) {\n    if (seen.has(value)) return null\n    seen.add(value)\n    const obj: Record<string, unknown> = {}\n    for (const [k, v] of value as Map<unknown, unknown>) {\n      obj[String(k)] = marshalValue(v, seen)\n    }\n    return obj\n  }\n  if (isInstanceOf(value, 'Set', Set)) {\n    if (seen.has(value)) return null\n    seen.add(value)\n    return Array.from(value as Set<unknown>).map((v) => marshalValue(v, seen))\n  }\n  if (isObject(value)) {\n    if (seen.has(value)) return null\n    seen.add(value)\n    const obj: Record<string, unknown> = {}\n    for (const [k, v] of Object.entries(value)) {\n      obj[k] = marshalValue(v, seen)\n    }\n    return obj\n  }\n  return null\n}\n\n/**\n * Marshal the whole `OutputTable` to a plain Lua-friendly object keyed `${nodeId}:${branchKey}`.\n * Each entry is an array of `{json: {...}}` items, mirroring `NodeOutput.items` so a predicate can\n * index `ctx[nodeId .. ':' .. branchKey][i].json.field`.\n */\nconst marshalOutputs = (outputs: ReadonlyMap<string, NodeOutput>): Record<string, unknown> => {\n  const out: Record<string, unknown> = {}\n  for (const [key, nodeOutput] of outputs) {\n    out[key] = (nodeOutput.items as OutputItem[]).map((item) => ({\n      json: marshalValue(item.json, new Set<unknown>()),\n    }))\n  }\n  return out\n}\n\n// ── text-mode-only loading ──────────────────────────────────────────────────\n//\n// Lua chunks may be either text source or precompiled bytecode. wasmoon's `loadString` uses\n// `luaL_loadbufferx` with a `null` mode, which accepts BOTH. The bytecode verifier is NOT a\n// security boundary (it has been bypassed repeatedly across Lua versions), so we refuse bytecode\n// BEFORE calling the VM loader by detecting its signature: every Lua 5.x bytecode chunk begins\n// with the magic bytes `\\x1bLua` (0x1b, 'L', 'u', 'a'). A source string can never start with\n// these because they include a non-printable ESC, and a leading `\\x1b` in source would be a\n// string literal — but the chunk as a whole would not match because `load`-time source is\n// parsed, not matched byte-for-byte. We check the raw string's first four bytes.\n\nconst LUA_BYTECODE_MAGIC = 0x1b // ESC\n\n/**\n * Returns `true` if `source` looks like precompiled Lua bytecode. Refuses bytecode so the cell\n * never relies on the bytecode verifier as a security boundary.\n */\nconst looksLikeBytecode = (source: string): boolean => {\n  if (source.length < 4) return false\n  return (\n    source.charCodeAt(0) === LUA_BYTECODE_MAGIC &&\n    source.charCodeAt(1) === 0x4c && // 'L'\n    source.charCodeAt(2) === 0x75 && // 'u'\n    source.charCodeAt(3) === 0x61 // 'a'\n  )\n}\n\n// ── a per-evaluate VM instance ──────────────────────────────────────────────\n//\n// A fresh engine is built for each `evaluate()` call. This is deliberate: it gives every\n// predicate a clean global table (no state leaks between branches), and it makes the instruction\n// budget and memory ceiling per-evaluation rather than per-cell. Building a WASM engine per call\n// is not free, but predicates are short and run at most once per node, so the cost is acceptable\n// for the isolation it buys.\n\ninterface VmHandle {\n  readonly engine: WasmoonLuaEngine\n  readonly global: WasmoonGlobal\n  /** The count-hook function pointer, to remove on teardown. `null` if no hook was installed. */\n  readonly hookPointer: number | null\n  /** True if the allocator cap is live (`setMemoryMax` gates allocation, confirmed by probe). */\n  readonly memoryCapLive: boolean\n}\n\n/**\n * Build a fresh, sandboxed engine and install the count hook. Returns a handle that must be\n * closed via `closeVm` in a `finally`. The hook enforces the instruction budget and runs the\n * secondary memory poll (which raises a clean named abort before the allocator's hard failure);\n * the allocator cap (`setMemoryMax`) is set on the engine itself; the wall-clock deadline is\n * checked in the hook too so a script that runs many short instructions without hitting the\n * instruction count is still bounded by time.\n */\nconst buildVm = async (\n  wasmoon: WasmoonModule,\n  opts: {\n    instructionLimit: number\n    memoryCeilingBytes: number\n    deadline: number\n    /** True if the allocator cap probe passed; the engine sets `setMemoryMax` and runs the poll. */\n    memoryCapLive: boolean\n  }\n): Promise<VmHandle> => {\n  const factory = new wasmoon.LuaFactory()\n  // ALLOWLIST build: no standard libs, no injected objects, no proxy. `traceAllocations: true`\n  // is REQUIRED for layer 2 — wasmoon installs its custom `lua_Alloc` (the cap) ONLY under that\n  // flag, and both `setMemoryMax` and `getMemoryUsed` throw without it — so the engine is ALWAYS\n  // built with tracing on, even when the cap probe failed (tracing is harmless and keeps\n  // `getMemoryUsed` available for the secondary poll). `enableProxy: false` keeps the global\n  // table a plain Lua table and prevents the proxy layer from synthesising globals on read\n  // (which would defeat the sandbox).\n  const engine = await factory.createEngine({\n    openStandardLibs: false,\n    injectObjects: false,\n    enableProxy: false,\n    traceAllocations: true,\n    functionTimeout: undefined, // we own the hook; do not also wire wasmoon's\n  })\n  const global = engine.global\n\n  // Layer 2: the REAL allocator cap. `setMemoryMax` makes wasmoon's custom `lua_Alloc` REFUSE any\n  // growing realloc whose end size would exceed the ceiling, returning NULL so the VM raises\n  // `LUA_ERRMEM` (\"not enough memory\"). This is the enforcement; the poll below is secondary.\n  if (opts.memoryCapLive) {\n    global.setMemoryMax(opts.memoryCeilingBytes)\n  }\n\n  // A hard total on hook firings. wasmoon's count hook `count` parameter is a per-firing\n  // INTERVAL, and wasmoon exposes no running total, so `instructionLimit` is the interval\n  // between checks (a script that runs forever fires the hook forever, and each firing\n  // re-checks the deadline and memory). The firings counter below turns \"interval\" into a\n  // hard total: after MAX_HOOK_FIRINGS firings the budget is exhausted and the hook raises.\n  // 10 firings means ~10x the per-check budget — generous for a legitimate predicate, fatal\n  // for an infinite loop. The wall-clock watchdog is the real backstop.\n  const MAX_HOOK_FIRINGS = 10\n  let instructionBudgetHookFirings = 0\n\n  // Install our own count hook with OUR instruction count (wasmoon's setTimeout hardcodes 1000).\n  // The hook fires every `opts.instructionLimit` VM instructions and checks all three bounds.\n  let hookPointer: number | null = null\n  try {\n    const module = global.lua.module\n    hookPointer = module.addFunction((_L: number): void => {\n      // Wall-clock deadline: checked on every hook firing so a long run of cheap instructions\n      // is still bounded. This is the in-VM early-out; the worker watchdog is the outer bound.\n      if (Date.now() > opts.deadline) {\n        global.pushValue(new Error(DEADLINE_EXCEEDED))\n        global.lua.lua_error(global.address)\n        return\n      }\n      // Secondary memory poll (only when the cap is live, since both need `traceAllocations`).\n      // This is NOT the enforcement — `setMemoryMax` is. The poll's job is to raise a clean,\n      // named, model-addressed `MEMORY_CEILING_EXCEEDED` error naming the ceiling BEFORE the\n      // allocator's hard `LUA_ERRMEM` (\"not enough memory\") fires, so the common case produces a\n      // friendly named abort rather than a raw wasmoon error. A single large allocation between\n      // firings can still beat the poll to the hard cap; the host catch in `evaluate` converts\n      // that raw failure into the same named failure, so a raw \"not enough memory\" never escapes.\n      if (opts.memoryCapLive) {\n        let used = 0\n        try {\n          used = global.getMemoryUsed()\n        } catch {\n          // If tracing silently failed at runtime, treat the poll as unavailable — the allocator\n          // cap and the watchdog still bound us. Do not abort from the poll.\n          used = 0\n        }\n        if (used > opts.memoryCeilingBytes) {\n          global.pushValue(new Error(memoryCeilingMessage(opts.memoryCeilingBytes)))\n          global.lua.lua_error(global.address)\n          return\n        }\n      }\n      // Instruction budget: each firing consumes one interval. After MAX_HOOK_FIRINGS the\n      // total budget is exhausted and the hook raises (see the interpretation note above).\n      instructionBudgetHookFirings++\n      if (instructionBudgetHookFirings > MAX_HOOK_FIRINGS) {\n        global.pushValue(new Error(INSTRUCTION_BUDGET_EXCEEDED))\n        global.lua.lua_error(global.address)\n      }\n    }, 'vii')\n    global.lua.lua_sethook(global.address, hookPointer, LUA_EVENT_MASK_COUNT, opts.instructionLimit)\n  } catch {\n    // Could not install the hook. The canary should have caught this at construction; if we reach\n    // here at evaluate time, remove a partial pointer and fall back to watchdog-only for this\n    // evaluation. The run is still correct, merely less protected.\n    if (hookPointer !== null) {\n      try {\n        global.lua.module.removeFunction(hookPointer)\n      } catch {\n        /* best effort */\n      }\n      hookPointer = null\n    }\n  }\n\n  return { engine, global, hookPointer, memoryCapLive: opts.memoryCapLive }\n}\n\n/**\n * Tear down a VM built by `buildVm`. Safe to call on a partially-built handle. Removes the count\n * hook pointer and closes the engine, releasing the WASM allocation.\n */\nconst closeVm = (handle: VmHandle | null): void => {\n  if (!handle) return\n  const { engine, global, hookPointer } = handle\n  if (hookPointer !== null) {\n    try {\n      global.lua.lua_sethook(global.address, null, 0, 0)\n    } catch {\n      /* best effort */\n    }\n    try {\n      global.lua.module.removeFunction(hookPointer)\n    } catch {\n      /* best effort */\n    }\n  }\n  try {\n    engine.close()\n  } catch {\n    /* best effort */\n  }\n}\n\n// ── the worker-thread watchdog ──────────────────────────────────────────────\n//\n// THE OUTER BOUND. A main-thread timer cannot do this job: a tight Lua loop owns the main thread\n// synchronously and starves setTimeout/nextTick/queueMicrotask/setImmediate alike, so no\n// main-thread timer ever fires. Only a separate event loop (a worker_thread) keeps time. On\n// expiry the worker SIGKILLs the SHARED process (not process.exit, which ends only the worker).\n//\n// The watchdog is authored as a Worker created from a source string, so this file stays a single\n// module with no second file to ship. The worker body is intentionally tiny: it arms a timer for\n// `timeoutMs`, then `process.kill(pid, 'SIGKILL')`s the host. `worker.unref()` is called so a\n// successful run does not keep the process alive.\n\n/**\n * The source of the watchdog worker. It receives `{ pid, deadline }` via `workerData`, arms a\n * timer to the absolute deadline epoch, and on expiry `process.kill(pid, 'SIGKILL')`s the shared\n * process. Using an absolute deadline (not a relative timeout) means the host and worker never\n * disagree about when \"expired\" is, and a deadline that was already in the past when the worker\n * started fires immediately.\n */\nconst WATCHDOG_SOURCE = `\nconst { workerData } = require('node:worker_threads')\nconst process = require('node:process')\nconst { pid, deadline } = workerData\nconst now = Date.now()\nif (now >= deadline) {\n  // Already expired BEFORE the worker started its timer. Kill synchronously so no host work that\n  // started past the deadline can complete. The host also checks this before its first import.\n  try { process.kill(pid, 'SIGKILL') } catch (_) {}\n} else {\n  const timer = setTimeout(() => {\n    try { process.kill(pid, 'SIGKILL') } catch (_) {}\n  }, deadline - now)\n  if (typeof timer.unref === 'function') timer.unref()\n}\n`\n\n/**\n * The Node-only worker_threads import, isolated so static analyzers can see exactly which\n * `node:*` modules this subpath pulls in. This is the load-bearing `node:*` import that makes the\n * module browser-incompatible by design.\n */\ntype WorkerLike = {\n  unref(): void\n  terminate(): Promise<number>\n}\n\ntype WorkerConstructor = new (\n  filename: string | URL,\n  options?: {\n    eval?: boolean\n    workerData?: unknown\n  }\n) => WorkerLike\n\n// `require('node:worker_threads')` is deferred to `armWatchdog` so that merely importing this\n// module (e.g. for types) does not pull `node:worker_threads` into a browser bundle. The dynamic\n// require is the boundary.\nlet WorkerCtor: WorkerConstructor | null = null\nconst getWorkerCtor = (): WorkerConstructor => {\n  if (WorkerCtor) return WorkerCtor\n\n  const wt = require('node:worker_threads') as {\n    Worker: WorkerConstructor\n  }\n  WorkerCtor = wt.Worker\n  return WorkerCtor\n}\n\n/**\n * Arm the worker-thread watchdog. Returns a disarm function that terminates the worker. The\n * watchdog SIGKILLs the shared process at `deadline`. `worker.unref()` is called so a successful\n * run does not keep the process alive.\n *\n * The expired-deadline check is synchronous and before the first wasmoon import on the host side\n * as well (see `evaluate`); the worker independently checks the same absolute epoch before arming\n * its timer, so a process that started past its deadline is killed from either side.\n */\nconst armWatchdog = (deadline: number): (() => void) => {\n  const Worker = getWorkerCtor()\n  const worker = new Worker(WATCHDOG_SOURCE, {\n    eval: true,\n    workerData: { pid: getPid(), deadline },\n  })\n  worker.unref()\n  return () => {\n    try {\n      void worker.terminate()\n    } catch {\n      /* best effort */\n    }\n  }\n}\n\n// `process` is read only for `process.pid` inside `armWatchdog`, via a lazy `require` so the\n// static `node:*` import surface stays at exactly what the watchdog needs. The host side uses\n// `Date.now()` against the same `deadline` epoch inside the count hook, so host and worker share\n// one definition of \"expired\".\nconst getPid = (): number => {\n  const p = require('node:process') as { pid: number }\n  return p.pid\n}\n\n// ── the cell ────────────────────────────────────────────────────────────────\n\n/**\n * Construct a Lua predicate evaluator cell.\n *\n * @remarks\n * **THE WATCHDOG'S LAST RESORT IS `SIGKILL` ON THE HOST PROCESS — read this before wiring the\n * cell.** wasmoon is WebAssembly, so the Lua VM runs IN-PROCESS on the main thread. The watchdog\n * is a worker thread, but what it kills is `process.pid`: your process, not an isolated\n * evaluator.\n *\n * That is deliberate and there is no lighter option. A synchronous WASM loop owns the main\n * thread, so `worker.terminate()` has nothing to terminate and `process.exit()` never runs; only\n * the OS killing the process breaks it. A timeout that cannot be enforced is not a timeout, and\n * this cell would rather enforce one violently than advertise one it cannot deliver.\n *\n * Be clear about the trade: **a non-terminating Lua predicate takes the whole process down with\n * it.** The instruction-count hook and the allocator cap normally stop a runaway long before the\n * deadline — the watchdog is the last resort, not the first — but if the construction canaries\n * fail those probes, {@link LuaCell.status} reports the reduced guarantee and the watchdog is all\n * that remains.\n *\n * If a process-wide kill is unacceptable in your deployment, do not wire this cell for untrusted\n * predicates. `createStructuredCell` cannot loop at all.\n *\n * @param options - Optional enforcement tuning: instruction budget, memory ceiling, timeout.\n * @returns A {@link LuaCell} with `id: 'lua'`, ready to be wired into an orchestration battery's\n *   `evaluators`. The cell's `load()` lazily imports `wasmoon` (an optional peer) and maps a\n *   missing peer to `E_ORCH_CELL_UNAVAILABLE`. `validate()` refuses a non-string predicate and\n *   loads the chunk so a syntax error surfaces at freeze. `evaluate()` builds a fresh sandboxed\n *   VM per call, injects the marshalled `ctx.outputs` as a `ctx` global, and interprets the\n *   result as a branch or select verdict.\n *\n *   The cell probes the count hook and the allocator cap against a canary at construction (the\n *   cap probe positively confirms `setMemoryMax` plus a tiny ceiling actually REFUSES a large\n *   allocation) and reports the actual enforcement guarantee via `status()`. If either probe\n *   fails it falls back to watchdog-only enforcement and REPORTS the reduced guarantee rather\n *   than claiming it.\n *\n *   **Do not run the runaway-script test in-process.** A test that confirms the watchdog\n *   actually SIGKILLs the process on `while true do end` must spawn a CHILD process and observe\n *   its exit signal; running it inside this process would kill the test runner. That test lives\n *   in a separate, opt-in harness and is never invoked by `evaluate` itself.\n */\nexport const createLuaCell = (options?: CreateLuaCellOptions): LuaCell => {\n  const instructionLimit = options?.instructionLimit ?? DEFAULT_INSTRUCTION_LIMIT\n  const memoryCeilingBytes = options?.memoryCeilingBytes ?? DEFAULT_MEMORY_CEILING_BYTES\n  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS\n\n  // Probed at load() time. The canary determines whether the count hook and the allocator cap\n  // are actually available on this platform; the results are stored here and reported via\n  // status(). `memoryCapLive` is set ONLY by a POSITIVE probe — `setMemoryMax` plus a tiny\n  // ceiling actually REFUSING a large allocation — never by a negative grep or an absence of a\n  // throw on mere construction.\n  let hookLive = false\n  let memoryCapLive = false\n\n  const load = loadOnce('lua', async () => {\n    // The first import of wasmoon is the point where an optional-peer failure surfaces as\n    // E_ORCH_CELL_UNAVAILABLE (loadOnce maps it). After a successful import we probe.\n    const wasmoon = (await import('wasmoon')) as unknown as WasmoonModule\n    if (typeof wasmoon.LuaEventMasks?.Count !== 'number') {\n      throw new TypeError('wasmoon: LuaEventMasks.Count missing — unsupported wasmoon version')\n    }\n\n    // Canary probe, in two parts.\n    //\n    // (1) Count hook: build a throwaway engine with `memoryCapLive: false` (no cap set yet), try\n    //     to `addFunction` + `lua_sethook`, and confirm the pointer is non-null. We do NOT fire\n    //     the hook — firing would require running a script, which we refuse without a watchdog.\n    //\n    // (2) Allocator cap: a POSITIVE probe. Build a SECOND throwaway engine, call `setMemoryMax`\n    //     with a DELIBERATELY TINY ceiling, and run a BOUNDED chunk that allocates a lot\n    //     (`for i = 1, 200000 do t[i] = i end` — a fixed 200k iterations, NOT `while true do`,\n    //     so it cannot loop forever). Layer 2 is claimed ONLY if that run THROWS — i.e. the\n    //     allocator actually REFUSED the allocation. A `setMemoryMax` that silently fails to gate\n    //     allocation (the failure mode the earlier negative grep hid) is caught here: the run\n    //     would succeed and the probe would report the reduced guarantee. The watchdog is armed\n    //     around the run for defence in depth, even though the chunk is bounded.\n    let hookCanary: VmHandle | null = null\n    try {\n      hookCanary = await buildVm(wasmoon, {\n        instructionLimit,\n        memoryCeilingBytes,\n        deadline: Date.now() + timeoutMs,\n        memoryCapLive: false,\n      })\n      hookLive = hookCanary.hookPointer !== null\n    } finally {\n      closeVm(hookCanary)\n    }\n\n    if (hookLive) {\n      // (2) Positive allocator-cap probe. The tiny ceiling must be SMALLER than what the bounded\n      // chunk allocates, or the refusal would not fire. 64 KiB is far below the ~3 MB the chunk\n      // needs, and is independent of the user's `memoryCeilingBytes` so the probe is stable.\n      const PROBE_CEILING = 64 * 1024\n      const PROBE_CHUNK = 'local t = {} for i = 1, 200000 do t[i] = i end'\n      let capCanary: VmHandle | null = null\n      const disarmProbe = armWatchdog(Date.now() + timeoutMs)\n      try {\n        capCanary = await buildVm(wasmoon, {\n          instructionLimit,\n          memoryCeilingBytes: PROBE_CEILING,\n          deadline: Date.now() + timeoutMs,\n          memoryCapLive: true,\n        })\n        capCanary.global.loadString(PROBE_CHUNK, 'lua-cell: cap probe')\n        try {\n          // `runSync` runs the chunk synchronously on the host thread; the allocator refuses the\n          // oversized realloc and `assertOk` throws. We do NOT `await run` here because the\n          // probe must be synchronous so the watchdog deadline is the only async boundary.\n          capCanary.global.runSync(0)\n          // No throw → the cap did NOT gate allocation. Do not claim layer 2.\n          memoryCapLive = false\n        } catch (err) {\n          // A throw is the EXPECTED outcome. Accept it as proof the cap is real only when the\n          // error is the OOM one (raw \"not enough memory\" or our named `MEMORY_CEILING_EXCEEDED`);\n          // any other error (a hook install failure, a syntax error in the probe chunk) is not\n          // proof of the cap and must not claim it.\n          memoryCapLive = isOutOfMemoryError(err)\n        }\n      } finally {\n        closeVm(capCanary)\n        disarmProbe()\n      }\n    } else {\n      memoryCapLive = false\n    }\n  })\n\n  const validate = async (node: PlanNode): Promise<void> => {\n    await load()\n    const def = node.definition as BranchNodeDefinition | SelectNodeDefinition\n    if (node.kind !== 'branch' && node.kind !== 'select') {\n      throw new TypeError(\n        `lua cell: node kind '${node.kind}' is not evaluable — only 'branch' and 'select' carry a Lua predicate`\n      )\n    }\n    const predicate = def.predicate\n    if (typeof predicate !== 'string') {\n      throw new TypeError(\n        `lua cell: predicate must be a SOURCE STRING for the lua cell (node '${node.id}'). Got ${typeof predicate}.`\n      )\n    }\n    if (predicate.length === 0) {\n      throw new TypeError(`lua cell: predicate is an empty string (node '${node.id}').`)\n    }\n    if (looksLikeBytecode(predicate)) {\n      throw new TypeError(\n        `lua cell: predicate looks like precompiled Lua bytecode, which is refused (node '${node.id}'). The bytecode verifier is not a security boundary; supply source text.`\n      )\n    }\n    if (node.kind === 'select') {\n      const cases = (def as SelectNodeDefinition).cases\n      if (!Array.isArray(cases) || cases.length === 0) {\n        throw new TypeError(\n          `lua cell: select node '${node.id}' must declare a non-empty 'cases' array.`\n        )\n      }\n      for (const c of cases) {\n        if (typeof c !== 'string') {\n          throw new TypeError(\n            `lua cell: select node '${node.id}' has a non-string case label: ${typeof c}.`\n          )\n        }\n      }\n    }\n    // Load the chunk so a syntax error surfaces at FREEZE rather than at run time. We build a\n    // throwaway engine for this: `loadString` parses but does not run, so no watchdog is needed.\n    const wasmoon = (await import('wasmoon')) as unknown as WasmoonModule\n    let canary: VmHandle | null = null\n    try {\n      canary = await buildVm(wasmoon, {\n        instructionLimit,\n        memoryCeilingBytes,\n        deadline: Date.now() + timeoutMs,\n        memoryCapLive: memoryCapLive,\n      })\n      // `loadString` throws on a syntax error (it asserts the luaL_loadbufferx result).\n      canary.global.loadString(predicate, `node:${node.id}`)\n    } finally {\n      closeVm(canary)\n    }\n  }\n\n  const evaluate = async (node: PlanNode, ctx: PredicateContext): Promise<PredicateVerdict> => {\n    await load()\n    // Expired-deadline check: SYNCHRONOUS and BEFORE the first wasmoon import on this call path.\n    // (wasmoon was already imported by load(), but we re-check the deadline here so a long gap\n    // between load and evaluate is also caught.) The worker checks the same epoch independently.\n    const deadline = Date.now() + timeoutMs\n    if (Date.now() > deadline) {\n      return safeFallbackVerdict(node)\n    }\n\n    const def = node.definition as BranchNodeDefinition | SelectNodeDefinition\n    if (typeof def.predicate !== 'string') {\n      // validate() should have caught this; a non-string here is a freeze-time bug. Never throw\n      // out of a predicate — return the safe fallback.\n      return safeFallbackVerdict(node)\n    }\n\n    const wasmoon = (await import('wasmoon')) as unknown as WasmoonModule\n    // Arm the watchdog FIRST, before any VM work, so the outer bound is in place.\n    const disarm = armWatchdog(deadline)\n    let handle: VmHandle | null = null\n    try {\n      handle = await buildVm(wasmoon, {\n        instructionLimit,\n        memoryCeilingBytes,\n        deadline,\n        memoryCapLive,\n      })\n      const { global } = handle\n\n      // Inject the marshalled context as a `ctx` global. A predicate reads\n      // `ctx['nodeId:branchKey'][i].json.field`.\n      global.set('ctx', marshalOutputs(ctx.outputs))\n\n      // Load the chunk in text mode. `loadString` uses luaL_loadbufferx with a null mode, which\n      // accepts both text and bytecode; we already refused bytecode in validate(), and we refuse\n      // it again here for defence in depth.\n      if (looksLikeBytecode(def.predicate)) {\n        return safeFallbackVerdict(node)\n      }\n      global.loadString(def.predicate, `node:${node.id}`)\n\n      // Run the chunk. The count hook interrupts a runaway loop by raising a Lua error, which\n      // `run` surfaces as a rejected promise. An out-of-memory abort is surfaced as a CLEAN,\n      // NAMED, model-addressed failure (via the secondary poll's `MEMORY_CEILING_EXCEEDED`, or —\n      // if the allocator's hard cap beat the poll — by converting the raw \"not enough memory\" to\n      // the same named failure here), so a raw wasmoon error never propagates. Other runtime\n      // errors (unknown global, type errors) are converted to the safe fallback — a predicate is\n      // never allowed to crash the run.\n      let result: unknown\n      try {\n        result = await global.run(0)\n      } catch (err) {\n        if (isOutOfMemoryError(err)) {\n          // Out of memory: surface as a clean, named, model-addressed failure naming the\n          // ceiling. This is NOT the safe fallback — resource exhaustion is a runtime condition\n          // the model/operator should SEE, not a predicate logic result to be silently defaulted.\n          // The executor's existing error handling turns this thrown Error into a `node_failed`\n          // (or handled `error` edge) whose `message` is the named, ceiling-naming string below.\n          throw new Error(memoryCeilingMessage(memoryCeilingBytes))\n        }\n        // A hook-induced instruction/deadline interruption or a non-OOM runtime error. A\n        // predicate is never allowed to crash the run, so this is the safe fallback — NOT a\n        // rethrow.\n        return safeFallbackVerdict(node, err)\n      }\n\n      // wasmoon's `run(0)` returns a `MultiReturn` (an Array subclass) carrying ALL of the\n      // chunk's return values, NOT a single value. Passing the `MultiReturn` itself to\n      // `toBoolean`/`toCaseLabel` would see a non-empty array object — truthy — for EVERY\n      // successful evaluation regardless of what the predicate actually returned, so `false`,\n      // `nil` and `true` all collapsed to `matched: true`. Take the FIRST returned value out of\n      // the `MultiReturn` before interpreting it; an empty `MultiReturn` (a chunk with no\n      // `return` statement) is Lua `nil` (JavaScript `undefined`), so a no-return branch is\n      // `matched: false`, not `true`.\n      const first = firstReturnValue(result)\n\n      // Interpret the result by node kind.\n      if (node.kind === 'branch') {\n        const matched = toBoolean(first)\n        return { kind: 'branch', matched }\n      }\n      // select: evaluate each declared case in order, return the first whose predicate matches.\n      const selectDef = def as SelectNodeDefinition\n      // The predicate string is the SELECT DISPATCHER: it is expected to RETURN a case label\n      // (a string) or nil. We honour that contract: the first returned string that is a member\n      // of `cases` is the verdict; nil or a non-member yields the default (null).\n      const label = toCaseLabel(first, selectDef.cases)\n      return { kind: 'select', caseLabel: label }\n    } catch (err) {\n      // An out-of-memory abort propagates as the clean named failure (it was either raised by\n      // the secondary poll or converted from the allocator's raw \"not enough memory\" above). Do\n      // NOT swallow it into the safe fallback — the model/operator must see resource exhaustion.\n      if (isOutOfMemoryError(err)) {\n        throw new Error(memoryCeilingMessage(memoryCeilingBytes))\n      }\n      // Any other host-side failure (engine build, setglobal, etc.) is converted to the safe\n      // fallback — a predicate is never allowed to crash the run for non-resource errors.\n      return safeFallbackVerdict(node, err)\n    } finally {\n      closeVm(handle)\n      disarm()\n    }\n  }\n\n  const status = (): LuaCellStatus => ({\n    guarantee: hookLive && memoryCapLive ? 'full' : 'watchdog-only',\n    instructionLimit: hookLive ? instructionLimit : null,\n    memoryCeilingBytes: memoryCapLive ? memoryCeilingBytes : null,\n    timeoutMs,\n  })\n\n  return {\n    id: 'lua',\n    load,\n    validate,\n    evaluate,\n    status,\n  }\n}\n\n// ── verdict helpers ─────────────────────────────────────────────────────────\n\n/**\n * Whether an error is the Lua/wasmoon out-of-memory signal: either wasmoon's raw\n * `LUA_ERRMEM` message (\"not enough memory\", surfaced verbatim by `assertOk` for `ErrorMem`),\n * or this cell's own named `MEMORY_CEILING_EXCEEDED` abort (raised by the secondary poll or\n * re-thrown by the host catch). Used to single out an OOM from other runtime errors: an OOM is\n * surfaced as a clean, named, model-addressed failure (naming the ceiling), while other runtime\n * errors are converted to the safe fallback verdict. Never throws.\n */\nconst isOutOfMemoryError = (err: unknown): boolean => {\n  if (!isInstanceOf(err, 'Error', Error)) return false\n  const msg = err.message\n  if (typeof msg !== 'string') return false\n  if (msg.startsWith(MEMORY_CEILING_EXCEEDED)) return true\n  return /not enough memory/i.test(msg)\n}\n\n/**\n * Extract the single value a predicate returned from a wasmoon call result.\n *\n * wasmoon's `Thread.run(argCount)` / `runSync(argCount)` return a `MultiReturn` — an `Array`\n * subclass (`wasmoon/dist/multireturn.d.ts`) carrying ALL of the chunk's return values, built by\n * `getStackValues` as `new MultiReturn(returns)` with `returnValues[i] = getValue(start + i + 1)`\n * for every value left on the Lua stack. So `result` is an array-like, and handing it straight to\n * {@link toBoolean} / {@link toCaseLabel} would see a non-empty array object — truthy in both JS\n * and the old `toBoolean` — for EVERY successful evaluation, whatever the predicate returned:\n * `return 1 > 2`, `return false`, `return nil` all became `matched: true`. That is the defect\n * this helper closes.\n *\n * We take the FIRST returned value (a predicate is a single-value expression; extra returns are\n * ignored by contract). A chunk that returns nothing leaves the stack empty, so `MultiReturn` is\n * length 0; that is Lua `nil`, surfaced here as JavaScript `undefined`, so a no-return branch is\n * `matched: false` rather than `true`.\n *\n * Defensive against a non-`MultiReturn` result (a future wasmoon surface, or a mock): a non-array\n * is passed through unchanged so interpretation still works.\n */\nconst firstReturnValue = (result: unknown): unknown => {\n  if (Array.isArray(result)) {\n    return result.length === 0 ? undefined : (result as unknown[])[0]\n  }\n  return result\n}\n\n/**\n * Convert a Lua return value to a boolean for a `branch` verdict using **Lua truthiness**, NOT\n * JS truthiness. In Lua only `false` and `nil` are falsy; EVERYTHING else is truthy — including\n * `0` and `''`, which are falsy in JS. So this does NOT apply `!!value` (that would make `0` and\n * `''` falsy and break a predicate author's Lua expectations); it returns `false` only for Lua\n * `nil` (surfaced by wasmoon as JavaScript `null`/`undefined`) and Lua `false` (surfaced as JS\n * `false`), and `true` for any other value — number (incl. `0`), string (incl. `''`), table,\n * function, etc. Author receives the value the predicate returned, interpreted the way Lua\n * itself would interpret it.\n */\nconst toBoolean = (value: unknown): boolean => {\n  if (value === null || value === undefined) return false\n  if (value === false) return false\n  // wasmoon surfaces Lua `nil` as JavaScript `null`/`undefined`; a Lua `false` as JS `false`.\n  // Everything else is truthy in Lua — including `0` and `''` (which are falsy in JS but TRUTHY\n  // in Lua). We deliberately do NOT use JS `!!value` here.\n  return true\n}\n\n/**\n * Convert a Lua return value to a select case label. Returns the label if it is a string and a\n * member of `cases`; otherwise `null` (the `default` handle). A non-string return (number,\n * boolean, table) is NOT coerced — the select contract is \"return the label string or nil\", and\n * coercing would let a predicate accidentally match by numeric index.\n */\nconst toCaseLabel = (value: unknown, cases: readonly string[]): string | null => {\n  if (typeof value !== 'string') return null\n  return cases.includes(value) ? value : null\n}\n\n/**\n * The safe fallback verdict for a node when a predicate cannot be evaluated — a runtime error, a\n * hook interruption, an expired deadline, a host-side failure, or a non-string predicate reaching\n * evaluate. For a `branch` this is `{kind:'branch', matched:false}` (the `no_match`/`default`\n * path); for a `select` this is `{kind:'select', caseLabel:null}` (the `default` handle). A\n * predicate is never allowed to crash the run, so this NEVER throws.\n */\nconst safeFallbackVerdict = (node: PlanNode, _err?: unknown): PredicateVerdict => {\n  if (node.kind === 'branch') return { kind: 'branch', matched: false }\n  return { kind: 'select', caseLabel: null }\n}\n"],"mappings":"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"}