fyflow-orchestration 0.9.1 → 0.10.0

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.
@@ -2764,6 +2764,74 @@ function isTerminalEvent(e) {
2764
2764
  return TERMINAL_EVENTS.includes(e.type);
2765
2765
  }
2766
2766
 
2767
+ // src/fold/policies.ts
2768
+ var arrivalCount = {
2769
+ id: "arrival-count",
2770
+ // **`region` is deliberately unread.** It was the source of `need` and is now
2771
+ // the source of nothing: `branches` cannot answer *how many branches did this
2772
+ // region open* for a map, and a policy that read it for a fanout and counted for
2773
+ // a map would be branching on the node kind — which it cannot see
2774
+ // ([Q159](../../doc/questions/q159.md)) and, per that row, should not.
2775
+ open: () => ({ need: 0, arrived: [] }),
2776
+ step: (s, e) => {
2777
+ if (e.type === "branch.opened") return { ...s, need: s.need + 1 };
2778
+ if (e.type !== "branch.arrived" || s.arrived.includes(e.branchPath)) return s;
2779
+ return { ...s, arrived: [...s.arrived, e.branchPath] };
2780
+ },
2781
+ // **`s.need > 0` is a PIN, not a refinement**, and it is exactly `JP-R8`'s own
2782
+ // *"having been positive"* argument in the policy that argument was denied to.
2783
+ // A bare `arrived.length >= need` is true at `0 >= 0`, so without it `open`
2784
+ // itself reports `satisfied` — and at a map of zero items this policy would fire
2785
+ // a join **no branch ever reached**, which is character for character what
2786
+ // [ADR 0025](../../doc/decisions/0025-the-zero-arrival-join-is-reportable-and-arrival-counting-is-not-repaired.md)
2787
+ // REFUSED `JP-R7`'s variant for. It is vacuous on every `fanout` region, because
2788
+ // `FD-V9`'s floor puts `need` at 2 or more.
2789
+ status: (s) => s.need > 0 && s.arrived.length >= s.need ? "satisfied" : "waiting",
2790
+ // Never `unsatisfiable`: this policy has no way to know a join can never fire,
2791
+ // which is precisely the argument Q4 states as a testable claim. **At a map of
2792
+ // zero items it says `waiting`, exactly as the other two do** — the region is
2793
+ // reported dead by the FOLD (`RN-R10`, via the `regionEntered` input) rather
2794
+ // than by any policy, which is `spec/join-policy.md` section 5's own case.
2795
+ describe: (s) => ({ need: s.need, arrived: [...s.arrived] })
2796
+ };
2797
+ var liveToken = {
2798
+ id: "live-token",
2799
+ open: () => ({ live: 0, positive: false }),
2800
+ step: (s, e) => {
2801
+ if (e.type === "branch.opened") return { live: s.live + 1, positive: true };
2802
+ if (e.type === "branch.arrived" || e.type === "branch.exited" || e.type === "branch.lost") {
2803
+ return { ...s, live: s.live - 1 };
2804
+ }
2805
+ return s;
2806
+ },
2807
+ status: (s) => s.positive && s.live === 0 ? "satisfied" : "waiting",
2808
+ // NOT requiring an arrival before reporting `satisfied`, and NOT returning
2809
+ // `unsatisfiable` on zero-with-no-arrivals. That is JP-R8's third policy, and
2810
+ // it is the one that would hand live-token a graceful answer arrival-count is
2811
+ // structurally denied — which is the asymmetry JP-R8 exists to forbid.
2812
+ describe: (s) => ({ live: s.live, positive: s.positive })
2813
+ };
2814
+ var liveTokenRequiringArrival = {
2815
+ id: "live-token-requiring-arrival",
2816
+ open: () => ({ live: 0, positive: false, arrived: 0 }),
2817
+ step: (s, e) => {
2818
+ if (e.type === "branch.opened") return { ...s, live: s.live + 1, positive: true };
2819
+ if (e.type === "branch.arrived") return { ...s, live: s.live - 1, arrived: s.arrived + 1 };
2820
+ if (e.type === "branch.exited" || e.type === "branch.lost") return { ...s, live: s.live - 1 };
2821
+ return s;
2822
+ },
2823
+ status: (s) => {
2824
+ if (!s.positive || s.live !== 0) return "waiting";
2825
+ return s.arrived > 0 ? "satisfied" : "unsatisfiable";
2826
+ },
2827
+ describe: (s) => ({ live: s.live, positive: s.positive, arrived: s.arrived })
2828
+ };
2829
+ var POLICIES = {
2830
+ "arrival-count": arrivalCount,
2831
+ "live-token": liveToken,
2832
+ "live-token-requiring-arrival": liveTokenRequiringArrival
2833
+ };
2834
+
2767
2835
  // src/driver/driver.ts
2768
2836
  import { FyflowTask } from "fyflow-scheduler";
2769
2837
  var InvalidDefinition = class _InvalidDefinition extends Error {
@@ -2879,6 +2947,12 @@ var ResumeRefused = class extends Error {
2879
2947
  }
2880
2948
  };
2881
2949
  function resumeRefusal(definition, checkpoint, runId) {
2950
+ const header = headerRefusal(definition, checkpoint, runId);
2951
+ if (header !== null) return header;
2952
+ const cp = checkpoint;
2953
+ return haltRefusal(cp.events);
2954
+ }
2955
+ function headerRefusal(definition, checkpoint, runId) {
2882
2956
  const cp = checkpoint;
2883
2957
  const h = typeof cp === "object" && cp !== null ? cp.header : void 0;
2884
2958
  if (typeof h !== "object" || h === null || !Array.isArray(cp.events)) {
@@ -2916,24 +2990,27 @@ function resumeRefusal(definition, checkpoint, runId) {
2916
2990
  `ResumeRefused (flow): the checkpoint is of flow ${JSON.stringify(h.flowId)} version ${JSON.stringify(h.version)}, and this driver runs "${definition.flowId}" version ${definition.version}. There is no migration (P23 decision 7): a checkpoint resumes against the definition that produced it, and a changed flow is a new version.`
2917
2991
  );
2918
2992
  }
2919
- const at = cp.events.findIndex((e) => {
2993
+ return null;
2994
+ }
2995
+ function haltRefusal(events) {
2996
+ const at = events.findIndex((e) => {
2920
2997
  const k = e?.kind;
2921
2998
  return k === "halted" || k === "stopped";
2922
2999
  });
2923
3000
  if (at >= 0) {
2924
- const e = cp.events[at];
3001
+ const e = events[at];
2925
3002
  return new ResumeRefused(
2926
3003
  "halted",
2927
3004
  at,
2928
3005
  `ResumeRefused (halted): the stored run was HALTED by a refusal \u2014 its element at position ${at} is \`${String(e.kind)}\`${e.kind === "stopped" ? " (the name 0.7.0 wrote for `halted`, ADR 0102)" : ""} (${JSON.stringify(e.reason)}). A halted run is not resumable (P23 decision 10): the refusal that halted it would halt it again, and the one class whose cause never reached the log \u2014 a result OR-R6 refused \u2014 cannot be regenerated at all.`
2929
3006
  );
2930
3007
  }
2931
- const c = cp.events.findIndex((e) => {
3008
+ const c = events.findIndex((e) => {
2932
3009
  const x = e;
2933
3010
  return x?.kind === "cancelRequested" || x?.kind === "outcome" && x.record?.terminalState === "cancelled";
2934
3011
  });
2935
3012
  if (c >= 0) {
2936
- const e = cp.events[c];
3013
+ const e = events[c];
2937
3014
  const what = e?.kind === "cancelRequested" ? `the request of ${JSON.stringify(e.actor)} to cancel it (\`cancelRequested\`)` : `the settlement of ${JSON.stringify(e?.record?.taskId)} as \`cancelled\``;
2938
3015
  return new ResumeRefused(
2939
3016
  "cancelled",
@@ -2993,6 +3070,25 @@ function describeElement(e) {
2993
3070
  }
2994
3071
  return s.length > 160 ? `${s.slice(0, 157)}...` : s;
2995
3072
  }
3073
+ function deepFreeze(root) {
3074
+ const stack = [root];
3075
+ while (stack.length > 0) {
3076
+ const v = stack.pop();
3077
+ if (v === null || typeof v !== "object" || ArrayBuffer.isView(v) || v instanceof RegExp || Object.isFrozen(v)) continue;
3078
+ Object.freeze(v);
3079
+ const proto = Object.getPrototypeOf(v);
3080
+ if (!Array.isArray(v) && proto !== Object.prototype && proto !== null) continue;
3081
+ for (const k of Object.keys(v)) {
3082
+ const x = v[k];
3083
+ if (x !== null && typeof x === "object") stack.push(x);
3084
+ }
3085
+ }
3086
+ }
3087
+ function sharedCopy(v) {
3088
+ const copy = cloneValue(v);
3089
+ deepFreeze(copy);
3090
+ return copy;
3091
+ }
2996
3092
  var NO_OUTPUTS = Object.freeze([]);
2997
3093
  function groupPosition(group, path) {
2998
3094
  let lo = 0;
@@ -3005,6 +3101,15 @@ function groupPosition(group, path) {
3005
3101
  }
3006
3102
  return lo;
3007
3103
  }
3104
+ var InputCut = class extends Error {
3105
+ };
3106
+ var DEAF_SCHEDULER = {
3107
+ addTask() {
3108
+ throw new Error("a reading hands nothing to a scheduler (RN-R45, ADR 0128) \u2014 this is a defect in the driver");
3109
+ },
3110
+ addEventListener() {
3111
+ }
3112
+ };
3008
3113
  var UndispatchedTask = class extends Error {
3009
3114
  constructor(message, options) {
3010
3115
  super(message, options);
@@ -3144,7 +3249,7 @@ var Fifo = class {
3144
3249
  for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
3145
3250
  }
3146
3251
  };
3147
- var Driver = class {
3252
+ var Driver = class _Driver {
3148
3253
  /**
3149
3254
  * `OR-R7` — nothing outside the inbound API writes to run state. Private, and
3150
3255
  * `snapshot()` is the only way out: a public `readonly` array still accepts
@@ -3276,7 +3381,8 @@ var Driver = class {
3276
3381
  * - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
3277
3382
  * by a run and by a resume's replay alike, so a replay REBUILDS it (`RN-R19`; P38's brief: *taken at rest*, never by
3278
3383
  * a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
3279
- * `needs` composition's per-key copy), and as its OWN copy only where `kept` says that element is the caller's
3384
+ * `needs` composition's value — since W202 an element of a reference's SHARED copy, which this keeps alive while held),
3385
+ * and as its OWN copy only where `kept` says that element is the caller's
3280
3386
  * (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
3281
3387
  * **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
3282
3388
  * `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
@@ -3698,7 +3804,7 @@ var Driver = class {
3698
3804
  const worker = type?.worker === void 0 ? void 0 : this.workers.get(type.worker);
3699
3805
  const schema = worker?.inputSchema;
3700
3806
  if (schema === void 0) continue;
3701
- const received = this.compose(n, value, EMPTY_INDICES, payload, () => value);
3807
+ const received = this.compose(n, value, EMPTY_INDICES, () => cloneValue(payload), () => value);
3702
3808
  const fs = findings(schema, received);
3703
3809
  if (fs.length) {
3704
3810
  throw new BoundaryRefused(
@@ -3795,7 +3901,7 @@ var Driver = class {
3795
3901
  const entryPayload = structuredClone(checkpoint.header.entryPayload);
3796
3902
  this.header = { format: CHECKPOINT_FORMAT, runId: this.runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
3797
3903
  this.started = true;
3798
- const replay = { stored, feeding: -1, held: /* @__PURE__ */ new Map() };
3904
+ const replay = { stored, feeding: -1, held: /* @__PURE__ */ new Map(), reading: null };
3799
3905
  this.replay = replay;
3800
3906
  try {
3801
3907
  this.replayStored(stored);
@@ -3860,6 +3966,7 @@ var Driver = class {
3860
3966
  if (!isFed(e)) continue;
3861
3967
  if (this.inputs.length !== i) throw this.diverged(this.inputs.length, "the replay produced nothing there");
3862
3968
  replay.feeding = i;
3969
+ if (replay.reading !== null) replay.reading.fed += 1;
3863
3970
  this.feed(() => this.feedOne(e, i));
3864
3971
  }
3865
3972
  if (this.inputs.length < stored.length) throw this.diverged(this.inputs.length, "the replay ended before it");
@@ -3913,12 +4020,15 @@ var Driver = class {
3913
4020
  this.appendResolution(cloneRecord(record), nodeId, branchPath);
3914
4021
  this.advance(runId, nodeId, branchPath);
3915
4022
  }
3916
- /** The listener's boundary, for the replay: a refusal routing raises stops the run (`OR-R10`); a refused resume passes through. */
4023
+ /**
4024
+ * The listener's boundary, for the replay: a refusal routing raises stops the run (`OR-R10`); a refused resume passes
4025
+ * through — and so does a reading's cut (`InputCut`, W205), which is no refusal and halts nothing.
4026
+ */
3917
4027
  feed(step2) {
3918
4028
  try {
3919
4029
  step2();
3920
4030
  } catch (e) {
3921
- if (e instanceof ResumeRefused) throw e;
4031
+ if (e instanceof ResumeRefused || e instanceof InputCut) throw e;
3922
4032
  this.failRun(e);
3923
4033
  }
3924
4034
  }
@@ -3931,6 +4041,119 @@ var Driver = class {
3931
4041
  `ResumeRefused (diverged): replaying run "${this.runId}" does not reproduce the stored element at position ${at} \u2014 stored ${describeElement(stored[at])}, replayed ${describeElement(this.inputs[at])} (${why}). P23 decision 9: a stored run the replay cannot regenerate \u2014 a predicate, a definition or an entry payload that is not what produced it \u2014 is refused rather than resumed on a different road. Nothing was handed to the scheduler.`
3932
4042
  );
3933
4043
  }
4044
+ /**
4045
+ * **A cut site offers what a node was handed** (`RN-R45`, W205). An offer for another path is dropped. `own` is the
4046
+ * position of the node's OWN element where it is already appended — a task's `dispatched`, a join's firing — and `null`
4047
+ * where it is the NEXT append, which `append` confirms (`Reading.pending`). Either way it is TAKEN only at a STORED
4048
+ * position, so a composition whose element lies past the prefix the reading verifies is never answered.
4049
+ */
4050
+ offer(r, input, own3) {
4051
+ if (r.branchPath !== null && input.branchPath !== r.branchPath) return;
4052
+ const stored = this.replay.stored.length;
4053
+ if (own3 === null) {
4054
+ if (this.inputs.length < stored) r.pending = { at: this.inputs.length, input };
4055
+ return;
4056
+ }
4057
+ if (own3 < stored) this.take(r, input);
4058
+ }
4059
+ /** **An offer taken** — kept, and for a target with a path the replay ends HERE: nothing after it is replayed (`InputCut`). */
4060
+ take(r, input) {
4061
+ r.found.push(input);
4062
+ if (r.branchPath !== null) throw new InputCut();
4063
+ }
4064
+ /**
4065
+ * **A READING — what a node was handed, by replaying the run to that node's composition** — `RN-R45`, W205,
4066
+ * [ADR 0128](../../doc/decisions/0128-what-a-node-was-handed-is-read-by-replaying-the-run-to-its-composition.md), P42
4067
+ * decisions 1-7. The export {@link inputOf} and `RunHandle.inputOf` are this and nothing else. In order:
4068
+ *
4069
+ * 1. **The header**, refused by `resumeRefusal`'s own header half (`format`, `flow`) — never its halt half.
4070
+ * 2. **The target**, refused `unknown` where it cannot name anything in this run.
4071
+ * 3. **A driver built exactly as `runtime.resume` builds one** (`RN-R25`) — the definition, the registry's members, a
4072
+ * `joinPolicy` defaulting to `liveToken` — with NO callback and a scheduler that is handed nothing (`DEAF_SCHEDULER`).
4073
+ * 4. **The replay** (`readStep`) over the stored PREFIX it can verify, the cut armed: it stops at the target's
4074
+ * composition and returns what it found.
4075
+ *
4076
+ * Private and STATIC, so the export and the handle reach one road, and a test reaches the reading's own driver — its
4077
+ * count — by a cast (ADR 0121 rule 7): `driver` is `null` where a refusal came before one was built.
4078
+ */
4079
+ static read(flow, options, run, target) {
4080
+ const refused = headerRefusal(flow, run);
4081
+ if (refused !== null) {
4082
+ return { answer: { ok: false, reason: refused.reason, index: null, message: `a reading refuses this header as a resume does \u2014 ${refused.message}` }, driver: null };
4083
+ }
4084
+ const header = run.header;
4085
+ try {
4086
+ assertRunId(header.runId);
4087
+ } catch (e) {
4088
+ return { answer: { ok: false, reason: "format", index: null, message: `the stored header's runId is not a run id (ID-R2): ${e instanceof Error ? e.message : String(e)}` }, driver: null };
4089
+ }
4090
+ const t = readTarget(flow, header.runId, target);
4091
+ if ("ok" in t) return { answer: t, driver: null };
4092
+ const { predicates, workers, nodeTypes, pools, groups, languages, gateTypes, joinPolicy, defaultRetryPolicy, validateBoundaries } = options;
4093
+ const driver = new _Driver(flow, {
4094
+ runId: header.runId,
4095
+ scheduler: DEAF_SCHEDULER,
4096
+ predicates,
4097
+ ...workers !== void 0 ? { workers } : {},
4098
+ ...nodeTypes !== void 0 ? { nodeTypes } : {},
4099
+ ...pools !== void 0 ? { pools } : {},
4100
+ ...groups !== void 0 ? { groups } : {},
4101
+ ...languages !== void 0 ? { languages } : {},
4102
+ ...gateTypes !== void 0 ? { gateTypes } : {},
4103
+ joinPolicy: joinPolicy ?? liveToken,
4104
+ ...defaultRetryPolicy !== void 0 ? { defaultRetryPolicy } : {},
4105
+ ...validateBoundaries !== void 0 ? { validateBoundaries } : {}
4106
+ });
4107
+ return { answer: driver.readStep(run, t), driver };
4108
+ }
4109
+ /**
4110
+ * **The reading's replay** — `resume()`'s first half (`resumeStep` to `replayStored`) with the cut armed, and nothing of
4111
+ * its second: no in-flight set, no `resumed`, nothing handed on. **The prefix it verifies ENDS at the first `halted`,
4112
+ * `stopped` or `cancelRequested` element** (P42 decision 5): after one a halted driver composes nothing more, whether the
4113
+ * replay regenerates it (a predicate's throw) or cannot (a consumer's callback, a refused result, an owner's call) —
4114
+ * what follows is `not-reached`, never `diverged` (P42 retro (a1) corrected the reason). Everything
4115
+ * before is compared as `RN-R20` compares it.
4116
+ */
4117
+ readStep(run, t) {
4118
+ const events = run.events;
4119
+ let end = events.length;
4120
+ for (let i = 0; i < events.length; i += 1) {
4121
+ const e = events[i];
4122
+ if (typeof e !== "object" || e === null) {
4123
+ return { ok: false, reason: "format", index: null, message: `the stored element at position ${i} is ${JSON.stringify(e) ?? String(e)}, which is not a fold input (RN-R14, RN-R17)` };
4124
+ }
4125
+ const k = e.kind;
4126
+ if (k === "halted" || k === "stopped" || k === "cancelRequested") {
4127
+ end = i;
4128
+ break;
4129
+ }
4130
+ }
4131
+ let entryPayload;
4132
+ try {
4133
+ entryPayload = structuredClone(run.header.entryPayload);
4134
+ } catch (e) {
4135
+ return { ok: false, reason: "format", index: null, message: `the stored header's entry payload does not clone (RN-R15, RN-R17): ${e instanceof Error ? e.message : String(e)}` };
4136
+ }
4137
+ this.header = { format: CHECKPOINT_FORMAT, runId: this.runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
4138
+ this.started = true;
4139
+ const reading = { nodeId: t.nodeId, branchPath: t.branchPath, pending: null, found: [], fed: 0, elements: 0 };
4140
+ this.replay = { stored: end === events.length ? events : events.slice(0, end), feeding: -1, held: /* @__PURE__ */ new Map(), reading };
4141
+ try {
4142
+ this.replayStored(this.replay.stored);
4143
+ } catch (e) {
4144
+ if (e instanceof InputCut) return { ok: true, inputs: [...reading.found] };
4145
+ if (e instanceof ResumeRefused) return { ok: false, reason: "diverged", index: e.index, message: e.message };
4146
+ throw e;
4147
+ }
4148
+ if (reading.found.length > 0) return { ok: true, inputs: [...reading.found] };
4149
+ const where = t.branchPath === null ? `node "${t.nodeId}" on any branch path` : `node "${t.nodeId}" at branch path "${t.branchPath}"`;
4150
+ return {
4151
+ ok: false,
4152
+ reason: "not-reached",
4153
+ index: end,
4154
+ message: `the replay handed ${where} nothing within the ${end} stored element(s) a reading verifies` + (end < events.length ? ` \u2014 the prefix ends at position ${end}, \`${String(events[end].kind)}\`, after which a halted driver composes nothing more (RN-R45)` : "") + "."
4155
+ };
4156
+ }
3934
4157
  /**
3935
4158
  * **What is IN FLIGHT: dispatched, and no settlement in the log** — `RN-R21`, in dispatch order. A
3936
4159
  * `user_action` settlement IS a settlement: a parked task is waiting on a person, not on a worker, and
@@ -4804,7 +5027,12 @@ var Driver = class {
4804
5027
  if (this.shut()) return;
4805
5028
  const node = this.graph.byId.get(nodeId);
4806
5029
  if (!node) return;
4807
- const received = this.compose(node, payload, itemIndices, this.runPayload(), () => this.itemOf(node.id, itemIndices));
5030
+ const received = this.compose(node, payload, itemIndices, () => this.sharedPayload(), () => this.itemOf(node.id, itemIndices));
5031
+ const reading = this.replay?.reading ?? null;
5032
+ if (reading !== null && node.kind !== "task" && node.id === reading.nodeId) {
5033
+ const path = node.kind === "join" || node.kind === "end" ? fromBranch : this.pathOf(node.id, itemIndices);
5034
+ this.offer(reading, { nodeId: node.id, kind: node.kind, branchPath: path, handed: node.kind === "join" ? "arrival" : "received", value: received }, null);
5035
+ }
4808
5036
  switch (node.kind) {
4809
5037
  case "task":
4810
5038
  return this.dispatch(runId, node, received, itemIndices);
@@ -5112,7 +5340,8 @@ var Driver = class {
5112
5340
  * described **today** before this method existed, and this adds **no retention**.
5113
5341
  * *(Q19 is RESOLVED since W53 —
5114
5342
  * [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
5115
- * — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
5343
+ * — and `RN-R13` names `outputsCache` by id: it holds results **by reference** — and since W202 (ADR 0126) one shared
5344
+ * COPY per output a reference has read, the one thing in it that is not the log's object,
5116
5345
  * asserted by IDENTITY at ladder L2 rather than by a byte count, because a
5117
5346
  * reachability SET cannot tell an alias from a copy. W53's readers found that
5118
5347
  * from both ends — the rule's stated test returned a third name on a `needs`
@@ -5140,7 +5369,7 @@ var Driver = class {
5140
5369
  * node or an edge that declares a `needs` asks, and a run that declares none never builds it.
5141
5370
  */
5142
5371
  outputs() {
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 };
5372
+ const c = this.cache();
5144
5373
  for (; c.at < this.log.length; c.at += 1) {
5145
5374
  const r = this.log[c.at];
5146
5375
  let t = c.tasks.get(r.taskId);
@@ -5160,6 +5389,23 @@ var Driver = class {
5160
5389
  }
5161
5390
  return c.byNode;
5162
5391
  }
5392
+ /**
5393
+ * The projection, made empty on first need and NOT caught up — `outputs()` catches it up; a `"$payload"` read, which
5394
+ * reads no output, only needs somewhere to keep its shared copy (W202).
5395
+ */
5396
+ cache() {
5397
+ return this.outputsCache ??= {
5398
+ at: 0,
5399
+ byNode: /* @__PURE__ */ new Map(),
5400
+ tasks: /* @__PURE__ */ new Map(),
5401
+ orders: /* @__PURE__ */ new Map(),
5402
+ index: /* @__PURE__ */ new Map(),
5403
+ depths: /* @__PURE__ */ new Map(),
5404
+ examined: 0,
5405
+ payload: null,
5406
+ copies: 0
5407
+ };
5408
+ }
5163
5409
  /**
5164
5410
  * **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
5165
5411
  * was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
@@ -5174,6 +5420,10 @@ var Driver = class {
5174
5420
  * that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
5175
5421
  * so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
5176
5422
  * none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
5423
+ *
5424
+ * **Both a replacement and a removal let go of the entry OBJECT, and with it the entry's shared copy** (W202, ADR 0126,
5425
+ * `OutputEntry.shared`): a replaced output's next read makes a new copy of the new result, and a removed one's copy is
5426
+ * held nowhere — `RN-R13`'s v0.33 term, released with what it belongs to (ADR 0046).
5177
5427
  */
5178
5428
  place(c, t, eff) {
5179
5429
  const list = c.byNode.get(t.nodeId);
@@ -5288,7 +5538,8 @@ var Driver = class {
5288
5538
  /**
5289
5539
  * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
5290
5540
  * 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
5541
+ * not a copy of them; since W202 an entry a reference has read also holds its ONE shared copy (`OutputEntry.shared`,
5542
+ * `RN-R13` v0.33), and `payload` the run's payload's — `tasks` is one slot per task id
5292
5543
  * read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
5293
5544
  * node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
5294
5545
  * read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
@@ -5324,7 +5575,7 @@ var Driver = class {
5324
5575
  continue;
5325
5576
  }
5326
5577
  if (t.form === "payload") {
5327
- out[key] = cloneValue(runPayload);
5578
+ out[key] = runPayload();
5328
5579
  continue;
5329
5580
  }
5330
5581
  if (t.form === "item") {
@@ -5343,7 +5594,7 @@ var Driver = class {
5343
5594
  *
5344
5595
  * **Extracted by [W42](../../doc/work-items/w42-what-a-decision-sees.md) rather
5345
5596
  * than copied.** An EDGE's `needs` resolves by exactly these rules — same grammar,
5346
- * same visibility predicate, same order, same per-key copy — and the ONE difference
5597
+ * same visibility predicate, same order, same shared copy (since W202, ADR 0126) — and the ONE difference
5347
5598
  * is that an edge has no `"."` form, which is a property of the caller rather than
5348
5599
  * of this expression. *Two callers, one resolution, so a change to what a reference
5349
5600
  * means cannot mean two different things to a worker and to a predicate*, which is
@@ -5363,15 +5614,38 @@ var Driver = class {
5363
5614
  `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.`
5364
5615
  );
5365
5616
  }
5366
- this.outputsCache.examined += t.form === "all" ? visible.length : 1;
5367
- return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
5617
+ const c = this.outputsCache;
5618
+ c.examined += t.form === "all" ? visible.length : 1;
5619
+ return t.form === "all" ? visible.map((e) => this.sharedOf(c, e)) : this.sharedOf(c, visible[0]);
5620
+ }
5621
+ /** **An output entry's shared copy**, made at its first read and kept on the entry (`OutputEntry.shared`) — counted. */
5622
+ sharedOf(c, e) {
5623
+ if (!("shared" in e)) {
5624
+ e.shared = sharedCopy(e.result);
5625
+ c.copies += 1;
5626
+ }
5627
+ return e.shared;
5628
+ }
5629
+ /**
5630
+ * **`"$payload"`'s value in a run: the run's ONE shared copy** — W202, ADR 0126, P41 decision 2: `sharedCopy` of the
5631
+ * header's `entryPayload` (`runPayload()`), made at the first `"$payload"` read of the run — or of a resume, whose driver
5632
+ * is a new one — and handed to every reader at both sites. Counted once. `FD-R4` does not come here: it composes from
5633
+ * the caller's own value, before a header exists, and keeps its clone.
5634
+ */
5635
+ sharedPayload() {
5636
+ const c = this.cache();
5637
+ if (c.payload === null) {
5638
+ c.payload = { value: sharedCopy(this.runPayload()) };
5639
+ c.copies += 1;
5640
+ }
5641
+ return c.payload.value;
5368
5642
  }
5369
5643
  /**
5370
5644
  * **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
5371
5645
  * caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
5372
5646
  * reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
5373
- * header exists. Read only where a node or an edge composes the token — a run declaring none never clones it
5374
- * (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
5647
+ * header exists. Read only where a node or an edge composes the token — a run declaring none never copies it
5648
+ * (P38 decision 2); since v0.47 read ONCE, by `sharedPayload`, whose one shared copy every reader is handed. The header is set before the first `enter` in both doors, so `null` here is not a run.
5375
5649
  */
5376
5650
  runPayload() {
5377
5651
  return this.header?.entryPayload;
@@ -5407,8 +5681,8 @@ var Driver = class {
5407
5681
  * received anyway* is `ctx.outcome`, which it already has. So there is no
5408
5682
  * `edgeValue` parameter here and none is derivable — see the call site. **A
5409
5683
  * `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
5410
- * the NODE received but read from the run's header, `runPayload()` above, its own
5411
- * copy per key per read.
5684
+ * the NODE received but read from the run's header — since v0.47 the run's ONE shared
5685
+ * copy of it, `sharedPayload()` above, the object a worker's `"$payload"` key is handed (ADR 0126).
5412
5686
  *
5413
5687
  * @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
5414
5688
  */
@@ -5421,7 +5695,7 @@ var Driver = class {
5421
5695
  for (const [key, ref] of Object.entries(needs)) {
5422
5696
  const t = referenceTarget(ref);
5423
5697
  if (t.form === "payload") {
5424
- out[key] = cloneValue(this.runPayload());
5698
+ out[key] = this.sharedPayload();
5425
5699
  continue;
5426
5700
  }
5427
5701
  if (t.form === "item") {
@@ -5452,7 +5726,7 @@ var Driver = class {
5452
5726
  }
5453
5727
  this.dispatched.add(taskId);
5454
5728
  if (this.replay === null) this.unhanded.add(taskId);
5455
- this.append({ kind: "dispatched", taskId });
5729
+ const dispatchedAt = this.append({ kind: "dispatched", taskId });
5456
5730
  const type = this.nodeTypes.get(node.type);
5457
5731
  const retryPolicy = node.retryPolicy ?? type.retryPolicy ?? this.opts.defaultRetryPolicy;
5458
5732
  const config = {
@@ -5464,7 +5738,11 @@ var Driver = class {
5464
5738
  ...node.workerGroups !== void 0 ? { workerGroups: node.workerGroups } : {}
5465
5739
  };
5466
5740
  if (this.replay !== null) {
5467
- this.replay.held.set(taskId, config);
5741
+ const reading = this.replay.reading;
5742
+ if (reading === null) this.replay.held.set(taskId, config);
5743
+ else if (node.id === reading.nodeId) {
5744
+ this.offer(reading, { nodeId: node.id, kind: "task", branchPath: this.pathOf(node.id, itemIndices), handed: "envelope", value: config.payload }, dispatchedAt);
5745
+ }
5468
5746
  return;
5469
5747
  }
5470
5748
  if ((this.paused || this.pauseHeld.size > 0) && !this.shut()) {
@@ -5826,6 +6104,15 @@ var Driver = class {
5826
6104
  if (replay !== null && seq < replay.stored.length) {
5827
6105
  const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
5828
6106
  if (!same) throw this.diverged(seq, "a different element there");
6107
+ const r = replay.reading;
6108
+ if (r !== null) {
6109
+ r.elements += 1;
6110
+ if (r.pending !== null && r.pending.at === seq) {
6111
+ const taken = r.pending.input;
6112
+ r.pending = null;
6113
+ this.take(r, taken);
6114
+ }
6115
+ }
5829
6116
  }
5830
6117
  if (this.opts.onFoldInput && this.delivers(seq)) this.opts.onFoldInput(freezeInput(input), seq);
5831
6118
  return seq;
@@ -5909,7 +6196,7 @@ var Driver = class {
5909
6196
  return;
5910
6197
  }
5911
6198
  this.firedAdd(region.fanoutId, rp);
5912
- this.append({
6199
+ const firedAt = this.append({
5913
6200
  kind: "joinSettled",
5914
6201
  regionId: region.fanoutId,
5915
6202
  regionPath: rp,
@@ -5919,6 +6206,10 @@ var Driver = class {
5919
6206
  if (this.shut()) return;
5920
6207
  const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
5921
6208
  this.releaseArrivals(region.fanoutId, rp);
6209
+ const reading = this.replay?.reading ?? null;
6210
+ if (reading !== null && region.joinId === reading.nodeId) {
6211
+ this.offer(reading, { nodeId: region.joinId, kind: "join", branchPath: rp, handed: "collection", value: collected }, firedAt);
6212
+ }
5922
6213
  const join = this.graph.byId.get(region.joinId);
5923
6214
  if (!join || join.kind !== "join") return;
5924
6215
  const edge = join.edges?.[0];
@@ -6053,6 +6344,46 @@ var Driver = class {
6053
6344
  return this.effectiveOutcomeOf(taskId)?.terminalState ?? null;
6054
6345
  }
6055
6346
  };
6347
+ function readTarget(flow, runId, target) {
6348
+ const unknown = (why) => ({ ok: false, reason: "unknown", index: null, message: `the target names nothing this run can have: ${why} (RN-R45)` });
6349
+ const t = target;
6350
+ const nodes = flow.nodes;
6351
+ if (typeof t?.taskId === "string") {
6352
+ let parts;
6353
+ try {
6354
+ parts = parseTaskId(t.taskId);
6355
+ } catch (e) {
6356
+ return unknown(`task id ${JSON.stringify(t.taskId)} does not parse \u2014 ${e instanceof Error ? e.message : String(e)}`);
6357
+ }
6358
+ if (parts.runId !== runId) return unknown(`task id ${JSON.stringify(t.taskId)} is of run "${parts.runId}", and this run is "${runId}"`);
6359
+ if (parts.attempt !== 0) return unknown(`task id ${JSON.stringify(t.taskId)} carries attempt ${parts.attempt}, and a run mints attempt 0`);
6360
+ const node = nodes.find((n) => n.id === parts.nodeId);
6361
+ if (node === void 0 || node.kind !== "task" && node.kind !== "ask") {
6362
+ return unknown(`task id ${JSON.stringify(t.taskId)} names ${node === void 0 ? "no node of the definition" : `a ${node.kind}, which has no task id`}`);
6363
+ }
6364
+ return { nodeId: parts.nodeId, branchPath: parts.branchPath };
6365
+ }
6366
+ if (typeof t?.nodeId === "string") {
6367
+ const nodeId = t.nodeId;
6368
+ if (!nodes.some((n) => n.id === nodeId)) return unknown(`the definition has no node "${nodeId}"`);
6369
+ if (t.branchPath === void 0) return { nodeId, branchPath: null };
6370
+ const path = t.branchPath;
6371
+ let ok = typeof path === "string";
6372
+ if (ok) {
6373
+ try {
6374
+ parseTaskId(`${runId}:${nodeId}:${path}:0`);
6375
+ } catch {
6376
+ ok = false;
6377
+ }
6378
+ }
6379
+ if (!ok) return unknown(`branch path ${JSON.stringify(path) ?? String(path)} is not spec/task-id.md's grammar ("-", or dot-separated non-negative integers)`);
6380
+ return { nodeId, branchPath: path };
6381
+ }
6382
+ return unknown("a target is { taskId } or { nodeId, branchPath? }");
6383
+ }
6384
+ function inputOf(flow, options, run, target) {
6385
+ return Driver.read(flow, options, run, target).answer;
6386
+ }
6056
6387
 
6057
6388
  // src/fold/accumulator.ts
6058
6389
  var Suspended = class {
@@ -6564,74 +6895,6 @@ function nodeOf(s, taskId) {
6564
6895
  return s.nodes[taskId] ?? null;
6565
6896
  }
6566
6897
 
6567
- // src/fold/policies.ts
6568
- var arrivalCount = {
6569
- id: "arrival-count",
6570
- // **`region` is deliberately unread.** It was the source of `need` and is now
6571
- // the source of nothing: `branches` cannot answer *how many branches did this
6572
- // region open* for a map, and a policy that read it for a fanout and counted for
6573
- // a map would be branching on the node kind — which it cannot see
6574
- // ([Q159](../../doc/questions/q159.md)) and, per that row, should not.
6575
- open: () => ({ need: 0, arrived: [] }),
6576
- step: (s, e) => {
6577
- if (e.type === "branch.opened") return { ...s, need: s.need + 1 };
6578
- if (e.type !== "branch.arrived" || s.arrived.includes(e.branchPath)) return s;
6579
- return { ...s, arrived: [...s.arrived, e.branchPath] };
6580
- },
6581
- // **`s.need > 0` is a PIN, not a refinement**, and it is exactly `JP-R8`'s own
6582
- // *"having been positive"* argument in the policy that argument was denied to.
6583
- // A bare `arrived.length >= need` is true at `0 >= 0`, so without it `open`
6584
- // itself reports `satisfied` — and at a map of zero items this policy would fire
6585
- // a join **no branch ever reached**, which is character for character what
6586
- // [ADR 0025](../../doc/decisions/0025-the-zero-arrival-join-is-reportable-and-arrival-counting-is-not-repaired.md)
6587
- // REFUSED `JP-R7`'s variant for. It is vacuous on every `fanout` region, because
6588
- // `FD-V9`'s floor puts `need` at 2 or more.
6589
- status: (s) => s.need > 0 && s.arrived.length >= s.need ? "satisfied" : "waiting",
6590
- // Never `unsatisfiable`: this policy has no way to know a join can never fire,
6591
- // which is precisely the argument Q4 states as a testable claim. **At a map of
6592
- // zero items it says `waiting`, exactly as the other two do** — the region is
6593
- // reported dead by the FOLD (`RN-R10`, via the `regionEntered` input) rather
6594
- // than by any policy, which is `spec/join-policy.md` section 5's own case.
6595
- describe: (s) => ({ need: s.need, arrived: [...s.arrived] })
6596
- };
6597
- var liveToken = {
6598
- id: "live-token",
6599
- open: () => ({ live: 0, positive: false }),
6600
- step: (s, e) => {
6601
- if (e.type === "branch.opened") return { live: s.live + 1, positive: true };
6602
- if (e.type === "branch.arrived" || e.type === "branch.exited" || e.type === "branch.lost") {
6603
- return { ...s, live: s.live - 1 };
6604
- }
6605
- return s;
6606
- },
6607
- status: (s) => s.positive && s.live === 0 ? "satisfied" : "waiting",
6608
- // NOT requiring an arrival before reporting `satisfied`, and NOT returning
6609
- // `unsatisfiable` on zero-with-no-arrivals. That is JP-R8's third policy, and
6610
- // it is the one that would hand live-token a graceful answer arrival-count is
6611
- // structurally denied — which is the asymmetry JP-R8 exists to forbid.
6612
- describe: (s) => ({ live: s.live, positive: s.positive })
6613
- };
6614
- var liveTokenRequiringArrival = {
6615
- id: "live-token-requiring-arrival",
6616
- open: () => ({ live: 0, positive: false, arrived: 0 }),
6617
- step: (s, e) => {
6618
- if (e.type === "branch.opened") return { ...s, live: s.live + 1, positive: true };
6619
- if (e.type === "branch.arrived") return { ...s, live: s.live - 1, arrived: s.arrived + 1 };
6620
- if (e.type === "branch.exited" || e.type === "branch.lost") return { ...s, live: s.live - 1 };
6621
- return s;
6622
- },
6623
- status: (s) => {
6624
- if (!s.positive || s.live !== 0) return "waiting";
6625
- return s.arrived > 0 ? "satisfied" : "unsatisfiable";
6626
- },
6627
- describe: (s) => ({ live: s.live, positive: s.positive, arrived: s.arrived })
6628
- };
6629
- var POLICIES = {
6630
- "arrival-count": arrivalCount,
6631
- "live-token": liveToken,
6632
- "live-token-requiring-arrival": liveTokenRequiringArrival
6633
- };
6634
-
6635
6898
  export {
6636
6899
  PREDICATE_OPS,
6637
6900
  PREDICATE_ROOTS,
@@ -6753,6 +7016,10 @@ export {
6753
7016
  selectEdge,
6754
7017
  TERMINAL_EVENTS,
6755
7018
  isTerminalEvent,
7019
+ arrivalCount,
7020
+ liveToken,
7021
+ liveTokenRequiringArrival,
7022
+ POLICIES,
6756
7023
  InvalidDefinition,
6757
7024
  DuplicateTaskId,
6758
7025
  UnresolvedMapArity,
@@ -6768,15 +7035,12 @@ export {
6768
7035
  UndispatchedTask,
6769
7036
  MissingJoinPolicy,
6770
7037
  Driver,
7038
+ inputOf,
6771
7039
  Accumulator,
6772
7040
  emptyState,
6773
7041
  step,
6774
7042
  fold,
6775
7043
  foldLog,
6776
7044
  isTerminal,
6777
- nodeOf,
6778
- arrivalCount,
6779
- liveToken,
6780
- liveTokenRequiringArrival,
6781
- POLICIES
7045
+ nodeOf
6782
7046
  };