{"version":3,"file":"encoding.cjs","names":[],"sources":["../../../src/batteries/orchestration/encoding.ts"],"sourcesContent":["/**\n * Encoding battery for orchestration: the reference classes the IR is built from, their\n * `@nhtio/encoder` registration, and the plan digest every approval binds to.\n *\n * @module @nhtio/adk/batteries/orchestration/encoding\n *\n * @remarks\n * This module owns three things:\n *\n * 1. `NodeRef` and `ParamRef` — the two CLASSES the IR uses for references and template holes.\n *    They are classes, not records with a marker field, for a reason stated in their TSDoc: a\n *    plain record can wear `{kind: 'nodeRef', …}` — it is an ordinary encodable record — so a\n *    marker property cannot separate a reference from a literal that merely looks like one, and a\n *    resolver keying on it would silently rewrite the literal. `instanceof` is unforgeable.\n * 2. `registerOrchestrationEncodables()` — the one call that makes `decode()` able to rebuild\n *    these classes. It MUST run before any `decode()`.\n * 3. `planDigest()` — the canonical, LOSSLESS digest of a `RawPlanView`. Every approval binds to\n *    this digest, so it must never collide across semantically-different plans.\n */\n\nimport { sha256 } from 'js-sha256'\nimport { isInstanceOf } from '@nhtio/adk/guards'\nimport { ENCODE_METHOD, DECODE_METHOD, encode, registerClass } from '@nhtio/encoder'\nimport type { Encodable } from '@nhtio/encoder'\nimport type { RawPlanView, NodeId, BranchId } from './types'\n\n// ── NodeRef ───────────────────────────────────────────────────────────────────\n/**\n * A serializable reference to another node's output — a CLASS, not a record.\n *\n * @remarks\n * Why a class rather than a record with a marker field: a plain record can wear\n * `{kind: 'nodeRef', node, select, …}` — it is an ordinary encodable record — so a marker\n * property cannot separate a reference from a literal that happens to look like one, and a\n * resolver keying on it would silently rewrite the literal. `instanceof` is unforgeable: no\n * record can satisfy `NodeRef.isNodeRef`, and the encoder round-trips instances as\n * `custom:NodeRef` rather than as records. Same mechanism core uses for `Media`/`Tokenizable`.\n *\n * `branchId` identifies WHICH EXECUTION of `node` to read — the same path identity `NodeOutput`\n * and `FrameRef` carry. Omitted means \"do not filter\".\n */\nexport class NodeRef {\n  /** The id of the node whose output this reference reads. */\n  node: NodeId\n  /** Which item of that node's output to take: 'first', 'last', 'all', or an explicit {index}. */\n  select: 'first' | 'last' | 'all' | { index: number }\n  /** An optional dot-path into the selected item's value. */\n  path?: string\n  /**\n   * WHICH EXECUTION of the node to read, as a path identity.\n   *\n   * @remarks\n   * This is NOT an outgoing branch of that node: a node fanning out to two successors still runs\n   * once and produces one output; what creates several outputs for one node is that node being\n   * REACHED by several paths. Omitted means \"do not filter\" — fine when exactly one path reaches\n   * the node, and refused at freeze when more than one does.\n   */\n  branchId?: BranchId\n\n  /**\n   * Construct a `NodeRef`.\n   *\n   * @param node - The id of the node whose output this reference reads.\n   * @param select - Which item of that node's output to take: 'first', 'last', 'all', or an explicit {index}.\n   * @param path - An optional dot-path into the selected item's value.\n   * @param branchId - Which execution of the node to read, as a path identity; omitted means \"do not filter\".\n   */\n  constructor(\n    node: NodeId,\n    select: 'first' | 'last' | 'all' | { index: number },\n    path?: string,\n    branchId?: BranchId\n  ) {\n    this.node = node\n    this.select = select\n    this.path = path\n    this.branchId = branchId\n  }\n\n  /** `instanceof` guard — a look-alike record cannot pass. */\n  static isNodeRef(v: unknown): v is NodeRef {\n    return isInstanceOf(v, 'NodeRef', NodeRef)\n  }\n\n  /** Emit a plain snapshot of the fields for the encoder. */\n  [ENCODE_METHOD](): Record<string, unknown> {\n    return { node: this.node, select: this.select, path: this.path, branchId: this.branchId }\n  }\n\n  /** Rebuild an instance from a {@link NodeRef.[ENCODE_METHOD]} snapshot. */\n  static [DECODE_METHOD](data: unknown): NodeRef {\n    const s = data as {\n      node: NodeId\n      select: NodeRef['select']\n      path?: string\n      branchId?: BranchId\n    }\n    return new NodeRef(s.node, s.select, s.path, s.branchId)\n  }\n}\n\n// ── ParamRef ─────────────────────────────────────────────────────────────────\n/**\n * A hole in a template's staged values, substituted at instantiation. A CLASS for the same\n * reason `NodeRef` is: a look-alike record must not be mistaken for a hole.\n */\nexport class ParamRef {\n  /** Must name a declared template `params` entry (checked at construction). */\n  path: string\n\n  /**\n   * Construct a `ParamRef`.\n   *\n   * @param path - The declared template `params` entry this hole names.\n   */\n  constructor(path: string) {\n    this.path = path\n  }\n\n  /** `instanceof` guard — a look-alike record cannot pass. */\n  static isParamRef(v: unknown): v is ParamRef {\n    return isInstanceOf(v, 'ParamRef', ParamRef)\n  }\n\n  /** Emit a plain snapshot of the fields for the encoder. */\n  [ENCODE_METHOD](): Record<string, unknown> {\n    return { path: this.path }\n  }\n\n  /** Rebuild an instance from a {@link ParamRef.[ENCODE_METHOD]} snapshot. */\n  static [DECODE_METHOD](data: unknown): ParamRef {\n    return new ParamRef((data as { path: string }).path)\n  }\n}\n\n// ── registration ──────────────────────────────────────────────────────────────\nlet registered = false\n\n/**\n * Register `NodeRef` and `ParamRef` with the `@nhtio/encoder` decoder.\n *\n * @remarks\n * **MUST run before any `decode()` — but {@link createOrchestration} already calls it**, so a\n * consumer who constructs through the battery's entry point never needs to. Call it directly only\n * when you reach the deep subpaths without constructing an `Orchestration` (decoding a persisted\n * plan in a worker, say).\n *\n * **MUST run before any `decode()`.** `registerClass` writes to a GLOBAL registry, and decoding\n * an unregistered class throws — the decoder must map a `custom:NodeRef`/`custom:ParamRef` wire\n * tag back to a constructor. Idempotent: safe to call more than once (re-registering a class is a\n * no-op overwrite). Encoding never needs this; only decoding does.\n */\nexport const registerOrchestrationEncodables = (): void => {\n  if (registered) return\n  registered = true\n  registerClass(NodeRef)\n  registerClass(ParamRef)\n}\n\n// ── plan digest ──────────────────────────────────────────────────────────────\n/**\n * True for a PLAIN object — one whose prototype is `Object.prototype` or `null`. Every\n * encoder-owned value (`Date`, `RegExp`, `Map`, `Set`, typed arrays, `ArrayBuffer`, `DataView`,\n * bigint, luxon values, `NodeRef`/`ParamRef` instances) has a non-plain prototype, so this is\n * exactly the set whose keys we are allowed to sort.\n */\nconst isPlainObject = (v: unknown): v is Record<string, unknown> => {\n  if (v === null || typeof v !== 'object') return false\n  const proto = Object.getPrototypeOf(v)\n  return proto === Object.prototype || proto === null\n}\n\n/**\n * Recursively sort PLAIN-OBJECT keys only, leaving every encoder-owned value untouched.\n *\n * @remarks\n * This is the deliberate alternative to `canonicalStringify` from `src/lib/utils/canonical_json.ts`.\n * That helper walks objects with `Object.keys`, and `Date`, `RegExp`, `Map` and `Set` have no\n * enumerable own keys, so it collapses each to `{}` — a proven collision (see the module TSDoc).\n * Here we sort only plain-object keys and hand every other value to the encoder UNCHANGED, so\n * `encode` serialises `Date`/`RegExp`/`Map`/`Set`/typed arrays/`ArrayBuffer`/`DataView`/bigint/\n * luxon/`NodeRef`/`ParamRef` faithfully. Sorting must NEVER replace the encoder's representation\n * of those values — that is exactly the mistake `canonicalStringify` makes. Arrays keep their\n * order (order is meaningful).\n */\nconst sortPlainObjectKeys = (value: unknown): unknown => {\n  if (Array.isArray(value)) {\n    return value.map(sortPlainObjectKeys)\n  }\n  if (isPlainObject(value)) {\n    const out: Record<string, unknown> = {}\n    for (const key of Object.keys(value).sort()) {\n      out[key] = sortPlainObjectKeys(value[key])\n    }\n    return out\n  }\n  // Encoder-owned value (Date, RegExp, Map, Set, typed array, ArrayBuffer, DataView, bigint,\n  // luxon, NodeRef, ParamRef, …) — leave untouched so `encode` serialises it faithfully.\n  return value\n}\n\n/**\n * Compute the canonical, LOSSLESS digest of a `RawPlanView`.\n *\n * @remarks\n * Every approval binds to this digest, so it must be a faithful fingerprint of the plan content\n * the operator actually saw — never a lossy one that could authorise a plan they did not see.\n *\n * The strategy: recursively sort PLAIN-OBJECT keys only (leaving every encoder-owned value —\n * `Date`, `RegExp`, `Map`, `Set`, typed arrays, `ArrayBuffer`, `DataView`, bigint, luxon values,\n * and `NodeRef`/`ParamRef` instances — untouched), then `sha256(encode(sortedSkeleton))`. The\n * encoder is the authority on how each value serialises, and it round-trips the reference classes\n * and the whole `EncodableValue` domain losslessly, so the digest is stable across key order while\n * remaining collision-free across semantically-different plans.\n *\n * This is the second of the two digest strategies considered. The first — `canonicalStringify`\n * from `src/lib/utils/canonical_json.ts` — was rejected because it walks objects with\n * `Object.keys`, and `Date`, `RegExp`, `Map` and `Set` have no enumerable own keys, so it collapses\n * each to `{}`. That is proven to collide: `{pattern: /^inv-\\d+$/i, when: <date A>, m: Map{k=>1}}`\n * and `{pattern: /^cust-\\d+$/, when: <date B>, m: Map{z=>9}}` both canonicalise to\n * `{\"m\":{},\"pattern\":{},\"when\":{}}`. An approval bound to that digest would authorise a plan the\n * operator never saw. This implementation sorts only plain-object keys and never replaces the\n * encoder's representation of the values it owns.\n *\n * @param view - The folded plan content at a revision.\n * @returns A hex sha256 digest over the canonical, lossless encoding of `view`.\n */\nexport const planDigest = (view: RawPlanView): string => {\n  const sorted = sortPlainObjectKeys(view)\n  return sha256(encode(sorted as Encodable))\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,IAAa,UAAb,MAAa,QAAQ;;CAEnB;;CAEA;;CAEA;;;;;;;;;;CAUA;;;;;;;;;CAUA,YACE,MACA,QACA,MACA,UACA;EACA,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,OAAO;EACZ,KAAK,WAAW;CAClB;;CAGA,OAAO,UAAU,GAA0B;EACzC,OAAO,eAAA,aAAa,GAAG,WAAW,OAAO;CAC3C;;CAGA,CAAC,eAAA,iBAA0C;EACzC,OAAO;GAAE,MAAM,KAAK;GAAM,QAAQ,KAAK;GAAQ,MAAM,KAAK;GAAM,UAAU,KAAK;EAAS;CAC1F;;CAGA,QAAQ,eAAA,eAAe,MAAwB;EAC7C,MAAM,IAAI;EAMV,OAAO,IAAI,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ;CACzD;AACF;;;;;AAOA,IAAa,WAAb,MAAa,SAAS;;CAEpB;;;;;;CAOA,YAAY,MAAc;EACxB,KAAK,OAAO;CACd;;CAGA,OAAO,WAAW,GAA2B;EAC3C,OAAO,eAAA,aAAa,GAAG,YAAY,QAAQ;CAC7C;;CAGA,CAAC,eAAA,iBAA0C;EACzC,OAAO,EAAE,MAAM,KAAK,KAAK;CAC3B;;CAGA,QAAQ,eAAA,eAAe,MAAyB;EAC9C,OAAO,IAAI,SAAU,KAA0B,IAAI;CACrD;AACF;AAGA,IAAI,aAAa;;;;;;;;;;;;;;;AAgBjB,IAAa,wCAA8C;CACzD,IAAI,YAAY;CAChB,aAAa;CACb,CAAA,GAAA,eAAA,eAAc,OAAO;CACrB,CAAA,GAAA,eAAA,eAAc,QAAQ;AACxB;;;;;;;AASA,IAAM,iBAAiB,MAA6C;CAClE,IAAI,MAAM,QAAQ,OAAO,MAAM,UAAU,OAAO;CAChD,MAAM,QAAQ,OAAO,eAAe,CAAC;CACrC,OAAO,UAAU,OAAO,aAAa,UAAU;AACjD;;;;;;;;;;;;;;AAeA,IAAM,uBAAuB,UAA4B;CACvD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,IAAI,mBAAmB;CAEtC,IAAI,cAAc,KAAK,GAAG;EACxB,MAAM,MAA+B,CAAC;EACtC,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE,KAAK,GACxC,IAAI,OAAO,oBAAoB,MAAM,IAAI;EAE3C,OAAO;CACT;CAGA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,cAAc,SAA8B;CAEvD,QAAA,GAAA,UAAA,SAAA,GAAA,eAAA,QADe,oBAAoB,IACd,CAAmB,CAAC;AAC3C"}