{"version":3,"file":"structured.cjs","names":[],"sources":["../../../../src/batteries/orchestration/cells/structured.ts"],"sourcesContent":["/**\n * @module @nhtio/adk/batteries/orchestration/cells/structured\n *\n * The `structured` predicate cell.\n *\n * This cell evaluates a declarative, JSON-shaped predicate tree against a\n * bounded, already-materialised plain-data snapshot of a run's outputs. It is\n * the default predicate evaluator for most plans because it has no external\n * dependencies: there is no parser to load and no runtime to initialise, so it\n * can never fail for a missing peer.\n *\n * Two properties hold by construction and are worth keeping in mind when\n * reading this cell:\n *\n * 1. It reads a plain-data snapshot rather than live objects. The predicate\n *    only ever touches values reached through `readPath` on the marshalled\n *    outputs, so it never invokes reachable methods or getters and cannot be\n *    surprised by object identity or prototype chains.\n * 2. It uses no clock and no randomness. A branch or select node is therefore\n *    safe to re-enter unconditionally when a run resumes: re-evaluating the\n *    same predicate against the same snapshot always yields the same verdict.\n */\n\nimport { readPath } from '../plan'\nimport { isObject, isInstanceOf } from '../../../lib/utils/guards'\nimport {\n  parseStructuredPredicate,\n  isPredicateLeaf,\n  isAllPredicate,\n  isAnyPredicate,\n  isNotPredicate,\n  type StructuredPredicate,\n} from '../predicates'\nimport type { PredicateEvaluator, PredicateContext, PredicateVerdict, PlanNode } from './../types'\n\n/**\n * Evaluate a structured predicate against a plain-data snapshot.\n *\n * Recurses over the predicate tree:\n * - `all` is true when every member is true.\n * - `any` is true when at least one member is true.\n * - `not` is the negation of its member.\n * - a leaf reads `leaf.path` from the snapshot and applies `leaf.op`.\n *\n * @param predicate - the structured predicate to evaluate.\n * @param snapshot - the plain-data snapshot to read leaf paths from.\n * @returns whether the predicate holds for the snapshot.\n */\nconst evaluatePredicate = (predicate: StructuredPredicate, snapshot: unknown): boolean => {\n  if (isPredicateLeaf(predicate)) {\n    return evaluateLeaf(predicate, snapshot)\n  }\n  if (isAllPredicate(predicate)) {\n    return predicate.all.every((member) => evaluatePredicate(member, snapshot))\n  }\n  if (isAnyPredicate(predicate)) {\n    return predicate.any.some((member) => evaluatePredicate(member, snapshot))\n  }\n  if (isNotPredicate(predicate)) {\n    return !evaluatePredicate(predicate.not, snapshot)\n  }\n  return false\n}\n\n/**\n * Apply a single leaf predicate to the snapshot.\n *\n * The value at `leaf.path` is read with `readPath`, which already rejects\n * prototype-polluting segments and returns `undefined` for a missing path.\n * Comparison semantics are deliberately total: mismatched or non-comparable\n * types yield `false` rather than throwing.\n *\n * @param leaf - the leaf predicate to apply.\n * @param snapshot - the plain-data snapshot to read from.\n * @returns whether the leaf holds for the snapshot.\n */\nconst evaluateLeaf = (\n  leaf: { path: string; op: string; value?: unknown },\n  snapshot: unknown\n): boolean => {\n  const actual = readPath(snapshot, leaf.path)\n  const expected = leaf.value\n\n  switch (leaf.op) {\n    case 'eq':\n      return equals(actual, expected)\n    case 'ne':\n      return !equals(actual, expected)\n    case 'lt':\n      return compare(actual, expected) < 0\n    case 'lte':\n      return compare(actual, expected) <= 0\n    case 'gt':\n      return compare(actual, expected) > 0\n    case 'gte':\n      return compare(actual, expected) >= 0\n    case 'in':\n      return Array.isArray(expected) && expected.some((item) => equals(actual, item))\n    case 'contains':\n      return contains(actual, expected)\n    case 'truthy':\n      return Boolean(actual)\n    case 'exists':\n      return actual !== undefined\n    default:\n      return false\n  }\n}\n\n/**\n * Strict equality that treats two `Date` instances as equal when they share a\n * time value. Mismatched types are simply not equal and never throw.\n *\n * @param a - the left operand.\n * @param b - the right operand.\n * @returns whether `a` and `b` are equal.\n */\nconst equals = (a: unknown, b: unknown): boolean => {\n  if (isInstanceOf(a, 'Date', Date) && isInstanceOf(b, 'Date', Date)) {\n    return a.getTime() === b.getTime()\n  }\n  return a === b\n}\n\n/**\n * Order two comparable values. Only two numbers, two `Date` instances, or two\n * strings are comparable; any other combination is treated as incomparable and\n * yields a non-zero result so the ordering operators report `false`.\n *\n * @param a - the left operand.\n * @param b - the right operand.\n * @returns a negative, zero, or positive number, or `NaN` when incomparable.\n */\nconst compare = (a: unknown, b: unknown): number => {\n  if (typeof a === 'number' && typeof b === 'number') {\n    return a - b\n  }\n  if (typeof a === 'string' && typeof b === 'string') {\n    return a < b ? -1 : a > b ? 1 : 0\n  }\n  if (isInstanceOf(a, 'Date', Date) && isInstanceOf(b, 'Date', Date)) {\n    return a.getTime() - b.getTime()\n  }\n  return Number.NaN\n}\n\n/**\n * Membership test for `contains`: a string containing the expected value, or an\n * array including it. Any other combination is `false`.\n *\n * @param actual - the value read from the snapshot.\n * @param expected - the value to look for.\n * @returns whether `actual` contains `expected`.\n */\nconst contains = (actual: unknown, expected: unknown): boolean => {\n  if (typeof actual === 'string' && typeof expected === 'string') {\n    return actual.includes(expected)\n  }\n  if (Array.isArray(actual)) {\n    return actual.some((item) => equals(item, expected))\n  }\n  return false\n}\n\n/**\n * Build the plain-data snapshot a predicate reads.\n *\n * @remarks\n * `ctx.outputs` is an `OutputTable` — a `ReadonlyMap`. `readPath` walks with property access, so\n * handing it the Map directly reads NOTHING: every path misses and every leaf evaluates false, so\n * a branch silently took `no_match` on a predicate that should have matched. Marshalling is also\n * the cross-cutting rule for every cell — a predicate reads a bounded, already-materialised\n * snapshot, never live objects with reachable methods — and the jexl cell already did this.\n *\n * Each table key maps to the merged `json` of that output's items, so a predicate addresses\n * `` `${nodeId}:${branchKey}`.field ``.\n *\n * @param outputs - The frame's branch-local output table.\n * @returns A plain record keyed identically to the table.\n */\nconst snapshotOf = (outputs: PredicateContext['outputs']): Record<string, unknown> => {\n  const snapshot: Record<string, unknown> = {}\n  for (const [key, output] of outputs) {\n    const merged: Record<string, unknown> = {}\n    for (const item of output.items) {\n      for (const [k, v] of Object.entries(item.json)) merged[k] = v\n    }\n    snapshot[key] = merged\n  }\n  return snapshot\n}\n\n/**\n * The `structured` predicate evaluator.\n *\n * Zero-dependency by design: `load` is an idempotent no-op, `validate` checks\n * that the node's predicate parses, and `evaluate` produces a branch or select\n * verdict from the parsed predicate tree.\n */\nexport const createStructuredCell = (): PredicateEvaluator => ({\n  id: 'structured',\n\n  /**\n   * No-op initialisation. This cell has no parser or runtime to load, so it is\n   * always ready and can be called any number of times.\n   */\n  async load(): Promise<void> {\n    // Intentionally empty: nothing to initialise.\n  },\n\n  /**\n   * Validate that the node's predicate is a well-formed structured predicate.\n   *\n   * @param node - the plan node to validate.\n   * @throws when the predicate fails to parse, carrying the parser's reason.\n   */\n  async validate(node: PlanNode): Promise<void> {\n    const definition = node.definition as { predicate?: unknown; cases?: string[] }\n\n    if (node.kind === 'select') {\n      // A `select`'s predicate is a record of case label -> structured predicate. Refusing here,\n      // at freeze, is what stops a plan reaching the approval gate with cases that can never fire.\n      const cases = definition.cases ?? []\n      if (!isObject(definition.predicate)) {\n        throw new Error(\n          `Select node \"${node.id}\" needs its \"predicate\" to be an object mapping each case label ` +\n            `to a structured predicate (for example {\"${cases[0] ?? 'label'}\": {\"path\": \"...\", \"op\": \"eq\", \"value\": \"...\"}}).`\n        )\n      }\n      const byCase = definition.predicate as Record<string, unknown>\n      for (const label of cases) {\n        if (!(label in byCase)) {\n          throw new Error(\n            `Select node \"${node.id}\" declares case \"${label}\" but its \"predicate\" object has no ` +\n              `entry for it; add one, or remove the case.`\n          )\n        }\n        const parsed = parseStructuredPredicate(byCase[label])\n        if (!parsed.ok) {\n          throw new Error(`Select node \"${node.id}\", case \"${label}\": ${parsed.reason}`)\n        }\n      }\n      return\n    }\n\n    const parsed = parseStructuredPredicate(definition?.predicate)\n    if (!parsed.ok) {\n      throw new Error(parsed.reason)\n    }\n  },\n\n  /**\n   * Evaluate the node's predicate against the run's outputs.\n   *\n   * For a `branch` node the verdict reports whether the predicate matched. For\n   * a `select` node the predicate is evaluated against each case label in\n   * declared order and the first match is returned; `null` routes to the\n   * `default` handle.\n   *\n   * @param node - the plan node to evaluate.\n   * @param ctx - the evaluation context carrying the outputs snapshot.\n   * @returns the branch or select verdict.\n   */\n  async evaluate(node: PlanNode, ctx: PredicateContext): Promise<PredicateVerdict> {\n    const definition = node.definition as {\n      predicate?: unknown\n      cases?: string[]\n    }\n    const snapshot = snapshotOf(ctx.outputs)\n\n    if (node.kind === 'select') {\n      // A `select` needs a predicate PER CASE, so the node's `predicate` is a record mapping each\n      // declared case label to its own structured predicate. The first label whose predicate holds\n      // wins, in the order `cases` declares — so the author controls precedence, and overlapping\n      // predicates resolve deterministically rather than by object key order.\n      //\n      // A single predicate cannot express an n-way choice: it answers true or false. The other\n      // cells solve this by having the predicate RETURN a label (jexl compares the evaluated value\n      // to each case; Lua likewise), but the structured IR is a closed boolean tree with no way to\n      // yield a string — so the mapping is declared instead of computed. An earlier version looped\n      // the case labels while re-evaluating ONE boolean, which returned the first label whenever\n      // that predicate was true and `null` otherwise: the second and later cases were unreachable.\n      const cases = definition.cases ?? []\n      const byCase = definition.predicate\n      if (!isObject(byCase)) return { kind: 'select', caseLabel: null }\n      for (const label of cases) {\n        const parsed = parseStructuredPredicate((byCase as Record<string, unknown>)[label])\n        if (!parsed.ok) continue\n        if (evaluatePredicate(parsed.predicate, snapshot)) {\n          return { kind: 'select', caseLabel: label }\n        }\n      }\n      // Nothing matched: the `default` handle. Also the total answer for a malformed predicate —\n      // a predicate is never allowed to crash a run.\n      return { kind: 'select', caseLabel: null }\n    }\n\n    const parsed = parseStructuredPredicate(definition?.predicate)\n    if (!parsed.ok) return { kind: 'branch', matched: false }\n    return { kind: 'branch', matched: evaluatePredicate(parsed.predicate, snapshot) }\n  },\n})\n"],"mappings":"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"}