/** * The op-log fold and the branch-key renderer for orchestration. * * @module @nhtio/adk/batteries/orchestration/ops * * @remarks * This module owns two things: * * 1. `foldOps` — the DETERMINISTIC fold that turns a plan's op log into a `RawPlanView`. The plan * IS the fold of its op log: two actors folding the same op set must reach the same state, and * ops may arrive out of order or twice. The fold never throws on a malformed-but-well-typed * log; it surfaces `PlanIssue`s instead. * 2. `branchKey` — the canonical, INJECTIVE string form of a `BranchId` route, used to key * `OutputTable`, identify `NodeRef.branchId`, order join contributors, and make duplicate * arrivals idempotent. */ import type { PlanOp, RawPlanView, PlanIssue, BranchId, PlanProvenance } from "./types"; /** * Fold an op log into a `RawPlanView`, deterministically and convergently. * * @remarks * The plan is the fold of its op log. Two actors folding the same op set must reach the same * state, and ops may arrive out of order or twice. The fold is therefore a pure function of the * op SET: it sorts by the three-part key `(lamport, actorId, opId)` — see {@link byKey} for why * all three parts are required — and applies the ops in that total order. Because the highest-key * op touching any element is applied last, the result is LWW (last-writer-wins) and identical for * every arrival order. * * **Ops are strictly read-only input.** The fold never mutates the caller's op objects: on * `add_node` and `set_node_definition` it copies the plain-object/array SPINE of the node or * definition (carrying every encoder-owned value — `Date`, `RegExp`, `Map`, `Set`, typed arrays, * bigint, `NodeRef`/`ParamRef` instances — across by reference), and `set_node_field`/ * `set_node_phase` write only onto those copies. So a `PlanStore` can serve historical views from * the same op log without a read-only projection silently altering it. * * **Bounds are the fold SEED, not an op.** The fold starts from `DEFAULT_PLAN_BOUNDS`, so an empty * log folds to revision 0 with a complete view and a stable digest, and the first authoring op * makes revision 1. `revision` is the number of ops folded. `set_bounds` ops override the seed by * LWW thereafter. * * **Element semantics.** `add_node`/`remove_node` and `add_edge`/`remove_edge` are LWW-ELEMENT, * not add-wins: the highest-key op touching an element decides whether it exists. Add-wins is * deliberately NOT attempted — it needs causal context a scalar lamport cannot provide. A * `remove_node` also records its `incidentEdgeIds`, so removal cascades to those edges * order-independently. `set_node_field` is LWW per field on the same three-part key (its value may * contain a `NodeRef`, so it accepts `ArgValue`); `set_node_definition` replaces a whole * definition by LWW; `set_node_phase` sets a phase, with `null` clearing it; `set_bounds` overrides * the seed by LWW. * * **The fold surfaces issues rather than throwing.** It never throws on a malformed-but-well-typed * log: * - An edge whose `from` or `to` node does not exist after folding is DROPPED and surfaced as a * `dangling_edge` issue — a dangling edge is never what anyone wanted. * - Two `add_edge` ops with the SAME id but different endpoints: LWW decides, the loser is dropped, * and the issue names BOTH so the author renames one. The second is not refused at append time — * that would make the fold order-dependent, and two offline writers can each legally append * before their logs meet. * - An op referencing an unknown nodeId is surfaced as an `unknown_node` issue, not thrown. * * The `digest` is computed via `planDigest(view)` with the digest field itself empty when hashing, * so it cannot depend on itself. * * @param planId - The plan's id, carried into the view. * @param ops - The op log to fold. May be empty, out of order, or contain duplicates. * @param provenance - Optional lineage (clone/template), carried into the view and covered by the * digest. * @returns The folded view and any issues the fold surfaced. */ export declare const foldOps: (planId: string, ops: readonly PlanOp[], provenance?: PlanProvenance) => { view: RawPlanView; issues: PlanIssue[]; }; /** * The canonical, INJECTIVE string form of a `BranchId` route. * * @remarks * `branchKey` keys `OutputTable`, identifies `NodeRef.branchId`, orders join contributors, and * makes duplicate arrivals idempotent — so a collision would overwrite one node's output with * another's or merge unrelated barriers. It MUST therefore be injective. * * Naive delimiter-joining is NOT injective: an edge id containing the delimiter (`a>b`) collides * with two segments (`a`, `b`), and an id shaped like `join:x(y)` collides with a join segment. So * the rendering is LENGTH-PREFIXED, concatenated with no separator: * - an edge segment renders as `` `e${id.length}:${id}` ``; * - a join segment renders as `` `j${nodeId.length}:${nodeId}(${of.map(len-prefixed).join('')})` ``. * * Length-prefixing is injective regardless of content: a parser reads the length, then exactly * that many characters for the id, so no delimiter can be forged and no two distinct routes render * to the same string. The `e`/`j` prefixes keep edge and join segments disjoint, and the join's * `of` ids are themselves length-prefixed so the closing `)` is unambiguous. * * @param b - The route to render. * @returns The length-prefixed, separator-free canonical string. */ export declare const branchKey: (b: BranchId) => string;