{"version":3,"file":"jexl.cjs","names":[],"sources":["../../../../src/batteries/orchestration/cells/jexl.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/batteries/orchestration/cells/jexl\n *\n * The `jexl` predicate cell.\n *\n * This cell lets a branch or select node express its predicate as a JEXL expression SOURCE\n * STRING, evaluated against a plain-data snapshot of the run's outputs. It is the alternative to\n * the declarative {@link import('./structured') structured} cell for authors who want the express\n * power of a real expression grammar.\n *\n * Why it is safe without a watchdog\n * ---------------------------------\n * RESOURCE BOUND — stated because the absence of a watchdog is easy to over-read. Expression-only\n * rules out non-termination, not slowness: a collection filter is LINEAR in the collection, so\n * evaluation cost scales with whatever a `call` node returned. `PlanBounds.maxEncodedBytes` caps\n * the PLAN, not a tool's runtime output, and the predicate reads the output. Measured: a filter\n * over 200,000 elements evaluates in roughly 100ms, so ordinary data is not a concern — but a consumer\n * whose tool can return unbounded data should cap it in `CallInvokerFn`, because this cell will\n * not.\n *\n * JEXL is a custom lexer/parser/AST interpreter, **not** `eval()`. It is expression-only by\n * design: there are no statements, no assignment, no loops, no function definitions. It is\n * therefore structurally non-Turing-complete and cannot fail to terminate on anything but\n * pathological data size. The grammar deliberately exposes only the safe surface —\n * comparisons, ternary/elvis, collection filtering (`employees[.age >= retireAge].first`, which\n * JEXL translates to a `filter` + `map` + projection), and the `|` transform pipe.\n *\n * The security boundary is the transform pipe. Every transform is host-registered through\n * `addTransform`, so the host decides the entire callable surface. This cell ships a CLOSED\n * ALLOWLIST: accept an optional `transforms` option, register exactly those, and make sure a\n * predicate cannot reach anything unregistered. If no `transforms` are supplied the pipe\n * resolves nothing, and any predicate that reaches for a transform fails closed at evaluation.\n *\n * Two cross-cutting properties hold by construction and are worth carrying into any caller:\n *\n * 1. The context is PRE-MARSHALLED PLAIN DATA. The evaluator builds a fresh plain record from\n *    `ctx.outputs` and never hands JEXL a live object whose methods are reachable, so a\n *    predicate can read values but cannot invoke arbitrary code through object identity.\n * 2. There is no clock and no randomness unless deliberately injected through a transform. A\n *    branch or select node is therefore safe to re-enter unconditionally when a run resumes:\n *    re-evaluating the same source string against the same snapshot always yields the same\n *    verdict. Evaluation stays synchronous via `evalSync` — reproducible even though the cell's\n *    seam is async.\n *\n * Honest dependency note\n * ----------------------\n * JEXL was last published 2022-06-19. It is a stable-but-frozen dependency rather than an\n * actively maintained one. That is acceptable here because the grammar this cell exposes is\n * closed and the transform surface is host-owned, so there is nothing upstream can change that\n * this cell depends on; but it should be read plainly: do not assume ongoing upstream work.\n * Unlike the Lua cell, this cell is browser-safe.\n */\n\nimport { loadOnce } from '../predicates'\nimport { isObject, isError } from '../../../lib/utils/guards'\nimport type {\n  EncodableValue,\n  PredicateEvaluator,\n  PredicateContext,\n  PredicateVerdict,\n  PlanNode,\n} from './../types'\n\n// ── the jexl surface this cell admits ────────────────────────────────────────\n// Declared structurally rather than imported: `jexl` is an optional peer that ships no usable\n// types here, so we describe just the subset this cell calls. The runtime module is cast to this\n// shape after the lazy import.\ninterface JexlExpression {\n  /** Synchronously evaluate the compiled expression against a plain-data context. */\n  evalSync(context: unknown): unknown\n  /** Parse the expression immediately, throwing a JEXL error on a syntax problem without evaluating. */\n  compile(): unknown\n}\n\ninterface JexlEngine {\n  /** Register a named transform; the only way a predicate reaches host code. Returns nothing. */\n  addTransform(name: string, fn: (value: unknown, ...args: unknown[]) => unknown): void\n  /**\n   * Build an expression object WITHOUT parsing it. `compile()` is what parses — a statement form\n   * such as `count = 5` survives this call and is only refused once compiled.\n   */\n  createExpression(source: string): JexlExpression\n  /** Parse source, throwing a JEXL error on a syntax problem without evaluating. */\n  compile(source: string): JexlExpression\n  /** Parse and synchronously evaluate source against a plain-data context. */\n  evalSync(source: string, context?: Record<string, unknown>): unknown\n}\n\n/** The module shape a dynamic `import('jexl')` resolves to. */\ninterface JexlModule {\n  /** The JEXL engine constructor; instantiate one per cell so transforms stay closed. */\n  Jexl: new () => JexlEngine\n}\n\n/**\n * A host-registered transform, keyed by the name a predicate uses on the `|` pipe.\n *\n * The first argument is the piped value; the rest are the arguments given in the predicate. The\n * value is whatever the preceding expression produced (never a live object with reachable\n * methods — the context is marshalled plain data), so a transform should treat its input as a\n * value and return a value.\n */\nexport type JexlTransform = (value: unknown, ...args: unknown[]) => unknown\n\n/**\n * Options for {@link createJexlCell}.\n */\nexport interface JexlCellOptions {\n  /**\n   * The CLOSED transform allowlist to register on the engine, keyed by pipe name.\n   *\n   * Registering nothing (the default) means the `|` pipe resolves no transform at all. This is\n   * the cell's security boundary: a predicate can never reach a transform that was not listed\n   * here.\n   */\n  transforms?: Record<string, JexlTransform>\n}\n\n/** Describe a non-string predicate so a validation error can name what was actually given. */\nconst describeValue = (value: unknown): string => {\n  if (value === null) return 'null'\n  if (value === undefined) return 'undefined'\n  if (typeof value === 'string') return `a string`\n  if (Array.isArray(value)) return 'an array'\n  if (isObject(value)) return 'an object'\n  return `a ${typeof value} value`\n}\n\n/**\n * Build the plain-data snapshot a branch/select predicate reads.\n *\n * The readable context is assembled purely from `ctx.outputs` into a fresh plain record: each\n * output key maps to the merged `json` of that output's items. Nothing in the result inherits\n * reachable methods and nothing is mutated — jexl gets a value snapshot, never live objects.\n *\n * @param outputs - the run's output table to marshal.\n * @returns a plain record keyed identically to the table, holding the merged item payloads.\n */\nconst snapshotOf = (\n  outputs: ReadonlyMap<string, { items: { json: Record<string, EncodableValue> }[] }>\n): Record<string, unknown> => {\n  const byKey: Record<string, unknown> = {}\n  const snapshot: Record<string, unknown> = {}\n\n  for (const [key, output] of outputs) {\n    const merged: Record<string, EncodableValue> = {}\n    for (const item of output.items) {\n      for (const [k, v] of Object.entries(item.json)) {\n        merged[k] = v\n      }\n    }\n\n    // The exact table key, addressable through the `outputs` wrapper.\n    byKey[key] = merged\n\n    // ALSO a bare node id, which is the form the dialect actually admits. A table key is\n    // `${nodeId}:${branchKey}` and JEXL's grammar cannot reach a bare identifier containing a\n    // colon by ANY syntax — `n1:.status` is a parse error, and an unwrapped `this[\"n1:\"]` throws\n    // because `this` is not bound. Measured against real jexl 2.3.0, not assumed. So a flat\n    // snapshot keyed by table key was addressable by nothing at all, and every predicate silently\n    // evaluated `undefined`.\n    //\n    // Where one node ran on ONE branch the bare id is unambiguous and is what an author writes.\n    // Where a node ran on several, the bare id is ambiguous, so it is deliberately NOT set to an\n    // arbitrary winner — the author uses `outputs['nodeId:branchKey']` to say which, exactly as\n    // the Lua cell requires.\n    const nodeId = key.slice(0, key.indexOf(':'))\n    if (nodeId !== '' && IDENTIFIER.test(nodeId)) {\n      snapshot[nodeId] = nodeId in snapshot ? AMBIGUOUS : merged\n    }\n  }\n\n  for (const [nodeId, value] of Object.entries(snapshot)) {\n    if (value === AMBIGUOUS) delete snapshot[nodeId]\n  }\n\n  // `outputs` is the escape hatch for an exact key, and the only way to disambiguate a node that\n  // ran on more than one branch. Set last so a node genuinely named `outputs` cannot shadow it.\n  snapshot.outputs = byKey\n  return snapshot\n}\n\n/** A bare identifier JEXL can actually address, so a key it cannot reach is never advertised. */\nconst IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/\n\n/** Marks a node id reached on more than one branch, so it is dropped rather than guessed. */\nconst AMBIGUOUS = Symbol('ambiguous')\n\n/**\n * Create the `jexl` predicate cell.\n *\n * The cell treats a branch/select node's `predicate` as a JEXL expression source string. `load`\n * lazily resolves the optional `jexl` peer and registers the closed transform allowlist; `load`\n * and `validate` run the dialect lint and a parse attempt so a syntax error surfaces at freeze\n * rather than mid-run; `evaluate` synchronously evaluates the source against a marshalled\n * snapshot of the outputs.\n *\n * @param options - optional cell configuration (the closed transform allowlist).\n * @returns a {@link PredicateEvaluator} whose `id` is `'jexl'`.\n */\nexport const createJexlCell = (options?: JexlCellOptions): PredicateEvaluator => {\n  // The shared engine, instantiated exactly once by load and reused for parse and evaluation so\n  // the transform allowlist stays closed across the cell's lifetime.\n  let engine: JexlEngine | undefined\n\n  // Idempotent lazy load: resolves 'jexl' once and maps a missing peer to\n  // E_ORCH_CELL_UNAVAILABLE. The engine is created here so addTransform — the security\n  // boundary — runs exactly once.\n  const load = loadOnce('jexl', async () => {\n    const mod = (await import('jexl')) as JexlModule\n    const instance = new mod.Jexl()\n    const transforms = options?.transforms\n    if (transforms) {\n      for (const [name, fn] of Object.entries(transforms)) {\n        instance.addTransform(name, fn)\n      }\n    }\n    engine = instance\n  })\n\n  return {\n    id: 'jexl',\n\n    /**\n     * Resolve the optional `jexl` peer and close the transform allowlist.\n     *\n     * Idempotent; a subsequent call resolves immediately with the same outcome. A missing\n     * peer becomes {@link './../exceptions'.E_ORCH_CELL_UNAVAILABLE} naming the install command.\n     */\n    async load(): Promise<void> {\n      await load()\n    },\n\n    /**\n     * Validate that the node's predicate is well-formed JEXL.\n     *\n     * First the predicate must be a source string (this cell reads strings, not the structured\n     * tree), then the dialect lint rejects two model mistakes BY NAME rather than leaving them\n     * to the parser, then a parse attempt confirms the grammar compiles. Each rejection names\n     * the specific correction.\n     *\n     * @param node - the plan node to validate.\n     */\n    async validate(node: PlanNode): Promise<void> {\n      await load()\n      const definition = node.definition as { predicate?: unknown }\n      const source = definition?.predicate\n\n      // This cell's contract: the predicate is a JEXL expression SOURCE STRING, not the\n      // structured tree the structured cell reads.\n      if (typeof source !== 'string') {\n        throw new Error(\n          `jexl cell: node '${node.id}' must carry a JEXL expression SOURCE STRING as its ` +\n            `predicate, but got ${describeValue(source)}. Write the predicate as a string, ` +\n            `e.g. \"order.total > 100\", or use the 'structured' cell for the declarative tree.`\n        )\n      }\n\n      // Dialect lint, part of per-cell validate because this cell owns the dialect.\n      if (source.includes('===')) {\n        throw new Error(\n          `jexl cell: node '${node.id}' uses '===' which JEXL does not support. ` +\n            `JEXL's equality operator is '=='. Replace '===' with '=='.`\n        )\n      }\n      if (/\\bctx\\s*\\./.test(source)) {\n        throw new Error(\n          `jexl cell: node '${node.id}' prefixes an identifier with 'ctx.'. JEXL reads bare ` +\n            `identifiers against the readable context — there is no 'ctx' object. ` +\n            `Reference the value directly, e.g. 'order.total == 100'.`\n        )\n      }\n\n      // Parse attempt: fail at freeze on a syntax error rather than mid-run. createExpression only\n      // BUILDS the expression object; the actual parse happens on .compile(). So invoke compile()\n      // to force the parse. The expression is never evaluated, so no identifier is resolved here.\n      try {\n        if (!engine) throw new Error('jexl engine not loaded')\n        engine.createExpression(source).compile()\n      } catch (err) {\n        const hint = isError(err) ? err.message : String(err)\n        throw new Error(`jexl cell: node '${node.id}' predicate is not valid JEXL: ${hint}`)\n      }\n\n      // Transform allowlist check. The pipe is the cell's security boundary, but jexl resolves a\n      // transform NAME at EVALUATION, not at parse: compile('name|evil') succeeds and only\n      // evalSync throws 'Transform evil is not defined'. Deferring that refusal to run time would\n      // let an approved plan reach an unregistered transform for the first time AFTER side effects\n      // have already landed. So extract the transform names at parse level and refuse any that is\n      // not in the registered allowlist, naming the unknown transform AND the registered set so an\n      // authoring model can correct it in one pass. Nothing is evaluated here.\n      const registered = options?.transforms ?? {}\n      const registeredNames = Object.keys(registered)\n      const transformUse = /\\|\\s*([a-zA-Z_][a-zA-Z0-9_]*)/g\n      let transformMatch: RegExpExecArray | null\n      while ((transformMatch = transformUse.exec(source)) !== null) {\n        const transformName = transformMatch[1]\n        if (!registeredNames.includes(transformName)) {\n          const listed = registeredNames.length ? `: ${registeredNames.join(', ')}` : ' is empty'\n          throw new Error(\n            `jexl cell: node '${node.id}' predicate calls transform '${transformName}', which is ` +\n              `not in this cell's registered allowlist${listed}. Register '${transformName}' via ` +\n              `createJexlCell({ transforms }) or change the predicate to use a registered transform.`\n          )\n        }\n      }\n    },\n\n    /**\n     * Evaluate the node's predicate against the run's outputs.\n     *\n     * Builds a plain-data snapshot, then synchronously evaluates the predicate source string.\n     * For a `branch` node the result is truthiness → `{kind:'branch', matched}`. For a `select`\n     * node the evaluated value is compared to each case label in declared order and the first\n     * equal label is returned; `null` routes to the `default` handle.\n     *\n     * @param node - the plan node to evaluate.\n     * @param ctx - the context carrying the run's outputs.\n     * @returns the branch or select verdict.\n     */\n    async evaluate(node: PlanNode, ctx: PredicateContext): Promise<PredicateVerdict> {\n      await load()\n      const definition = node.definition as { predicate?: unknown; cases?: string[] }\n      const source = definition?.predicate\n      if (typeof source !== 'string' || !engine) {\n        return { kind: 'branch', matched: false }\n      }\n\n      const snapshot = snapshotOf(ctx.outputs)\n\n      // A PREDICATE IS NEVER ALLOWED TO CRASH THE RUN. jexl's `evalSync` throws on a runtime\n      // fault the parse could not have caught — reaching into an undefined intermediate\n      // (`missing.deep.thing`) is the ordinary case, and it is exactly what an author writing\n      // against a branch that has not produced output yet will hit. Unguarded, that TypeError\n      // propagates out of `evaluate`, is caught by the executor as a node failure, and halts the\n      // whole run on a plan the operator approved.\n      //\n      // The safe verdict is the negative one: `no_match` for a branch and the `default` handle\n      // for a select, which is what the Lua cell already documents for an equivalent fault. A\n      // predicate that cannot be answered has not selected anything.\n      let value: unknown\n      try {\n        value = engine.evalSync(source, snapshot)\n      } catch {\n        return node.kind === 'select'\n          ? { kind: 'select', caseLabel: null }\n          : { kind: 'branch', matched: false }\n      }\n\n      if (node.kind === 'select') {\n        const cases = definition.cases ?? []\n        for (const label of cases) {\n          if (String(value) === String(label)) {\n            return { kind: 'select', caseLabel: label }\n          }\n        }\n        return { kind: 'select', caseLabel: null }\n      }\n\n      return { kind: 'branch', matched: Boolean(value) }\n    },\n  }\n}\n"],"mappings":"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"}