fyflow-orchestration 0.9.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.
package/AGENTS.md CHANGED
@@ -477,6 +477,16 @@ once per item* is a fact about your document rather than about your run. **Measu
477
477
  obstacle** — a size measurement is about 0.03% of what it would warn about at arity 1000.
478
478
  `spec/flow-definition.md` section 3 carries the whole verdict and the measurements.
479
479
 
480
+ **Finding what a reference names costs its ANSWER** — from 0.9.1
481
+ ([ADR 0124](doc/decisions/0124-a-reference-read-costs-its-answer.md), `spec/flow-definition.md` v0.46). A branch task
482
+ may read any earlier task of its own `map` instance by id — one step back or several, where `"."` reaches only its
483
+ predecessor — and the read takes that one output, however many instances the map has: the driver files a node's outputs
484
+ by the branch-path prefix its readers share. In 0.9.0 every read searched every output of the node it names, so such a
485
+ map cost O(items²) (the driver alone at 8,000 items: 22 s in 0.9.0, 0.22 s from 0.9.1, node 24). **A read never costs
486
+ less than its answer**: `["n"]` from a reader outside every region `n` is in — a SIBLING map's reader — is handed every
487
+ output of `n`, so two maps of `n` and `m` instances compose `n·m` entries. That is the answer's price, the cliff above by
488
+ another road, and the remedy is the same.
489
+
480
490
  ### `"$payload"` — what reached the RUN, from any node
481
491
 
482
492
  **`"$payload"` names the value the run was STARTED with** — `start(flow, payload)`'s `payload` — on any node that may
@@ -1743,7 +1753,8 @@ need the state; step only where you hold a short one. Both return exactly what t
1743
1753
  **The driver's own work per settlement does not grow with the run either** — `RN-R13` v0.30, from 0.9.0
1744
1754
  ([ADR 0121](doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md)): every question it asks of its log about
1745
1755
  ONE task — a settlement's effective outcome, whether a task is parked, what is in flight, what a `needs` can reach — reads
1746
- that task's records alone, where 0.8.0 scanned the whole log (a 16,000-item `map` driven on a scheduler double: 999 ms in
1756
+ that task's records alone (and from 0.9.1 a `needs` read takes the outputs it is handed rather than searching its node's —
1757
+ `RN-R13` v0.32, [ADR 0124](doc/decisions/0124-a-reference-read-costs-its-answer.md)), where 0.8.0 scanned the whole log (a 16,000-item `map` driven on a scheduler double: 999 ms in
1747
1758
  0.8.0, 229 ms from 0.9.0, node 24). `snapshot()`, `foldInputs()` and `checkpoint()` still hand you a copy of the whole run,
1748
1759
  O(run) a call: read them when you need the run, not after every event.
1749
1760
 
package/README.md CHANGED
@@ -17,7 +17,7 @@ worker returned, routing can **fan out** into parallel branches, and a **join**
17
17
  The scheduler runs tasks; this library decides **which task runs next**. `fyflow-scheduler` has no
18
18
  dependency graph of its own, so sequence, decision, fan-out and join live here.
19
19
 
20
- Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.9.0`.
20
+ Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.9.1`.
21
21
 
22
22
  ## Install
23
23
 
@@ -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);
@@ -5128,7 +5140,7 @@ var Driver = class {
5128
5140
  * node or an edge that declares a `needs` asks, and a run that declares none never builds it.
5129
5141
  */
5130
5142
  outputs() {
5131
- const c = this.outputsCache ??= { at: 0, byNode: /* @__PURE__ */ new Map(), tasks: /* @__PURE__ */ new Map(), orders: /* @__PURE__ */ new Map() };
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 };
5132
5144
  for (; c.at < this.log.length; c.at += 1) {
5133
5145
  const r = this.log[c.at];
5134
5146
  let t = c.tasks.get(r.taskId);
@@ -5171,6 +5183,7 @@ var Driver = class {
5171
5183
  const at = list.indexOf(t.entry);
5172
5184
  list.splice(at, 1);
5173
5185
  orders.splice(at, 1);
5186
+ this.reindex(c, t.nodeId, t.entry, null);
5174
5187
  t.entry = null;
5175
5188
  if (list.length === 0) {
5176
5189
  c.byNode.delete(t.nodeId);
@@ -5181,10 +5194,12 @@ var Driver = class {
5181
5194
  const entry = { path: t.path, result: eff.result };
5182
5195
  if (t.entry !== null && list !== void 0) {
5183
5196
  list[list.indexOf(t.entry)] = entry;
5197
+ this.reindex(c, t.nodeId, t.entry, entry);
5184
5198
  t.entry = entry;
5185
5199
  return;
5186
5200
  }
5187
5201
  t.entry = entry;
5202
+ this.reindex(c, t.nodeId, null, entry);
5188
5203
  if (list === void 0 || orders === void 0) {
5189
5204
  c.byNode.set(t.nodeId, [entry]);
5190
5205
  c.orders.set(t.nodeId, [t.order]);
@@ -5195,6 +5210,81 @@ var Driver = class {
5195
5210
  list.splice(i, 0, entry);
5196
5211
  orders.splice(i, 0, t.order);
5197
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
+ }
5198
5288
  /**
5199
5289
  * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
5200
5290
  * it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
@@ -5225,7 +5315,7 @@ var Driver = class {
5225
5315
  compose(node, edgeValue, itemIndices, runPayload, item) {
5226
5316
  const needs = node.needs;
5227
5317
  if (!needs) return edgeValue;
5228
- let lookup = null;
5318
+ let here = null;
5229
5319
  const out = {};
5230
5320
  for (const [key, ref] of Object.entries(needs)) {
5231
5321
  const t = referenceTarget(ref);
@@ -5241,8 +5331,8 @@ var Driver = class {
5241
5331
  out[key] = cloneValue(item());
5242
5332
  continue;
5243
5333
  }
5244
- lookup ??= { here: this.pathOf(node.id, itemIndices), produced: this.outputs() };
5245
- 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");
5246
5336
  if (v !== ABSENT) out[key] = v;
5247
5337
  }
5248
5338
  return out;
@@ -5263,8 +5353,8 @@ var Driver = class {
5263
5353
  *
5264
5354
  * @throws UnresolvedReference — `FD-R3`.
5265
5355
  */
5266
- resolveReference(atNodeId, here, produced, key, t, site) {
5267
- 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);
5268
5358
  if (visible.length === 0 && t.conditional) {
5269
5359
  return t.form === "all" ? [] : ABSENT;
5270
5360
  }
@@ -5273,6 +5363,7 @@ var Driver = class {
5273
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.`
5274
5364
  );
5275
5365
  }
5366
+ this.outputsCache.examined += t.form === "all" ? visible.length : 1;
5276
5367
  return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
5277
5368
  }
5278
5369
  /**
@@ -5325,7 +5416,7 @@ var Driver = class {
5325
5416
  const needs = edge.needs;
5326
5417
  if (!needs) return EMPTY_NEEDS;
5327
5418
  const here = this.pathOf(node.id, itemIndices);
5328
- const produced = this.outputs();
5419
+ this.outputs();
5329
5420
  const out = {};
5330
5421
  for (const [key, ref] of Object.entries(needs)) {
5331
5422
  const t = referenceTarget(ref);
@@ -5340,7 +5431,7 @@ var Driver = class {
5340
5431
  if (t.form === "self") {
5341
5432
  throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
5342
5433
  }
5343
- const v = this.resolveReference(node.id, here, produced, key, t, "edge needs");
5434
+ const v = this.resolveReference(node.id, here, key, t, "edge needs");
5344
5435
  if (v !== ABSENT) out[key] = v;
5345
5436
  }
5346
5437
  return Object.freeze(out);
@@ -6556,6 +6647,7 @@ export {
6556
6647
  innermostMapOf,
6557
6648
  sharedRegionDepth,
6558
6649
  outputIsVisible,
6650
+ visibilityKey,
6559
6651
  branchIndexOf,
6560
6652
  certainlySettled,
6561
6653
  SCHEMA_KEYWORDS,
@@ -141,9 +141,10 @@ import {
141
141
  transformShapeProblem,
142
142
  validateDefinition,
143
143
  violatedRules,
144
+ visibilityKey,
144
145
  witness,
145
146
  workersOf
146
- } from "./chunks/chunk-3U4UZC34.js";
147
+ } from "./chunks/chunk-I3KDEOLB.js";
147
148
 
148
149
  // src/definition/assist.ts
149
150
  var TYPES = ["object", "array", "string", "number", "integer", "boolean", "null"];
@@ -440,6 +441,7 @@ export {
440
441
  tsTypeOf,
441
442
  validateDefinition,
442
443
  violatedRules,
444
+ visibilityKey,
443
445
  witness,
444
446
  workersOf
445
447
  };
@@ -25,7 +25,7 @@ import {
25
25
  retryPolicyProblem,
26
26
  validateDefinition,
27
27
  workersOf
28
- } from "../chunks/chunk-3U4UZC34.js";
28
+ } from "../chunks/chunk-I3KDEOLB.js";
29
29
 
30
30
  // src/scheduler/bootstrap.ts
31
31
  import { WorkerManager } from "fyflow-scheduler";
@@ -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);
@@ -5128,7 +5140,7 @@ var Driver = class {
5128
5140
  * node or an edge that declares a `needs` asks, and a run that declares none never builds it.
5129
5141
  */
5130
5142
  outputs() {
5131
- const c = this.outputsCache ??= { at: 0, byNode: /* @__PURE__ */ new Map(), tasks: /* @__PURE__ */ new Map(), orders: /* @__PURE__ */ new Map() };
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 };
5132
5144
  for (; c.at < this.log.length; c.at += 1) {
5133
5145
  const r = this.log[c.at];
5134
5146
  let t = c.tasks.get(r.taskId);
@@ -5171,6 +5183,7 @@ var Driver = class {
5171
5183
  const at = list.indexOf(t.entry);
5172
5184
  list.splice(at, 1);
5173
5185
  orders.splice(at, 1);
5186
+ this.reindex(c, t.nodeId, t.entry, null);
5174
5187
  t.entry = null;
5175
5188
  if (list.length === 0) {
5176
5189
  c.byNode.delete(t.nodeId);
@@ -5181,10 +5194,12 @@ var Driver = class {
5181
5194
  const entry = { path: t.path, result: eff.result };
5182
5195
  if (t.entry !== null && list !== void 0) {
5183
5196
  list[list.indexOf(t.entry)] = entry;
5197
+ this.reindex(c, t.nodeId, t.entry, entry);
5184
5198
  t.entry = entry;
5185
5199
  return;
5186
5200
  }
5187
5201
  t.entry = entry;
5202
+ this.reindex(c, t.nodeId, null, entry);
5188
5203
  if (list === void 0 || orders === void 0) {
5189
5204
  c.byNode.set(t.nodeId, [entry]);
5190
5205
  c.orders.set(t.nodeId, [t.order]);
@@ -5195,6 +5210,81 @@ var Driver = class {
5195
5210
  list.splice(i, 0, entry);
5196
5211
  orders.splice(i, 0, t.order);
5197
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
+ }
5198
5288
  /**
5199
5289
  * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
5200
5290
  * it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
@@ -5225,7 +5315,7 @@ var Driver = class {
5225
5315
  compose(node, edgeValue, itemIndices, runPayload, item) {
5226
5316
  const needs = node.needs;
5227
5317
  if (!needs) return edgeValue;
5228
- let lookup = null;
5318
+ let here = null;
5229
5319
  const out = {};
5230
5320
  for (const [key, ref] of Object.entries(needs)) {
5231
5321
  const t = referenceTarget(ref);
@@ -5241,8 +5331,8 @@ var Driver = class {
5241
5331
  out[key] = cloneValue(item());
5242
5332
  continue;
5243
5333
  }
5244
- lookup ??= { here: this.pathOf(node.id, itemIndices), produced: this.outputs() };
5245
- 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");
5246
5336
  if (v !== ABSENT) out[key] = v;
5247
5337
  }
5248
5338
  return out;
@@ -5263,8 +5353,8 @@ var Driver = class {
5263
5353
  *
5264
5354
  * @throws UnresolvedReference — `FD-R3`.
5265
5355
  */
5266
- resolveReference(atNodeId, here, produced, key, t, site) {
5267
- 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);
5268
5358
  if (visible.length === 0 && t.conditional) {
5269
5359
  return t.form === "all" ? [] : ABSENT;
5270
5360
  }
@@ -5273,6 +5363,7 @@ var Driver = class {
5273
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.`
5274
5364
  );
5275
5365
  }
5366
+ this.outputsCache.examined += t.form === "all" ? visible.length : 1;
5276
5367
  return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
5277
5368
  }
5278
5369
  /**
@@ -5325,7 +5416,7 @@ var Driver = class {
5325
5416
  const needs = edge.needs;
5326
5417
  if (!needs) return EMPTY_NEEDS;
5327
5418
  const here = this.pathOf(node.id, itemIndices);
5328
- const produced = this.outputs();
5419
+ this.outputs();
5329
5420
  const out = {};
5330
5421
  for (const [key, ref] of Object.entries(needs)) {
5331
5422
  const t = referenceTarget(ref);
@@ -5340,7 +5431,7 @@ var Driver = class {
5340
5431
  if (t.form === "self") {
5341
5432
  throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
5342
5433
  }
5343
- const v = this.resolveReference(node.id, here, produced, key, t, "edge needs");
5434
+ const v = this.resolveReference(node.id, here, key, t, "edge needs");
5344
5435
  if (v !== ABSENT) out[key] = v;
5345
5436
  }
5346
5437
  return Object.freeze(out);
@@ -6556,6 +6647,7 @@ export {
6556
6647
  innermostMapOf,
6557
6648
  sharedRegionDepth,
6558
6649
  outputIsVisible,
6650
+ visibilityKey,
6559
6651
  branchIndexOf,
6560
6652
  certainlySettled,
6561
6653
  SCHEMA_KEYWORDS,
@@ -141,9 +141,10 @@ import {
141
141
  transformShapeProblem,
142
142
  validateDefinition,
143
143
  violatedRules,
144
+ visibilityKey,
144
145
  witness,
145
146
  workersOf
146
- } from "./chunks/chunk-3U4UZC34.js";
147
+ } from "./chunks/chunk-I3KDEOLB.js";
147
148
 
148
149
  // src/definition/assist.ts
149
150
  var TYPES = ["object", "array", "string", "number", "integer", "boolean", "null"];
@@ -440,6 +441,7 @@ export {
440
441
  tsTypeOf,
441
442
  validateDefinition,
442
443
  violatedRules,
444
+ visibilityKey,
443
445
  witness,
444
446
  workersOf
445
447
  };
@@ -25,7 +25,7 @@ import {
25
25
  retryPolicyProblem,
26
26
  validateDefinition,
27
27
  workersOf
28
- } from "../chunks/chunk-3U4UZC34.js";
28
+ } from "../chunks/chunk-I3KDEOLB.js";
29
29
 
30
30
  // src/scheduler/bootstrap.ts
31
31
  import { WorkerManager } from "fyflow-scheduler";
@@ -309,6 +309,18 @@ export declare function sharedRegionDepth(regions: RegionSets[], a: string, b: s
309
309
  * own, so for an edge's `needs` this is the node the edge hangs on (`FD-C3`).
310
310
  */
311
311
  export declare function outputIsVisible(regions: RegionSets[], readerId: string, readerPath: string, targetId: string, targetPath: string): boolean;
312
+ /**
313
+ * **The key an output is filed under for a reader that shares `depth` regions with it** — the first `depth` SEGMENTS of
314
+ * its branch path, joined, or `null` for a path with fewer (`-` has none) — W199,
315
+ * [ADR 0124](../../doc/decisions/0124-a-reference-read-costs-its-answer.md). An output is visible to a reader exactly
316
+ * when the two have the SAME key at their shared depth ({@link outputIsVisible}), so the driver files a node's outputs
317
+ * under it once and a read takes its group, where it searched every output (Q735).
318
+ *
319
+ * **Segment-wise and never by string** (ADR 0044 rule 3): a segment holds no `.`, so two keys of `depth` segments are
320
+ * equal exactly when their segments are — `1` and `10` stay distinct, and `1.0` at depth 1 is `1`. At depth 0 every path
321
+ * has the one key `''`.
322
+ */
323
+ export declare function visibilityKey(path: string, depth: number): string | null;
312
324
  /**
313
325
  * Which region a node belongs to, and via which branch — the value the task id's
314
326
  * `branchPath` segment is derived from (`spec/task-id.md` section 1).
@@ -1749,6 +1749,31 @@ export declare class Driver {
1749
1749
  * none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
1750
1750
  */
1751
1751
  private place;
1752
+ /**
1753
+ * **One entry's change carried into every index built for its node** — W199, ADR 0124: called by `place` at each of
1754
+ * its cases (a first entry, an append, an insertion at a first-record position, a replacement in place, a removal), so
1755
+ * the index is caught up exactly when `byNode` is and can say nothing `byNode` does not. `was` is the entry leaving
1756
+ * (`null` for an insertion), `now` the one arriving (`null` for a removal); a replacement keeps its path, so its
1757
+ * position. A group is ordered by PATH, not by first record, so where `byNode` inserted mid-list the group finds its own
1758
+ * place by a binary search. An entry with fewer than `depth` segments is visible to no reader at that depth and is filed
1759
+ * nowhere.
1760
+ */
1761
+ private reindex;
1762
+ /**
1763
+ * **A node's outputs grouped for readers at `depth`** — built on the first read at that depth from the node's current
1764
+ * entries (each one examined), each group sorted ONCE, and from then on kept by `reindex`.
1765
+ */
1766
+ private groupsOf;
1767
+ /** The depth `reader` and `target` share — `sharedRegionDepth`, a graph fact, asked once per pair. */
1768
+ private depthOf;
1769
+ /**
1770
+ * **The outputs of `nodeId` visible to `atNodeId` at `here`, in `compareBranchPaths` order** — W199, ADR 0124: ADR
1771
+ * 0044's predicate answered by a LOOKUP. The predicate is `visibilityKey(here, d) === visibilityKey(path, d)` at the depth
1772
+ * `d` the two share (`outputIsVisible`), so the visible outputs are exactly the group filed under the reader's key: what
1773
+ * the filter over every output and the sort of its survivors returned until W199, found in the cost of its answer.
1774
+ * **The index's own list, never handed out** — the caller maps it to copies.
1775
+ */
1776
+ private visibleOutputs;
1752
1777
  /**
1753
1778
  * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
1754
1779
  * it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fyflow-orchestration",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Flow orchestration over fyflow-scheduler: sequence, decision, fan-out and join",
5
5
  "keywords": [
6
6
  "workflow",