fyflow-orchestration 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -136,14 +136,14 @@ function sharedRegionDepth(regions, a, b) {
136
136
  }
137
137
  function outputIsVisible(regions, readerId, readerPath, targetId, targetPath) {
138
138
  const depth = sharedRegionDepth(regions, readerId, targetId);
139
- if (depth === 0) return true;
140
- const xs = readerPath === "-" ? [] : readerPath.split(".");
141
- const ys = targetPath === "-" ? [] : targetPath.split(".");
142
- for (let i = 0; i < depth; i++) {
143
- if (xs[i] === void 0 || ys[i] === void 0) return false;
144
- if (xs[i] !== ys[i]) return false;
145
- }
146
- return true;
139
+ const key = visibilityKey(readerPath, depth);
140
+ return key !== null && key === visibilityKey(targetPath, depth);
141
+ }
142
+ function visibilityKey(path, depth) {
143
+ if (depth === 0) return "";
144
+ const segs = path === "-" ? [] : path.split(".");
145
+ if (segs.length < depth) return null;
146
+ return segs.length === depth ? path : segs.slice(0, depth).join(".");
147
147
  }
148
148
  function branchIndexOf(regions, nodeId) {
149
149
  const r = innermostRegionOf(regions, nodeId);
@@ -2993,6 +2993,18 @@ function describeElement(e) {
2993
2993
  }
2994
2994
  return s.length > 160 ? `${s.slice(0, 157)}...` : s;
2995
2995
  }
2996
+ var NO_OUTPUTS = Object.freeze([]);
2997
+ function groupPosition(group, path) {
2998
+ let lo = 0;
2999
+ let hi = group.length;
3000
+ if (hi === 0 || compareBranchPaths(group[hi - 1].path, path) < 0) return hi;
3001
+ while (lo < hi) {
3002
+ const mid = lo + hi >>> 1;
3003
+ if (compareBranchPaths(group[mid].path, path) < 0) lo = mid + 1;
3004
+ else hi = mid;
3005
+ }
3006
+ return lo;
3007
+ }
2996
3008
  var UndispatchedTask = class extends Error {
2997
3009
  constructor(message, options) {
2998
3010
  super(message, options);
@@ -3089,6 +3101,49 @@ var defaultCreateTask = (config) => new FyflowTask(config);
3089
3101
  var SETTLEMENT_EVENTS = ["task.completed", "task.failed", "task.user_action", "task.cancelled"];
3090
3102
  var SUBSCRIBED_EVENTS = [...SETTLEMENT_EVENTS, "task.running"];
3091
3103
  var PASS_THROUGH = [["task.message", "onTaskMessage"], ["task.progress", "onTaskProgress"]];
3104
+ var NO_RECORDS = Object.freeze([]);
3105
+ var TaskLog = class {
3106
+ byTask = /* @__PURE__ */ new Map();
3107
+ visits = 0;
3108
+ /** `append`'s half: the record joins its task's sub-log, by reference. */
3109
+ push(record) {
3110
+ const sub = this.byTask.get(record.taskId);
3111
+ if (sub) sub.push(record);
3112
+ else this.byTask.set(record.taskId, [record]);
3113
+ }
3114
+ /** One task's records, in append order — what `effectiveOutcome` and `isSuspended` are handed. Read, never kept. */
3115
+ of(taskId) {
3116
+ const sub = this.byTask.get(taskId) ?? NO_RECORDS;
3117
+ this.visits += sub.length;
3118
+ return sub;
3119
+ }
3120
+ };
3121
+ var Fifo = class {
3122
+ items = [];
3123
+ head = 0;
3124
+ get length() {
3125
+ return this.items.length - this.head;
3126
+ }
3127
+ push(x) {
3128
+ this.items.push(x);
3129
+ }
3130
+ /** The oldest element, removed — `undefined` when empty, as `Array.prototype.shift`. */
3131
+ shift() {
3132
+ if (this.head >= this.items.length) return void 0;
3133
+ const x = this.items[this.head];
3134
+ this.items[this.head] = void 0;
3135
+ this.head += 1;
3136
+ if (this.head === this.items.length) {
3137
+ this.items = [];
3138
+ this.head = 0;
3139
+ }
3140
+ return x;
3141
+ }
3142
+ /** Every element still queued, oldest first. */
3143
+ *[Symbol.iterator]() {
3144
+ for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
3145
+ }
3146
+ };
3092
3147
  var Driver = class {
3093
3148
  /**
3094
3149
  * `OR-R7` — nothing outside the inbound API writes to run state. Private, and
@@ -3106,13 +3161,21 @@ var Driver = class {
3106
3161
  * channels with no shared order, so `RN-R3`'s *"read in append order"* was about
3107
3162
  * a value no one could hold.
3108
3163
  *
3109
- * `log` above is the **record-kind subsequence of this array**, kept as its own
3110
- * array only because `effectiveOutcome` and `isSuspended` scan it on every
3111
- * settlement and rebuilding it by filter each time is quadratic. It holds the
3112
- * same record objects, never copies. `test/driver/` asserts the two agree, so
3113
- * the redundancy is checked rather than trusted.
3164
+ * `log` above is the **record-kind subsequence of this array**, by reference — the
3165
+ * same record objects, never copies; `test/driver/` asserts the two agree, so the
3166
+ * redundancy is checked rather than trusted. *(It was kept "only because
3167
+ * `effectiveOutcome` and `isSuspended` scan it on every settlement" until W195,
3168
+ * whose {@link TaskLog} hands each of them one task's records instead — P39 fact 5.
3169
+ * It stays for what still reads it WHOLE and in order: `snapshot()`, `outputs()`'s
3170
+ * catch-up, and the retention walk's alias check.)*
3114
3171
  */
3115
3172
  inputs = [];
3173
+ /**
3174
+ * **Each task's records, by reference and in append order** — {@link TaskLog}, `RN-R13` v0.30, ADR 0121 (W195). Written in
3175
+ * `append` beside `log`; read by every per-task question the driver asks of its log — `advance`, `parked`, `hasParked`,
3176
+ * `inFlight`, `outputs()`, `effectiveOutcomeOf` — so each costs the task, never the run.
3177
+ */
3178
+ taskLog = new TaskLog();
3116
3179
  graph;
3117
3180
  regions;
3118
3181
  dispatched = /* @__PURE__ */ new Set();
@@ -3360,7 +3423,7 @@ var Driver = class {
3360
3423
  * brief: *an owner element a resume's replay feeds is TAKEN AT REST*): accepted at the call — every refusal already asked — and its resolution
3361
3424
  * appended when the step ends, in the same call order as the pauses beside it.
3362
3425
  */
3363
- pausePending = [];
3426
+ pausePending = new Fifo();
3364
3427
  /**
3365
3428
  * **The highest attempt number RECORDED for each task, by task id** — `RN-R40`, v0.26, W186, P36 (f) and (g): the
3366
3429
  * number the last `attempted` element for it carried — appended, or fed by a replay. What decides that a `task.running`
@@ -3375,7 +3438,7 @@ var Driver = class {
3375
3438
  * every one is appended when the step ends, BEFORE a pause, an unpause, an answer or a stop pending on that step is taken
3376
3439
  * (`leaveStep()`): the attempt started before any of them. Empty at rest.
3377
3440
  */
3378
- attemptsPending = [];
3441
+ attemptsPending = new Fifo();
3379
3442
  /**
3380
3443
  * **The dispatch hold is SHUT: a halt is on the run, and the driver dispatches and routes nothing further for it** —
3381
3444
  * `RN-R29`,
@@ -3874,9 +3937,7 @@ var Driver = class {
3874
3937
  * handing it to a worker again would run what a person was asked to decide.
3875
3938
  */
3876
3939
  inFlight() {
3877
- const settled = /* @__PURE__ */ new Set();
3878
- for (const r of this.log) if (r.kind === "outcome") settled.add(r.taskId);
3879
- return [...this.dispatched].filter((id) => !settled.has(id));
3940
+ return [...this.dispatched].filter((id) => !this.taskLog.of(id).some((r) => r.kind === "outcome"));
3880
3941
  }
3881
3942
  /**
3882
3943
  * **Whether a callback fires for the element at `seq`** — `RN-R24`. Always, except during a replay,
@@ -3912,7 +3973,7 @@ var Driver = class {
3912
3973
  /** `resolve`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
3913
3974
  resolveStep(taskId, resolvedAs, value, midStep = false) {
3914
3975
  if (!this.parked(taskId)) return false;
3915
- if (this.pausePending.some((c) => c.kind === "answer" && c.record.taskId === taskId)) return false;
3976
+ for (const c of this.pausePending) if (c.kind === "answer" && c.record.taskId === taskId) return false;
3916
3977
  const { runId, nodeId, branchPath } = parseTaskId(taskId);
3917
3978
  if (this.shut() || this.stopRequest !== null) return false;
3918
3979
  if (resolvedAs === "done" && this.opts.validateBoundaries !== false) {
@@ -4267,7 +4328,7 @@ var Driver = class {
4267
4328
  throw new RunControlRefused(
4268
4329
  "pause",
4269
4330
  "paused",
4270
- `RunControlRefused (paused): run "${this.runId}" is already paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 by a call earlier in this step, taken when it ends" : ""} (RN-R35, P34 (e)). Unpause it first. Nothing was recorded.`
4331
+ `RunControlRefused (paused): run "${this.runId}" is already paused${[...this.pausePending].some((c) => c.kind !== "answer") ? " \u2014 by a call earlier in this step, taken when it ends" : ""} (RN-R35, P34 (e)). Unpause it first. Nothing was recorded.`
4271
4332
  );
4272
4333
  }
4273
4334
  if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
@@ -4293,7 +4354,7 @@ var Driver = class {
4293
4354
  throw new RunControlRefused(
4294
4355
  "unpause",
4295
4356
  "not-paused",
4296
- `RunControlRefused (not-paused): run "${this.runId}" is not paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 an unpause earlier in this step already lifts it" : ""} (RN-R35, P34 (e)). Nothing was recorded and nothing was handed on.`
4357
+ `RunControlRefused (not-paused): run "${this.runId}" is not paused${[...this.pausePending].some((c) => c.kind !== "answer") ? " \u2014 an unpause earlier in this step already lifts it" : ""} (RN-R35, P34 (e)). Nothing was recorded and nothing was handed on.`
4297
4358
  );
4298
4359
  }
4299
4360
  this.pausePending.push({ kind: "unpaused", actor: options.actor });
@@ -4301,7 +4362,7 @@ var Driver = class {
4301
4362
  }
4302
4363
  /** The pause the owner has ASKED for: the last call still to be taken, else the pause taken (`RN-R35`). */
4303
4364
  pauseWanted() {
4304
- const last = this.pausePending.filter((c) => c.kind !== "answer").at(-1);
4365
+ const last = [...this.pausePending].filter((c) => c.kind !== "answer").at(-1);
4305
4366
  return last !== void 0 ? last.kind === "paused" : this.paused;
4306
4367
  }
4307
4368
  /**
@@ -4694,7 +4755,7 @@ var Driver = class {
4694
4755
  if (!node) return;
4695
4756
  const itemIndices = this.indicesFrom(nodeId, branchPath);
4696
4757
  const taskId = buildTaskId(runId, nodeId, branchIndices(this.pathOf(nodeId, itemIndices)));
4697
- const outcome = effectiveOutcome(this.log, taskId);
4758
+ const outcome = effectiveOutcome(this.taskLog.of(taskId), taskId);
4698
4759
  if (!outcome) return;
4699
4760
  const result = selectEdge(
4700
4761
  node,
@@ -5041,7 +5102,7 @@ var Driver = class {
5041
5102
  * [ADR 0040](../../doc/decisions/0040-what-a-node-can-reach-is-the-runs-own-outputs-and-a-node-id-names-them-relative-to-a-branch-path.md)
5042
5103
  * rules 2 and 3, and `FD-C2`.
5043
5104
  *
5044
- * **It is a PROJECTION of the log and adds no retention** — measured at
5105
+ * **It is a PROJECTION of the log and adds no retention of a result** — measured at
5045
5106
  * [P12](../../doc/plans/p12-what-a-node-receives-and-what-a-decision-sees.md)'s
5046
5107
  * declaration and again before this was written: this driver holds
5047
5108
  * `private readonly log: LogRecord[]` with exactly one `push` and no `splice`,
@@ -5051,14 +5112,11 @@ var Driver = class {
5051
5112
  * described **today** before this method existed, and this adds **no retention**.
5052
5113
  * *(Q19 is RESOLVED since W53 —
5053
5114
  * [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
5054
- * — and `RN-R13` names this memo by id: it holds results **by reference only**,
5115
+ * — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
5055
5116
  * asserted by IDENTITY at ladder L2 rather than by a byte count, because a
5056
5117
  * reachability SET cannot tell an alias from a copy. W53's readers found that
5057
5118
  * from both ends — the rule's stated test returned a third name on a `needs`
5058
5119
  * flow, and cloning here cost ZERO.)*
5059
- * *(It adds ONE field — `outputsCache` below — which is a memo the log invalidates
5060
- * and which holds nothing the log does not. This read *"zero fields"* until W41's
5061
- * conformity reader read it beside the memo the cost measurement had just added.)*
5062
5120
  *
5063
5121
  * **`effectiveOutcome` and not the raw record**, for `spec/flow-definition.md`
5064
5122
  * section 3 rule 4's own reason: a resumed node has two records in the log and
@@ -5066,43 +5124,175 @@ var Driver = class {
5066
5124
  * suspended has no effective outcome and therefore no entry — which is what makes
5067
5125
  * `FD-R3` fire rather than a reference quietly resolving to `undefined`.
5068
5126
  *
5069
- * **Computed on demand rather than maintained**, and the cost is why: it is
5070
- * `O(log)` per call, so maintaining it would be a second source that could
5071
- * disagree with the log — the defect [Q43](../../doc/questions/q43.md) and
5072
- * `arrival-count`'s re-sourcing are each an instance of. **A node that declares no
5073
- * `needs` never reaches this method**, which is what keeps ADR 0040 rule 1's *the
5074
- * default is unchanged* true of the cost as well as of the value.
5127
+ * **CAUGHT UP rather than rebuilt** — `RN-R13` v0.30, P39 decision 4 and fact 7,
5128
+ * [ADR 0121](../../doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md) (W195), amending ADR 0040
5129
+ * rule 2's *computed rather than maintained*. Until W195 this was memoised on the log's LENGTH and rebuilt from the
5130
+ * WHOLE log whenever the log had grown — so a node with a `needs` entered once per settlement, which is every instance
5131
+ * of a `map` whose branch declares one, paid O(log) per entry and the run O(log²): the case its own memo missed by
5132
+ * construction. Now each call reads only the records appended since the last (`at`, an index into the append-only
5133
+ * `log`), and for each one asks `effectiveOutcome` of THAT TASK's records ({@link TaskLog}) — so the merge stays in
5134
+ * ONE place, which is ADR 0040 rule 2's own reason, and an entry is always the merge of its task's whole sub-log at
5135
+ * the moment of the call: nothing here can say what the log does not. **Equal to a rebuild at every read**
5136
+ * (`test/driver/log-view.test.ts`, at every fold input of every valid corpus flow): the same entries, a node's list
5137
+ * in its tasks' first-record order, each `result` the log record's own object.
5138
+ *
5139
+ * **Still LAZY, so ADR 0040 rule 1's *the default is unchanged* holds of the cost**: nothing is caught up until a
5140
+ * node or an edge that declares a `needs` asks, and a run that declares none never builds it.
5075
5141
  */
5076
5142
  outputs() {
5077
- if (this.outputsCache && this.outputsCache.at === this.log.length) return this.outputsCache.byNode;
5078
- const byNode = /* @__PURE__ */ new Map();
5079
- const groups = /* @__PURE__ */ new Map();
5080
- for (const r of this.log) {
5081
- const g = groups.get(r.taskId);
5082
- if (g) g.push(r);
5083
- else groups.set(r.taskId, [r]);
5084
- }
5085
- for (const [taskId, group] of groups) {
5086
- const eff = effectiveOutcome(group, taskId);
5087
- if (!eff) continue;
5088
- let parts;
5089
- try {
5090
- parts = parseTaskId(taskId);
5091
- } catch {
5092
- continue;
5143
+ const c = this.outputsCache ??= { at: 0, byNode: /* @__PURE__ */ new Map(), tasks: /* @__PURE__ */ new Map(), orders: /* @__PURE__ */ new Map(), index: /* @__PURE__ */ new Map(), depths: /* @__PURE__ */ new Map(), examined: 0 };
5144
+ for (; c.at < this.log.length; c.at += 1) {
5145
+ const r = this.log[c.at];
5146
+ let t = c.tasks.get(r.taskId);
5147
+ if (t === void 0) {
5148
+ let parts;
5149
+ try {
5150
+ parts = parseTaskId(r.taskId);
5151
+ } catch {
5152
+ c.tasks.set(r.taskId, null);
5153
+ continue;
5154
+ }
5155
+ t = { nodeId: parts.nodeId, path: parts.branchPath, order: c.tasks.size, entry: null };
5156
+ c.tasks.set(r.taskId, t);
5093
5157
  }
5094
- const list = byNode.get(parts.nodeId);
5095
- if (list) list.push({ path: parts.branchPath, result: eff.result });
5096
- else byNode.set(parts.nodeId, [{ path: parts.branchPath, result: eff.result }]);
5158
+ if (t === null) continue;
5159
+ this.place(c, t, effectiveOutcome(this.taskLog.of(r.taskId), r.taskId));
5097
5160
  }
5098
- this.outputsCache = { at: this.log.length, byNode };
5099
- return byNode;
5161
+ return c.byNode;
5100
5162
  }
5101
5163
  /**
5102
- * The memo above. **Not run state and not a second copy of it** — it is discarded
5103
- * and rebuilt the moment the log grows, and nothing ever reads it without checking
5104
- * the length first, so there is no state here that could be stale in a way anything
5105
- * could observe. `ADR 0014`'s *the driver computes no run state* is untouched.
5164
+ * **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
5165
+ * was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
5166
+ * in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
5167
+ *
5168
+ * **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
5169
+ * RESOLUTION, later, gives it one: that is the one entry that is not appended at the END of its node's list but placed
5170
+ * at its first record's position — reachable, and the corpus's *parked, attempted, resolved* drain reaches it. *(Whether
5171
+ * a suspended node's ABSENCE is ever read: no — W41's adequacy reader measured making it contribute
5172
+ * `{result: undefined}` cost **0**, because every shipped policy leaves a parked branch holding a token, so no join
5173
+ * fires while one is parked and no reader downstream of it is entered. [Q97](../../doc/questions/q97.md).)* An entry
5174
+ * that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
5175
+ * so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
5176
+ * none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
5177
+ */
5178
+ place(c, t, eff) {
5179
+ const list = c.byNode.get(t.nodeId);
5180
+ const orders = c.orders.get(t.nodeId);
5181
+ if (eff === null) {
5182
+ if (t.entry === null || list === void 0 || orders === void 0) return;
5183
+ const at = list.indexOf(t.entry);
5184
+ list.splice(at, 1);
5185
+ orders.splice(at, 1);
5186
+ this.reindex(c, t.nodeId, t.entry, null);
5187
+ t.entry = null;
5188
+ if (list.length === 0) {
5189
+ c.byNode.delete(t.nodeId);
5190
+ c.orders.delete(t.nodeId);
5191
+ }
5192
+ return;
5193
+ }
5194
+ const entry = { path: t.path, result: eff.result };
5195
+ if (t.entry !== null && list !== void 0) {
5196
+ list[list.indexOf(t.entry)] = entry;
5197
+ this.reindex(c, t.nodeId, t.entry, entry);
5198
+ t.entry = entry;
5199
+ return;
5200
+ }
5201
+ t.entry = entry;
5202
+ this.reindex(c, t.nodeId, null, entry);
5203
+ if (list === void 0 || orders === void 0) {
5204
+ c.byNode.set(t.nodeId, [entry]);
5205
+ c.orders.set(t.nodeId, [t.order]);
5206
+ return;
5207
+ }
5208
+ let i = list.length;
5209
+ while (i > 0 && orders[i - 1] > t.order) i -= 1;
5210
+ list.splice(i, 0, entry);
5211
+ orders.splice(i, 0, t.order);
5212
+ }
5213
+ /**
5214
+ * **One entry's change carried into every index built for its node** — W199, ADR 0124: called by `place` at each of
5215
+ * its cases (a first entry, an append, an insertion at a first-record position, a replacement in place, a removal), so
5216
+ * the index is caught up exactly when `byNode` is and can say nothing `byNode` does not. `was` is the entry leaving
5217
+ * (`null` for an insertion), `now` the one arriving (`null` for a removal); a replacement keeps its path, so its
5218
+ * position. A group is ordered by PATH, not by first record, so where `byNode` inserted mid-list the group finds its own
5219
+ * place by a binary search. An entry with fewer than `depth` segments is visible to no reader at that depth and is filed
5220
+ * nowhere.
5221
+ */
5222
+ reindex(c, nodeId, was, now) {
5223
+ const byDepth = c.index.get(nodeId);
5224
+ if (byDepth === void 0) return;
5225
+ const path = (now ?? was).path;
5226
+ for (const [depth, groups] of byDepth) {
5227
+ const key = visibilityKey(path, depth);
5228
+ if (key === null) continue;
5229
+ c.examined += 1;
5230
+ const group = groups.get(key);
5231
+ if (was !== null) {
5232
+ const at = groupPosition(group, path);
5233
+ if (now !== null) group[at] = now;
5234
+ else if (group.length === 1) groups.delete(key);
5235
+ else group.splice(at, 1);
5236
+ } else if (group === void 0) {
5237
+ groups.set(key, [now]);
5238
+ } else {
5239
+ group.splice(groupPosition(group, path), 0, now);
5240
+ }
5241
+ }
5242
+ }
5243
+ /**
5244
+ * **A node's outputs grouped for readers at `depth`** — built on the first read at that depth from the node's current
5245
+ * entries (each one examined), each group sorted ONCE, and from then on kept by `reindex`.
5246
+ */
5247
+ groupsOf(c, nodeId, depth) {
5248
+ let byDepth = c.index.get(nodeId);
5249
+ if (byDepth === void 0) c.index.set(nodeId, byDepth = /* @__PURE__ */ new Map());
5250
+ let groups = byDepth.get(depth);
5251
+ if (groups !== void 0) return groups;
5252
+ groups = /* @__PURE__ */ new Map();
5253
+ for (const e of c.byNode.get(nodeId) ?? NO_OUTPUTS) {
5254
+ c.examined += 1;
5255
+ const key = visibilityKey(e.path, depth);
5256
+ if (key === null) continue;
5257
+ const group = groups.get(key);
5258
+ if (group === void 0) groups.set(key, [e]);
5259
+ else group.push(e);
5260
+ }
5261
+ for (const group of groups.values()) if (group.length > 1) group.sort((x, y) => compareBranchPaths(x.path, y.path));
5262
+ byDepth.set(depth, groups);
5263
+ return groups;
5264
+ }
5265
+ /** The depth `reader` and `target` share — `sharedRegionDepth`, a graph fact, asked once per pair. */
5266
+ depthOf(c, reader, target) {
5267
+ let byTarget = c.depths.get(reader);
5268
+ if (byTarget === void 0) c.depths.set(reader, byTarget = /* @__PURE__ */ new Map());
5269
+ let depth = byTarget.get(target);
5270
+ if (depth === void 0) byTarget.set(target, depth = sharedRegionDepth(this.regions, reader, target));
5271
+ return depth;
5272
+ }
5273
+ /**
5274
+ * **The outputs of `nodeId` visible to `atNodeId` at `here`, in `compareBranchPaths` order** — W199, ADR 0124: ADR
5275
+ * 0044's predicate answered by a LOOKUP. The predicate is `visibilityKey(here, d) === visibilityKey(path, d)` at the depth
5276
+ * `d` the two share (`outputIsVisible`), so the visible outputs are exactly the group filed under the reader's key: what
5277
+ * the filter over every output and the sort of its survivors returned until W199, found in the cost of its answer.
5278
+ * **The index's own list, never handed out** — the caller maps it to copies.
5279
+ */
5280
+ visibleOutputs(atNodeId, here, nodeId) {
5281
+ this.outputs();
5282
+ const c = this.outputsCache;
5283
+ const depth = this.depthOf(c, atNodeId, nodeId);
5284
+ const key = visibilityKey(here, depth);
5285
+ if (key === null) return NO_OUTPUTS;
5286
+ return this.groupsOf(c, nodeId, depth).get(key) ?? NO_OUTPUTS;
5287
+ }
5288
+ /**
5289
+ * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
5290
+ * it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
5291
+ * never a copy — `tasks` is one slot per task id
5292
+ * read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
5293
+ * node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
5294
+ * read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
5295
+ * *the driver computes no run state* is untouched. `null` until a `needs` first asks.
5106
5296
  */
5107
5297
  outputsCache = null;
5108
5298
  /**
@@ -5125,7 +5315,7 @@ var Driver = class {
5125
5315
  compose(node, edgeValue, itemIndices, runPayload, item) {
5126
5316
  const needs = node.needs;
5127
5317
  if (!needs) return edgeValue;
5128
- let lookup = null;
5318
+ let here = null;
5129
5319
  const out = {};
5130
5320
  for (const [key, ref] of Object.entries(needs)) {
5131
5321
  const t = referenceTarget(ref);
@@ -5141,8 +5331,8 @@ var Driver = class {
5141
5331
  out[key] = cloneValue(item());
5142
5332
  continue;
5143
5333
  }
5144
- lookup ??= { here: this.pathOf(node.id, itemIndices), produced: this.outputs() };
5145
- const v = this.resolveReference(node.id, lookup.here, lookup.produced, key, t, "needs");
5334
+ here ??= this.pathOf(node.id, itemIndices);
5335
+ const v = this.resolveReference(node.id, here, key, t, "needs");
5146
5336
  if (v !== ABSENT) out[key] = v;
5147
5337
  }
5148
5338
  return out;
@@ -5163,8 +5353,8 @@ var Driver = class {
5163
5353
  *
5164
5354
  * @throws UnresolvedReference — `FD-R3`.
5165
5355
  */
5166
- resolveReference(atNodeId, here, produced, key, t, site) {
5167
- const visible = (produced.get(t.node) ?? []).filter((e) => outputIsVisible(this.regions, atNodeId, here, t.node, e.path)).sort((x, y) => compareBranchPaths(x.path, y.path));
5356
+ resolveReference(atNodeId, here, key, t, site) {
5357
+ const visible = this.visibleOutputs(atNodeId, here, t.node);
5168
5358
  if (visible.length === 0 && t.conditional) {
5169
5359
  return t.form === "all" ? [] : ABSENT;
5170
5360
  }
@@ -5173,6 +5363,7 @@ var Driver = class {
5173
5363
  `node "${atNodeId}" at branch path "${here}" declares \`${site}[${JSON.stringify(key)}]\` naming "${t.node}", and no output of that node is on this path. \`FD-V27\` checked that this node is reachable FROM it and \`FD-V29\` that it must have RUN, which is everything the graph can say; whether any of its outputs is on YOUR path is a statement about this run \u2014 a reader inside a region whose indices do not line up with the target\`s, or a join policy that fired before that node settled. FD-R3.`
5174
5364
  );
5175
5365
  }
5366
+ this.outputsCache.examined += t.form === "all" ? visible.length : 1;
5176
5367
  return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
5177
5368
  }
5178
5369
  /**
@@ -5225,7 +5416,7 @@ var Driver = class {
5225
5416
  const needs = edge.needs;
5226
5417
  if (!needs) return EMPTY_NEEDS;
5227
5418
  const here = this.pathOf(node.id, itemIndices);
5228
- const produced = this.outputs();
5419
+ this.outputs();
5229
5420
  const out = {};
5230
5421
  for (const [key, ref] of Object.entries(needs)) {
5231
5422
  const t = referenceTarget(ref);
@@ -5240,7 +5431,7 @@ var Driver = class {
5240
5431
  if (t.form === "self") {
5241
5432
  throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
5242
5433
  }
5243
- const v = this.resolveReference(node.id, here, produced, key, t, "edge needs");
5434
+ const v = this.resolveReference(node.id, here, key, t, "edge needs");
5244
5435
  if (v !== ABSENT) out[key] = v;
5245
5436
  }
5246
5437
  return Object.freeze(out);
@@ -5332,7 +5523,7 @@ var Driver = class {
5332
5523
  * predicate read it here: `resolve()`, the replay's fed resolution, and the three `ended` refusals (`hasParked`).
5333
5524
  */
5334
5525
  parked(taskId) {
5335
- return isSuspended(this.log, taskId, this.asked.has(taskId));
5526
+ return isSuspended(this.taskLog.of(taskId), taskId, this.asked.has(taskId));
5336
5527
  }
5337
5528
  /**
5338
5529
  * **Whether anything in the run waits on a person** — a parked task or an unanswered ask. `cancel`, `stop` and `pause`
@@ -5341,7 +5532,7 @@ var Driver = class {
5341
5532
  * never dispatched, it is never in flight, so every road reading `inFlight()` passes it by (`RN-R38`, W181).
5342
5533
  */
5343
5534
  hasParked() {
5344
- for (const id of this.dispatched) if (isSuspended(this.log, id)) return true;
5535
+ for (const id of this.dispatched) if (isSuspended(this.taskLog.of(id), id)) return true;
5345
5536
  for (const id of this.asked) if (this.parked(id)) return true;
5346
5537
  return false;
5347
5538
  }
@@ -5619,13 +5810,18 @@ var Driver = class {
5619
5810
  *
5620
5811
  * A record is pushed to `log` as well, by reference: `effectiveOutcome` reads
5621
5812
  * the stored object and `OR-R8` derives rather than appends, so a second copy
5622
- * here would be a second truth. What leaves through `onFoldInput` **is** a
5813
+ * here would be a second truth. **And to its task's sub-log in {@link TaskLog}**,
5814
+ * by reference too (`RN-R13` v0.30, ADR 0121, W195) — here, the one place every
5815
+ * element passes, so the view can never lag the log. What leaves through `onFoldInput` **is** a
5623
5816
  * copy, frozen, for the same reason `snapshot()` is: a listener holding the
5624
5817
  * driver's own array could forge a settlement and bypass `RS-R1`.
5625
5818
  */
5626
5819
  append(input) {
5627
5820
  const seq = this.inputs.push(input) - 1;
5628
- if (input.kind === "outcome" || input.kind === "resolution") this.log.push(input.record);
5821
+ if (input.kind === "outcome" || input.kind === "resolution") {
5822
+ this.log.push(input.record);
5823
+ this.taskLog.push(input.record);
5824
+ }
5629
5825
  const replay = this.replay;
5630
5826
  if (replay !== null && seq < replay.stored.length) {
5631
5827
  const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
@@ -5850,7 +6046,7 @@ var Driver = class {
5850
6046
  * the way in.
5851
6047
  */
5852
6048
  effectiveOutcomeOf(taskId) {
5853
- const o = effectiveOutcome(this.log, taskId);
6049
+ const o = effectiveOutcome(this.taskLog.of(taskId), taskId);
5854
6050
  return o === null ? null : cloneRecord(o);
5855
6051
  }
5856
6052
  terminalStateOf(taskId) {
@@ -5858,6 +6054,484 @@ var Driver = class {
5858
6054
  }
5859
6055
  };
5860
6056
 
6057
+ // src/fold/accumulator.ts
6058
+ var Suspended = class {
6059
+ items = [];
6060
+ alive = /* @__PURE__ */ new Map();
6061
+ gen = 0;
6062
+ count;
6063
+ constructor(from, count) {
6064
+ this.count = count;
6065
+ const g = ++this.gen;
6066
+ for (const v of from) {
6067
+ this.alive.set(v, g);
6068
+ this.items.push({ v, g });
6069
+ }
6070
+ count(from.length);
6071
+ }
6072
+ has(v) {
6073
+ return this.alive.has(v);
6074
+ }
6075
+ add(v) {
6076
+ if (this.alive.has(v)) return;
6077
+ const g = ++this.gen;
6078
+ this.alive.set(v, g);
6079
+ this.items.push({ v, g });
6080
+ }
6081
+ remove(v) {
6082
+ this.alive.delete(v);
6083
+ }
6084
+ toArray() {
6085
+ this.count(this.items.length);
6086
+ const out = [];
6087
+ for (const e of this.items) if (this.alive.get(e.v) === e.g) out.push(e.v);
6088
+ return out;
6089
+ }
6090
+ };
6091
+ function regionWeight(opened, joined) {
6092
+ const unjoined = joined !== true ? 1 : 0;
6093
+ return { unjoined, openedNothing: opened.length === 0 && joined !== true ? 1 : 0 };
6094
+ }
6095
+ var hasOwn2 = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
6096
+ function put(o, k, v) {
6097
+ if (k === "__proto__") Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
6098
+ else o[k] = v;
6099
+ }
6100
+ var Accumulator = class _Accumulator {
6101
+ runId;
6102
+ outcome;
6103
+ haltedBy;
6104
+ cancelRequest;
6105
+ interruption;
6106
+ paused;
6107
+ /** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
6108
+ nodes = /* @__PURE__ */ new Map();
6109
+ /** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
6110
+ regions = /* @__PURE__ */ new Map();
6111
+ pathEnds = [];
6112
+ resumes = [];
6113
+ gated = [];
6114
+ counts = null;
6115
+ base;
6116
+ counter;
6117
+ constructor(base, counter) {
6118
+ this.base = base;
6119
+ this.counter = counter;
6120
+ this.runId = base.runId;
6121
+ this.outcome = base.outcome;
6122
+ this.haltedBy = base.haltedBy;
6123
+ this.cancelRequest = base.cancelRequest;
6124
+ this.interruption = base.interruption;
6125
+ this.paused = base.paused;
6126
+ }
6127
+ /** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
6128
+ static load(state, counter) {
6129
+ return new _Accumulator(state, counter);
6130
+ }
6131
+ tick(n = 1) {
6132
+ if (this.counter !== void 0) this.counter.visits += n;
6133
+ }
6134
+ // --- the reads a runtime keeps an accumulator OPEN for — W196, ADR 0122, `RN-R44` -----------------------------
6135
+ /**
6136
+ * **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
6137
+ * O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
6138
+ */
6139
+ outcomeNow() {
6140
+ return this.outcome;
6141
+ }
6142
+ /**
6143
+ * **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
6144
+ * An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
6145
+ * dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
6146
+ */
6147
+ entry(taskId) {
6148
+ if (this.nodes.has(taskId)) return this.nodes.get(taskId);
6149
+ return hasOwn2(this.base.nodes, taskId) ? this.base.nodes[taskId] : void 0;
6150
+ }
6151
+ // --- reads, through to the base ---------------------------------------------------------------------------
6152
+ /** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
6153
+ node(taskId) {
6154
+ return this.nodes.has(taskId) ? this.nodes.get(taskId) : this.base.nodes[taskId];
6155
+ }
6156
+ /** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
6157
+ instance(regionId, regionPath) {
6158
+ const touched = this.regions.get(regionId)?.get(regionPath);
6159
+ return touched ?? this.base.regions[regionId]?.[regionPath];
6160
+ }
6161
+ /** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
6162
+ tally() {
6163
+ if (this.counts !== null) return this.counts;
6164
+ const c = { byState: /* @__PURE__ */ new Map(), nodes: 0, unjoined: 0, openedNothing: 0, noEligibleEdge: 0, ends: 0, pathEnds: 0 };
6165
+ const baseNodes = this.base.nodes;
6166
+ const keys = Object.keys(baseNodes);
6167
+ this.tick(keys.length);
6168
+ for (const k of keys) if (!this.nodes.has(k)) this.countNode(c, baseNodes[k], 1);
6169
+ for (const n of this.nodes.values()) this.countNode(c, n, 1);
6170
+ this.tick(this.nodes.size);
6171
+ const baseRegions = this.base.regions;
6172
+ for (const id of Object.keys(baseRegions)) {
6173
+ const byPath = baseRegions[id];
6174
+ const touched = this.regions.get(id);
6175
+ for (const p of Object.keys(byPath)) {
6176
+ this.tick();
6177
+ if (touched?.has(p)) continue;
6178
+ const r = byPath[p];
6179
+ const w = regionWeight(r.opened, r.joined);
6180
+ c.unjoined += w.unjoined;
6181
+ c.openedNothing += w.openedNothing;
6182
+ }
6183
+ }
6184
+ for (const byPath of this.regions.values()) {
6185
+ for (const acc of byPath.values()) {
6186
+ this.tick();
6187
+ const w = regionWeight(acc.opened, acc.joined);
6188
+ c.unjoined += w.unjoined;
6189
+ c.openedNothing += w.openedNothing;
6190
+ }
6191
+ }
6192
+ for (const list of [this.base.pathEnds, this.pathEnds]) {
6193
+ this.tick(list.length);
6194
+ for (const e of list) {
6195
+ c.pathEnds += 1;
6196
+ if (e.reason === "no-eligible-edge") c.noEligibleEdge += 1;
6197
+ if (e.reason === "end") c.ends += 1;
6198
+ }
6199
+ }
6200
+ this.counts = c;
6201
+ return c;
6202
+ }
6203
+ countNode(c, n, by) {
6204
+ c.nodes += by;
6205
+ c.byState.set(n.state, (c.byState.get(n.state) ?? 0) + by);
6206
+ }
6207
+ stateCount(state) {
6208
+ return this.tally().byState.get(state) ?? 0;
6209
+ }
6210
+ // --- writes ------------------------------------------------------------------------------------------------
6211
+ /** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
6212
+ setNode(taskId, entry) {
6213
+ if (this.counts !== null) {
6214
+ const own3 = this.nodes.has(taskId) || hasOwn2(this.base.nodes, taskId);
6215
+ if (own3) this.countNode(this.counts, this.node(taskId), -1);
6216
+ this.countNode(this.counts, entry, 1);
6217
+ }
6218
+ this.nodes.set(taskId, entry);
6219
+ this.tick();
6220
+ }
6221
+ /**
6222
+ * The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
6223
+ * paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
6224
+ */
6225
+ touchRegion(regionId, regionPath) {
6226
+ let byPath = this.regions.get(regionId);
6227
+ let acc = byPath?.get(regionPath);
6228
+ if (acc === void 0) {
6229
+ const at = this.base.regions[regionId]?.[regionPath];
6230
+ if (at) {
6231
+ this.tick(at.opened.length + Object.keys(at.settled).length);
6232
+ acc = {
6233
+ origin: at,
6234
+ opened: [...at.opened],
6235
+ openedSet: new Set(at.opened),
6236
+ settled: new Map(Object.entries(at.settled)),
6237
+ suspended: new Suspended(at.suspended, (n) => this.tick(n)),
6238
+ joined: at.joined
6239
+ };
6240
+ } else {
6241
+ acc = {
6242
+ origin: { opened: [], settled: {}, suspended: [], joined: null },
6243
+ opened: [],
6244
+ openedSet: /* @__PURE__ */ new Set(),
6245
+ settled: /* @__PURE__ */ new Map(),
6246
+ suspended: new Suspended([], (n) => this.tick(n)),
6247
+ joined: null
6248
+ };
6249
+ }
6250
+ if (byPath === void 0) {
6251
+ byPath = /* @__PURE__ */ new Map();
6252
+ this.regions.set(regionId, byPath);
6253
+ }
6254
+ byPath.set(regionPath, acc);
6255
+ const inner = this.base.regions[regionId];
6256
+ if (this.counts !== null && at && inner !== void 0 && hasOwn2(this.base.regions, regionId) && hasOwn2(inner, regionPath)) {
6257
+ this.unweigh(regionWeight(at.opened, at.joined));
6258
+ }
6259
+ } else if (this.counts !== null) {
6260
+ this.unweigh(regionWeight(acc.opened, acc.joined));
6261
+ }
6262
+ this.tick();
6263
+ return acc;
6264
+ }
6265
+ unweigh(w) {
6266
+ this.counts.unjoined -= w.unjoined;
6267
+ this.counts.openedNothing -= w.openedNothing;
6268
+ }
6269
+ /** The instance's share of the region counts put back after a write. */
6270
+ settleRegion(acc) {
6271
+ if (this.counts === null) return;
6272
+ const w = regionWeight(acc.opened, acc.joined);
6273
+ this.counts.unjoined += w.unjoined;
6274
+ this.counts.openedNothing += w.openedNothing;
6275
+ }
6276
+ addPathEnd(e) {
6277
+ this.pathEnds.push(e);
6278
+ this.tick();
6279
+ if (this.counts !== null) {
6280
+ this.counts.pathEnds += 1;
6281
+ if (e.reason === "no-eligible-edge") this.counts.noEligibleEdge += 1;
6282
+ if (e.reason === "end") this.counts.ends += 1;
6283
+ }
6284
+ }
6285
+ // --- the arms -------------------------------------------------------------------------------------------------
6286
+ /**
6287
+ * **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
6288
+ * `false` where it returned the state it was handed, so `step` can return that same object. Throws where
6289
+ * that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
6290
+ * the module header's stated limit on region paths.
6291
+ */
6292
+ apply(input) {
6293
+ this.tick();
6294
+ if (input.kind === "halted" || input.kind === "stopped") {
6295
+ this.haltedBy = input.reason;
6296
+ this.outcome = "halted";
6297
+ return true;
6298
+ }
6299
+ if (input.kind === "cancelRequested") {
6300
+ if (this.cancelRequest !== null || this.outcome === "cancelled") return false;
6301
+ this.cancelRequest = { actor: input.actor, reason: input.reason };
6302
+ return this.withOutcome();
6303
+ }
6304
+ if (input.kind === "interrupted") {
6305
+ if (this.interruption !== null) return false;
6306
+ this.interruption = { actor: input.actor, reason: input.reason };
6307
+ return this.withOutcome();
6308
+ }
6309
+ if (input.kind === "paused" || input.kind === "unpaused") {
6310
+ const paused = input.kind === "paused";
6311
+ if (this.paused === paused) return false;
6312
+ this.paused = paused;
6313
+ return true;
6314
+ }
6315
+ if (input.kind === "region") {
6316
+ this.applyRegion(input.event);
6317
+ return this.withOutcome();
6318
+ }
6319
+ if (input.kind === "dispatched") {
6320
+ const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
6321
+ if (this.node(input.taskId)) return false;
6322
+ this.runId = this.runId ?? runId2;
6323
+ this.setNode(input.taskId, {
6324
+ runId: runId2,
6325
+ nodeId: nodeId2,
6326
+ branchPath: branchPath2,
6327
+ flowAttempt: attempt2,
6328
+ attemptNumber: null,
6329
+ state: "running",
6330
+ result: null,
6331
+ error: null
6332
+ });
6333
+ return this.withOutcome();
6334
+ }
6335
+ if (input.kind === "asked") {
6336
+ const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
6337
+ if (this.node(input.taskId)) return false;
6338
+ this.runId = this.runId ?? runId2;
6339
+ this.setNode(input.taskId, {
6340
+ runId: runId2,
6341
+ nodeId: nodeId2,
6342
+ branchPath: branchPath2,
6343
+ flowAttempt: attempt2,
6344
+ attemptNumber: null,
6345
+ state: "suspended",
6346
+ result: null,
6347
+ error: null,
6348
+ input: structuredClone(input.input)
6349
+ });
6350
+ return this.withOutcome();
6351
+ }
6352
+ if (input.kind === "pathEnded") {
6353
+ this.addPathEnd({ reason: input.reason, at: input.at });
6354
+ return this.withOutcome();
6355
+ }
6356
+ if (input.kind === "regionEntered") {
6357
+ if (this.instance(input.regionId, input.regionPath)) return false;
6358
+ const acc = this.touchRegion(input.regionId, input.regionPath);
6359
+ this.settleRegion(acc);
6360
+ return this.withOutcome();
6361
+ }
6362
+ if (input.kind === "resumed") {
6363
+ this.resumes.push([...input.taskIds]);
6364
+ this.tick(input.taskIds.length + 1);
6365
+ if (this.interruption !== null) {
6366
+ this.interruption = null;
6367
+ return this.withOutcome();
6368
+ }
6369
+ return true;
6370
+ }
6371
+ if (input.kind === "gated") {
6372
+ const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
6373
+ if (input.key !== void 0) d.key = structuredClone(input.key);
6374
+ this.gated.push(d);
6375
+ this.tick();
6376
+ return true;
6377
+ }
6378
+ if (input.kind === "attempted") {
6379
+ const node = this.node(input.taskId);
6380
+ if (node === void 0) return false;
6381
+ this.setNode(input.taskId, { ...node, attemptNumber: input.attemptNumber });
6382
+ return true;
6383
+ }
6384
+ if (input.kind === "joinSettled") {
6385
+ const acc = this.touchRegion(input.regionId, input.regionPath);
6386
+ acc.joined = input.fired;
6387
+ this.settleRegion(acc);
6388
+ return this.withOutcome();
6389
+ }
6390
+ const r = input.record;
6391
+ const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
6392
+ const existing = this.node(r.taskId);
6393
+ const attemptNumber = existing?.attemptNumber ?? null;
6394
+ if (r.kind === "outcome") {
6395
+ this.setNode(r.taskId, {
6396
+ runId,
6397
+ nodeId,
6398
+ branchPath,
6399
+ flowAttempt: attempt,
6400
+ attemptNumber,
6401
+ // `user_action` is `suspended` at flow level: a resolution resumes it.
6402
+ state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
6403
+ result: r.result,
6404
+ error: r.error
6405
+ });
6406
+ } else {
6407
+ this.setNode(r.taskId, {
6408
+ runId: existing?.runId ?? runId,
6409
+ nodeId: existing?.nodeId ?? nodeId,
6410
+ branchPath: existing?.branchPath ?? branchPath,
6411
+ flowAttempt: existing?.flowAttempt ?? attempt,
6412
+ attemptNumber,
6413
+ state: r.resolvedAs,
6414
+ result: r.result,
6415
+ error: r.error,
6416
+ ...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
6417
+ });
6418
+ }
6419
+ this.runId = this.runId ?? runId;
6420
+ return this.withOutcome();
6421
+ }
6422
+ applyRegion(e) {
6423
+ const acc = this.touchRegion(e.regionId, e.regionPath);
6424
+ switch (e.type) {
6425
+ case "branch.opened":
6426
+ if (!acc.openedSet.has(e.branchPath)) {
6427
+ acc.opened.push(e.branchPath);
6428
+ acc.openedSet.add(e.branchPath);
6429
+ }
6430
+ break;
6431
+ case "branch.arrived":
6432
+ case "branch.exited":
6433
+ case "branch.lost": {
6434
+ const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
6435
+ acc.settled.set(e.branchPath, how);
6436
+ acc.suspended.remove(e.branchPath);
6437
+ break;
6438
+ }
6439
+ case "branch.suspended":
6440
+ acc.suspended.add(e.branchPath);
6441
+ break;
6442
+ case "branch.resumed":
6443
+ acc.suspended.remove(e.branchPath);
6444
+ break;
6445
+ }
6446
+ this.settleRegion(acc);
6447
+ }
6448
+ /**
6449
+ * **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
6450
+ * one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
6451
+ * recomputes has moved the state.
6452
+ */
6453
+ withOutcome() {
6454
+ this.outcome = this.precedence();
6455
+ return true;
6456
+ }
6457
+ precedence() {
6458
+ if (this.haltedBy !== null) return "halted";
6459
+ if (this.cancelRequest === null && this.stateCount("cancelled") > 0) return "cancelled";
6460
+ if (this.cancelRequest !== null) return this.stateCount("running") > 0 ? "cancelling" : "cancelled";
6461
+ if (this.interruption !== null) return "interrupted";
6462
+ if (this.stateCount("suspended") > 0) return "awaiting_action";
6463
+ if (this.stateCount("running") > 0) return "running";
6464
+ const c = this.tally();
6465
+ if (c.nodes === 0 && c.pathEnds === 0 && c.openedNothing === 0) return "running";
6466
+ if (c.unjoined > 0) return "stuck";
6467
+ if (c.noEligibleEdge > 0) return "failed";
6468
+ if (c.ends > 0) return "completed";
6469
+ if (this.stateCount("failed") > 0) return "failed";
6470
+ return "completed";
6471
+ }
6472
+ // --- materialisation ------------------------------------------------------------------------------------------
6473
+ /**
6474
+ * **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
6475
+ * the base's own object, and one that was written is a new one with the base's entries in their places and the new
6476
+ * entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
6477
+ */
6478
+ materialise() {
6479
+ const b = this.base;
6480
+ let nodes = b.nodes;
6481
+ if (this.nodes.size > 0) {
6482
+ nodes = { ...b.nodes };
6483
+ this.tick(Object.keys(b.nodes).length);
6484
+ for (const [k, n] of this.nodes) put(nodes, k, n);
6485
+ this.tick(this.nodes.size);
6486
+ }
6487
+ let regions = b.regions;
6488
+ if (this.regions.size > 0) {
6489
+ regions = { ...b.regions };
6490
+ this.tick(Object.keys(b.regions).length);
6491
+ for (const [id, byPath] of this.regions) {
6492
+ const inner = { ...b.regions[id] };
6493
+ this.tick(Object.keys(inner).length);
6494
+ for (const [p, acc] of byPath) put(inner, p, this.materialiseRegion(acc));
6495
+ put(regions, id, inner);
6496
+ }
6497
+ }
6498
+ return {
6499
+ ...b,
6500
+ runId: this.runId,
6501
+ outcome: this.outcome,
6502
+ nodes,
6503
+ regions,
6504
+ pathEnds: this.list(b.pathEnds, this.pathEnds),
6505
+ haltedBy: this.haltedBy,
6506
+ cancelRequest: this.cancelRequest,
6507
+ interruption: this.interruption,
6508
+ paused: this.paused,
6509
+ resumes: this.list(b.resumes, this.resumes),
6510
+ gated: this.list(b.gated, this.gated)
6511
+ };
6512
+ }
6513
+ list(base, added) {
6514
+ if (added.length === 0) return base;
6515
+ this.tick(base.length + added.length);
6516
+ return [...base, ...added];
6517
+ }
6518
+ materialiseRegion(acc) {
6519
+ const settled = {};
6520
+ for (const [k, v] of acc.settled) settled[k] = v;
6521
+ this.tick(acc.opened.length + acc.settled.size + 1);
6522
+ return { ...acc.origin, opened: [...acc.opened], settled, suspended: acc.suspended.toArray(), joined: acc.joined };
6523
+ }
6524
+ };
6525
+ function foldWith(inputs, counter) {
6526
+ const acc = Accumulator.load(emptyState(), counter);
6527
+ for (const i of inputs) acc.apply(i);
6528
+ return acc.materialise();
6529
+ }
6530
+ function stepWith(prev, input, counter) {
6531
+ const acc = Accumulator.load(prev, counter);
6532
+ return acc.apply(input) ? acc.materialise() : prev;
6533
+ }
6534
+
5861
6535
  // src/fold/run-state.ts
5862
6536
  function emptyState() {
5863
6537
  return {
@@ -5875,181 +6549,10 @@ function emptyState() {
5875
6549
  };
5876
6550
  }
5877
6551
  function step(prev, input) {
5878
- if (input.kind === "halted" || input.kind === "stopped") {
5879
- return { ...prev, haltedBy: input.reason, outcome: "halted" };
5880
- }
5881
- if (input.kind === "cancelRequested") {
5882
- if (prev.cancelRequest !== null || prev.outcome === "cancelled") return prev;
5883
- return withOutcome({ ...prev, cancelRequest: { actor: input.actor, reason: input.reason } });
5884
- }
5885
- if (input.kind === "interrupted") {
5886
- if (prev.interruption !== null) return prev;
5887
- return withOutcome({ ...prev, interruption: { actor: input.actor, reason: input.reason } });
5888
- }
5889
- if (input.kind === "paused" || input.kind === "unpaused") {
5890
- const paused = input.kind === "paused";
5891
- if (prev.paused === paused) return prev;
5892
- return { ...prev, paused };
5893
- }
5894
- if (input.kind === "region") {
5895
- return withOutcome(applyRegion(prev, input.event));
5896
- }
5897
- if (input.kind === "dispatched") {
5898
- const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
5899
- if (prev.nodes[input.taskId]) return prev;
5900
- return withOutcome({
5901
- ...prev,
5902
- runId: prev.runId ?? runId2,
5903
- nodes: {
5904
- ...prev.nodes,
5905
- [input.taskId]: { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, flowAttempt: attempt2, attemptNumber: null, state: "running", result: null, error: null }
5906
- }
5907
- });
5908
- }
5909
- if (input.kind === "asked") {
5910
- const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
5911
- if (prev.nodes[input.taskId]) return prev;
5912
- return withOutcome({
5913
- ...prev,
5914
- runId: prev.runId ?? runId2,
5915
- nodes: {
5916
- ...prev.nodes,
5917
- [input.taskId]: {
5918
- runId: runId2,
5919
- nodeId: nodeId2,
5920
- branchPath: branchPath2,
5921
- flowAttempt: attempt2,
5922
- attemptNumber: null,
5923
- state: "suspended",
5924
- result: null,
5925
- error: null,
5926
- input: structuredClone(input.input)
5927
- }
5928
- }
5929
- });
5930
- }
5931
- if (input.kind === "pathEnded") {
5932
- return withOutcome({ ...prev, pathEnds: [...prev.pathEnds, { reason: input.reason, at: input.at }] });
5933
- }
5934
- if (input.kind === "regionEntered") {
5935
- if (prev.regions[input.regionId]?.[input.regionPath]) return prev;
5936
- return withOutcome({
5937
- ...prev,
5938
- regions: withRegion(prev.regions, input.regionId, input.regionPath, emptyRegion())
5939
- });
5940
- }
5941
- if (input.kind === "resumed") {
5942
- const resumes = [...prev.resumes, [...input.taskIds]];
5943
- if (prev.interruption !== null) return withOutcome({ ...prev, interruption: null, resumes });
5944
- return { ...prev, resumes };
5945
- }
5946
- if (input.kind === "gated") {
5947
- const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
5948
- if (input.key !== void 0) d.key = structuredClone(input.key);
5949
- return { ...prev, gated: [...prev.gated, d] };
5950
- }
5951
- if (input.kind === "attempted") {
5952
- const node = prev.nodes[input.taskId];
5953
- if (node === void 0) return prev;
5954
- return { ...prev, nodes: { ...prev.nodes, [input.taskId]: { ...node, attemptNumber: input.attemptNumber } } };
5955
- }
5956
- if (input.kind === "joinSettled") {
5957
- const cur = prev.regions[input.regionId]?.[input.regionPath] ?? emptyRegion();
5958
- return withOutcome({
5959
- ...prev,
5960
- regions: withRegion(prev.regions, input.regionId, input.regionPath, { ...cur, joined: input.fired })
5961
- });
5962
- }
5963
- const r = input.record;
5964
- const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
5965
- const nodes = { ...prev.nodes };
5966
- const existing = nodes[r.taskId];
5967
- const attemptNumber = existing?.attemptNumber ?? null;
5968
- if (r.kind === "outcome") {
5969
- nodes[r.taskId] = {
5970
- runId,
5971
- nodeId,
5972
- branchPath,
5973
- flowAttempt: attempt,
5974
- attemptNumber,
5975
- // `user_action` is `suspended` at flow level: a resolution resumes it, and
5976
- // calling it terminal is what makes a design drop the fifth state.
5977
- state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
5978
- result: r.result,
5979
- error: r.error
5980
- };
5981
- } else {
5982
- nodes[r.taskId] = {
5983
- runId: existing?.runId ?? runId,
5984
- nodeId: existing?.nodeId ?? nodeId,
5985
- branchPath: existing?.branchPath ?? branchPath,
5986
- flowAttempt: existing?.flowAttempt ?? attempt,
5987
- attemptNumber,
5988
- state: r.resolvedAs,
5989
- result: r.result,
5990
- error: r.error,
5991
- ...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
5992
- };
5993
- }
5994
- return withOutcome({ ...prev, runId: prev.runId ?? runId, nodes });
5995
- }
5996
- function emptyRegion() {
5997
- return { opened: [], settled: {}, suspended: [], joined: null };
5998
- }
5999
- function withRegion(regions, regionId, regionPath, run) {
6000
- return { ...regions, [regionId]: { ...regions[regionId], [regionPath]: run } };
6001
- }
6002
- function allRegionRuns(s) {
6003
- return Object.values(s.regions).flatMap((byPath) => Object.values(byPath));
6004
- }
6005
- function applyRegion(prev, e) {
6006
- const at = prev.regions[e.regionId]?.[e.regionPath];
6007
- const cur = at ? { ...at, opened: [...at.opened], settled: { ...at.settled }, suspended: [...at.suspended] } : emptyRegion();
6008
- switch (e.type) {
6009
- case "branch.opened":
6010
- if (!cur.opened.includes(e.branchPath)) cur.opened.push(e.branchPath);
6011
- break;
6012
- case "branch.arrived":
6013
- case "branch.exited":
6014
- case "branch.lost": {
6015
- const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
6016
- cur.settled[e.branchPath] = how;
6017
- cur.suspended = cur.suspended.filter((b) => b !== e.branchPath);
6018
- break;
6019
- }
6020
- case "branch.suspended":
6021
- if (!cur.suspended.includes(e.branchPath)) cur.suspended.push(e.branchPath);
6022
- break;
6023
- case "branch.resumed":
6024
- cur.suspended = cur.suspended.filter((b) => b !== e.branchPath);
6025
- break;
6026
- }
6027
- return { ...prev, regions: withRegion(prev.regions, e.regionId, e.regionPath, cur) };
6028
- }
6029
- function withOutcome(s) {
6030
- if (s.haltedBy !== null) return { ...s, outcome: "halted" };
6031
- const nodes = Object.values(s.nodes);
6032
- if (s.cancelRequest === null && nodes.some((n) => n.state === "cancelled")) return { ...s, outcome: "cancelled" };
6033
- if (s.cancelRequest !== null) {
6034
- return { ...s, outcome: nodes.some((n) => n.state === "running") ? "cancelling" : "cancelled" };
6035
- }
6036
- if (s.interruption !== null) return { ...s, outcome: "interrupted" };
6037
- if (nodes.some((n) => n.state === "suspended")) return { ...s, outcome: "awaiting_action" };
6038
- if (nodes.some((n) => n.state === "running")) return { ...s, outcome: "running" };
6039
- const openedNothing = allRegionRuns(s).some((r) => r.opened.length === 0 && r.joined !== true);
6040
- if (nodes.length === 0 && s.pathEnds.length === 0 && !openedNothing) {
6041
- return { ...s, outcome: "running" };
6042
- }
6043
- if (allRegionRuns(s).some((r) => r.joined !== true)) return { ...s, outcome: "stuck" };
6044
- if (s.pathEnds.some((e) => e.reason === "no-eligible-edge")) return { ...s, outcome: "failed" };
6045
- if (s.pathEnds.some((e) => e.reason === "end")) return { ...s, outcome: "completed" };
6046
- if (nodes.some((n) => n.state === "failed")) return { ...s, outcome: "failed" };
6047
- return { ...s, outcome: "completed" };
6552
+ return stepWith(prev, input);
6048
6553
  }
6049
6554
  function fold(inputs) {
6050
- let s = emptyState();
6051
- for (const i of inputs) s = step(s, i);
6052
- return s;
6555
+ return foldWith(inputs);
6053
6556
  }
6054
6557
  function foldLog(log) {
6055
6558
  return fold(log.map((r) => ({ kind: r.kind, record: r })));
@@ -6144,6 +6647,7 @@ export {
6144
6647
  innermostMapOf,
6145
6648
  sharedRegionDepth,
6146
6649
  outputIsVisible,
6650
+ visibilityKey,
6147
6651
  branchIndexOf,
6148
6652
  certainlySettled,
6149
6653
  SCHEMA_KEYWORDS,
@@ -6264,6 +6768,7 @@ export {
6264
6768
  UndispatchedTask,
6265
6769
  MissingJoinPolicy,
6266
6770
  Driver,
6771
+ Accumulator,
6267
6772
  emptyState,
6268
6773
  step,
6269
6774
  fold,