---
url: >-
  https://adk.nht.io/api/@nhtio/adk/batteries/orchestration/ops/functions/foldOps.md
---

# Function: foldOps()

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

Defined in: [src/batteries/orchestration/ops.ts:165](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/ops.ts#L165)

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

## Parameters

| Parameter     | Type                                                           | Description                                                                         |
| ------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `planId`      | `string`                                                       | The plan's id, carried into the view.                                               |
| `ops`         | readonly [`PlanOp`](../../types/type-aliases/PlanOp.md)\[]      | The op log to fold. May be empty, out of order, or contain duplicates.              |
| `provenance?` | [`PlanProvenance`](../../types/type-aliases/PlanProvenance.md) | Optional 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.

| Name     | Type                                                   | Defined in                                                                                                                            |
| -------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `issues` | [`PlanIssue`](../../types/interfaces/PlanIssue.md)\[]   | [src/batteries/orchestration/ops.ts:169](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/ops.ts#L169) |
| `view`   | [`RawPlanView`](../../types/interfaces/RawPlanView.md) | [src/batteries/orchestration/ops.ts:169](https://github.com/NHTIO/ADK/blob/v1.20260906.0/src/src/batteries/orchestration/ops.ts#L169) |

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