Skip to content
3 min read · 561 words

Function: foldOps()

ts
function foldOps(
  planId: string,
  ops: readonly PlanOp[],
  provenance?: PlanProvenance,
): {
  issues: PlanIssue[];
  view: RawPlanView;
};

Defined in: src/batteries/orchestration/ops.ts:165

Fold an op log into a RawPlanView, deterministically and convergently.

Parameters

ParameterTypeDescription
planIdstringThe plan's id, carried into the view.
opsreadonly PlanOp[]The op log to fold. May be empty, out of order, or contain duplicates.
provenance?PlanProvenanceOptional lineage (clone/template), carried into the view and covered by the digest.

Returns

ts
{
  issues: PlanIssue[];
  view: RawPlanView;
}

The folded view and any issues the fold surfaced.

NameTypeDefined in
issuesPlanIssue[]src/batteries/orchestration/ops.ts:169
viewRawPlanViewsrc/batteries/orchestration/ops.ts:169

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 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.