fyflow-orchestration 0.8.0 → 0.9.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.
@@ -143,7 +143,7 @@ import {
143
143
  violatedRules,
144
144
  witness,
145
145
  workersOf
146
- } from "./chunks/chunk-EUU44PHD.js";
146
+ } from "./chunks/chunk-3U4UZC34.js";
147
147
 
148
148
  // src/definition/assist.ts
149
149
  var TYPES = ["object", "array", "string", "number", "integer", "boolean", "null"];
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Accumulator,
2
3
  BUILT_IN_NODE_TYPES,
3
4
  ConfigRefused,
4
5
  Driver,
@@ -22,10 +23,9 @@ import {
22
23
  resolveRegistry,
23
24
  resumeRefusal,
24
25
  retryPolicyProblem,
25
- step,
26
26
  validateDefinition,
27
27
  workersOf
28
- } from "../chunks/chunk-EUU44PHD.js";
28
+ } from "../chunks/chunk-3U4UZC34.js";
29
29
 
30
30
  // src/scheduler/bootstrap.ts
31
31
  import { WorkerManager } from "fyflow-scheduler";
@@ -281,14 +281,50 @@ function createRuntime(document, code = {}, options = {}) {
281
281
  const done = new Promise((resolve) => {
282
282
  settle = resolve;
283
283
  });
284
- let state = emptyState();
284
+ const acc = Accumulator.load(emptyState());
285
+ let cached = null;
286
+ const frozen = /* @__PURE__ */ new WeakSet();
285
287
  let settled = false;
286
288
  let seed = from === null ? null : from.events;
289
+ const own = (input) => {
290
+ if (input.kind !== "outcome" && input.kind !== "resolution") return input;
291
+ const r = input.record;
292
+ return typeof r.result === "object" && r.result !== null ? { kind: input.kind, record: structuredClone(r) } : input;
293
+ };
294
+ const take = (input) => {
295
+ if (acc.apply(own(input))) cached = null;
296
+ };
297
+ const catchUp = () => {
298
+ if (seed === null) return;
299
+ for (const stored of seed) take(stored);
300
+ seed = null;
301
+ };
302
+ const freezeDeep = (root) => {
303
+ const stack = [root];
304
+ while (stack.length > 0) {
305
+ const v = stack.pop();
306
+ if (typeof v !== "object" || v === null || frozen.has(v)) continue;
307
+ frozen.add(v);
308
+ if (ArrayBuffer.isView(v)) continue;
309
+ if (v instanceof Map) for (const [k, x] of v) stack.push(k, x);
310
+ else if (v instanceof Set) for (const x of v) stack.push(x);
311
+ for (const k of Reflect.ownKeys(v)) {
312
+ const d = Object.getOwnPropertyDescriptor(v, k);
313
+ if (d !== void 0 && "value" in d) stack.push(d.value);
314
+ }
315
+ Object.freeze(v);
316
+ }
317
+ return root;
318
+ };
319
+ const current = () => {
320
+ catchUp();
321
+ return cached ??= freezeDeep(acc.materialise());
322
+ };
287
323
  const settleIfTerminal = () => {
288
- if (settled || !isTerminal(state)) return;
324
+ if (settled || !isTerminal({ outcome: acc.outcomeNow() })) return;
289
325
  settled = true;
290
- if (state.outcome === "interrupted" && stopping.size > 0) {
291
- const at = state;
326
+ const at = current();
327
+ if (at.outcome === "interrupted" && stopping.size > 0) {
292
328
  const fin = () => {
293
329
  settle(at);
294
330
  router.release(runId);
@@ -296,7 +332,7 @@ function createRuntime(document, code = {}, options = {}) {
296
332
  void Promise.allSettled([...stopping]).then(fin);
297
333
  return;
298
334
  }
299
- settle(state);
335
+ settle(at);
300
336
  router.release(runId);
301
337
  };
302
338
  const view = router.forRun(runId);
@@ -333,11 +369,8 @@ function createRuntime(document, code = {}, options = {}) {
333
369
  // of a run from its first, and it is the well-formed wrong answer the first time a driver's
334
370
  // sequence does not start at this callback's first call.
335
371
  onFoldInput: (input, seq) => {
336
- if (seed !== null) {
337
- for (const stored of seed) state = step(state, stored);
338
- seed = null;
339
- }
340
- state = step(state, input);
372
+ catchUp();
373
+ take(input);
341
374
  queueMicrotask(settleIfTerminal);
342
375
  startOptions.onFoldInput?.(input, seq);
343
376
  },
@@ -361,7 +394,22 @@ function createRuntime(document, code = {}, options = {}) {
361
394
  router.release(runId);
362
395
  throw e;
363
396
  }
364
- return { driver, done, header: driver.checkpoint().header, stop: (stopOptions) => driver.stop(stopOptions) };
397
+ return {
398
+ driver,
399
+ done,
400
+ header: driver.checkpoint().header,
401
+ stop: (stopOptions) => driver.stop(stopOptions),
402
+ state: current,
403
+ outcome: () => {
404
+ catchUp();
405
+ return acc.outcomeNow();
406
+ },
407
+ node: (taskId) => {
408
+ catchUp();
409
+ const entry = acc.entry(taskId);
410
+ return entry === void 0 ? null : freezeDeep(entry);
411
+ }
412
+ };
365
413
  }
366
414
  }
367
415
  function settlementRouter(scheduler) {
@@ -939,13 +939,21 @@ export declare class Driver {
939
939
  * channels with no shared order, so `RN-R3`'s *"read in append order"* was about
940
940
  * a value no one could hold.
941
941
  *
942
- * `log` above is the **record-kind subsequence of this array**, kept as its own
943
- * array only because `effectiveOutcome` and `isSuspended` scan it on every
944
- * settlement and rebuilding it by filter each time is quadratic. It holds the
945
- * same record objects, never copies. `test/driver/` asserts the two agree, so
946
- * the redundancy is checked rather than trusted.
942
+ * `log` above is the **record-kind subsequence of this array**, by reference — the
943
+ * same record objects, never copies; `test/driver/` asserts the two agree, so the
944
+ * redundancy is checked rather than trusted. *(It was kept "only because
945
+ * `effectiveOutcome` and `isSuspended` scan it on every settlement" until W195,
946
+ * whose {@link TaskLog} hands each of them one task's records instead — P39 fact 5.
947
+ * It stays for what still reads it WHOLE and in order: `snapshot()`, `outputs()`'s
948
+ * catch-up, and the retention walk's alias check.)*
947
949
  */
948
950
  private readonly inputs;
951
+ /**
952
+ * **Each task's records, by reference and in append order** — {@link TaskLog}, `RN-R13` v0.30, ADR 0121 (W195). Written in
953
+ * `append` beside `log`; read by every per-task question the driver asks of its log — `advance`, `parked`, `hasParked`,
954
+ * `inFlight`, `outputs()`, `effectiveOutcomeOf` — so each costs the task, never the run.
955
+ */
956
+ private readonly taskLog;
949
957
  private readonly graph;
950
958
  private readonly regions;
951
959
  private readonly dispatched;
@@ -1687,7 +1695,7 @@ export declare class Driver {
1687
1695
  * [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)
1688
1696
  * rules 2 and 3, and `FD-C2`.
1689
1697
  *
1690
- * **It is a PROJECTION of the log and adds no retention** — measured at
1698
+ * **It is a PROJECTION of the log and adds no retention of a result** — measured at
1691
1699
  * [P12](../../doc/plans/p12-what-a-node-receives-and-what-a-decision-sees.md)'s
1692
1700
  * declaration and again before this was written: this driver holds
1693
1701
  * `private readonly log: LogRecord[]` with exactly one `push` and no `splice`,
@@ -1697,14 +1705,11 @@ export declare class Driver {
1697
1705
  * described **today** before this method existed, and this adds **no retention**.
1698
1706
  * *(Q19 is RESOLVED since W53 —
1699
1707
  * [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
1700
- * — and `RN-R13` names this memo by id: it holds results **by reference only**,
1708
+ * — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
1701
1709
  * asserted by IDENTITY at ladder L2 rather than by a byte count, because a
1702
1710
  * reachability SET cannot tell an alias from a copy. W53's readers found that
1703
1711
  * from both ends — the rule's stated test returned a third name on a `needs`
1704
1712
  * flow, and cloning here cost ZERO.)*
1705
- * *(It adds ONE field — `outputsCache` below — which is a memo the log invalidates
1706
- * and which holds nothing the log does not. This read *"zero fields"* until W41's
1707
- * conformity reader read it beside the memo the cost measurement had just added.)*
1708
1713
  *
1709
1714
  * **`effectiveOutcome` and not the raw record**, for `spec/flow-definition.md`
1710
1715
  * section 3 rule 4's own reason: a resumed node has two records in the log and
@@ -1712,19 +1717,46 @@ export declare class Driver {
1712
1717
  * suspended has no effective outcome and therefore no entry — which is what makes
1713
1718
  * `FD-R3` fire rather than a reference quietly resolving to `undefined`.
1714
1719
  *
1715
- * **Computed on demand rather than maintained**, and the cost is why: it is
1716
- * `O(log)` per call, so maintaining it would be a second source that could
1717
- * disagree with the log — the defect [Q43](../../doc/questions/q43.md) and
1718
- * `arrival-count`'s re-sourcing are each an instance of. **A node that declares no
1719
- * `needs` never reaches this method**, which is what keeps ADR 0040 rule 1's *the
1720
- * default is unchanged* true of the cost as well as of the value.
1720
+ * **CAUGHT UP rather than rebuilt** — `RN-R13` v0.30, P39 decision 4 and fact 7,
1721
+ * [ADR 0121](../../doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md) (W195), amending ADR 0040
1722
+ * rule 2's *computed rather than maintained*. Until W195 this was memoised on the log's LENGTH and rebuilt from the
1723
+ * WHOLE log whenever the log had grown — so a node with a `needs` entered once per settlement, which is every instance
1724
+ * of a `map` whose branch declares one, paid O(log) per entry and the run O(log²): the case its own memo missed by
1725
+ * construction. Now each call reads only the records appended since the last (`at`, an index into the append-only
1726
+ * `log`), and for each one asks `effectiveOutcome` of THAT TASK's records ({@link TaskLog}) — so the merge stays in
1727
+ * 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
1728
+ * the moment of the call: nothing here can say what the log does not. **Equal to a rebuild at every read**
1729
+ * (`test/driver/log-view.test.ts`, at every fold input of every valid corpus flow): the same entries, a node's list
1730
+ * in its tasks' first-record order, each `result` the log record's own object.
1731
+ *
1732
+ * **Still LAZY, so ADR 0040 rule 1's *the default is unchanged* holds of the cost**: nothing is caught up until a
1733
+ * node or an edge that declares a `needs` asks, and a run that declares none never builds it.
1721
1734
  */
1722
1735
  private outputs;
1723
1736
  /**
1724
- * The memo above. **Not run state and not a second copy of it** — it is discarded
1725
- * and rebuilt the moment the log grows, and nothing ever reads it without checking
1726
- * the length first, so there is no state here that could be stale in a way anything
1727
- * could observe. `ADR 0014`'s *the driver computes no run state* is untouched.
1737
+ * **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
1738
+ * was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
1739
+ * in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
1740
+ *
1741
+ * **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
1742
+ * RESOLUTION, later, gives it one: that is the one entry that is not appended at the END of its node's list but placed
1743
+ * at its first record's position — reachable, and the corpus's *parked, attempted, resolved* drain reaches it. *(Whether
1744
+ * a suspended node's ABSENCE is ever read: no — W41's adequacy reader measured making it contribute
1745
+ * `{result: undefined}` cost **0**, because every shipped policy leaves a parked branch holding a token, so no join
1746
+ * fires while one is parked and no reader downstream of it is entered. [Q97](../../doc/questions/q97.md).)* An entry
1747
+ * that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
1748
+ * so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
1749
+ * none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
1750
+ */
1751
+ private place;
1752
+ /**
1753
+ * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
1754
+ * it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
1755
+ * never a copy — `tasks` is one slot per task id
1756
+ * read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
1757
+ * node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
1758
+ * read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
1759
+ * *the driver computes no run state* is untouched. `null` until a `needs` first asks.
1728
1760
  */
1729
1761
  private outputsCache;
1730
1762
  /**
@@ -1980,7 +2012,9 @@ export declare class Driver {
1980
2012
  *
1981
2013
  * A record is pushed to `log` as well, by reference: `effectiveOutcome` reads
1982
2014
  * the stored object and `OR-R8` derives rather than appends, so a second copy
1983
- * here would be a second truth. What leaves through `onFoldInput` **is** a
2015
+ * here would be a second truth. **And to its task's sub-log in {@link TaskLog}**,
2016
+ * by reference too (`RN-R13` v0.30, ADR 0121, W195) — here, the one place every
2017
+ * element passes, so the view can never lag the log. What leaves through `onFoldInput` **is** a
1984
2018
  * copy, frozen, for the same reason `snapshot()` is: a listener holding the
1985
2019
  * driver's own array could forge a settlement and bypass `RS-R1`.
1986
2020
  */
@@ -0,0 +1,94 @@
1
+ import type { FoldInput, NodeRun, RunOutcome, RunState } from './run-state.js';
2
+ /**
3
+ * **A count of the work an accumulator did** — P39 decision 6 and stop-and-ask (e): one per input applied, and one per
4
+ * entry, element or cell it reads or writes while loading, copying or materialising. Clock-free, so a ratio of two
5
+ * counts is a property of the code and not of a machine (Q179). Owned by the caller; absent, nothing is counted.
6
+ */
7
+ export interface FoldCounter {
8
+ visits: number;
9
+ }
10
+ export declare class Accumulator {
11
+ private runId;
12
+ private outcome;
13
+ private haltedBy;
14
+ private cancelRequest;
15
+ private interruption;
16
+ private paused;
17
+ /** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
18
+ private readonly nodes;
19
+ /** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
20
+ private readonly regions;
21
+ private readonly pathEnds;
22
+ private readonly resumes;
23
+ private readonly gated;
24
+ private counts;
25
+ private readonly base;
26
+ private readonly counter;
27
+ private constructor();
28
+ /** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
29
+ static load(state: RunState, counter?: FoldCounter): Accumulator;
30
+ private tick;
31
+ /**
32
+ * **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
33
+ * O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
34
+ */
35
+ outcomeNow(): RunOutcome;
36
+ /**
37
+ * **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
38
+ * An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
39
+ * dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
40
+ */
41
+ entry(taskId: string): NodeRun | undefined;
42
+ /** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
43
+ private node;
44
+ /** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
45
+ private instance;
46
+ /** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
47
+ private tally;
48
+ private countNode;
49
+ private stateCount;
50
+ /** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
51
+ private setNode;
52
+ /**
53
+ * The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
54
+ * paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
55
+ */
56
+ private touchRegion;
57
+ private unweigh;
58
+ /** The instance's share of the region counts put back after a write. */
59
+ private settleRegion;
60
+ private addPathEnd;
61
+ /**
62
+ * **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
63
+ * `false` where it returned the state it was handed, so `step` can return that same object. Throws where
64
+ * that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
65
+ * the module header's stated limit on region paths.
66
+ */
67
+ apply(input: FoldInput): boolean;
68
+ private applyRegion;
69
+ /**
70
+ * **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
71
+ * one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
72
+ * recomputes has moved the state.
73
+ */
74
+ private withOutcome;
75
+ private precedence;
76
+ /**
77
+ * **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
78
+ * 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
79
+ * entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
80
+ */
81
+ materialise(): RunState;
82
+ private list;
83
+ private materialiseRegion;
84
+ }
85
+ /**
86
+ * **The fold, with its work counted** — the INTERNAL entry point P39 decision 6 names: `fold` is this with no counter.
87
+ * One accumulator, every input applied in append order (`RN-R3`), materialised once: linear in `inputs`.
88
+ */
89
+ export declare function foldWith(inputs: readonly FoldInput[], counter?: FoldCounter): RunState;
90
+ /**
91
+ * **One step, with its work counted** — `step` is this with no counter: load, apply, materialise, and the state it was
92
+ * handed itself back wherever the arm moves nothing. O(the state it is handed).
93
+ */
94
+ export declare function stepWith(prev: RunState, input: FoldInput, counter?: FoldCounter): RunState;
@@ -6,6 +6,12 @@
6
6
  * ladder L3 checks by folding the same log twice and deep-comparing, and it is
7
7
  * what makes [Q14](../../doc/questions/q14.md)'s answer an answer rather than a
8
8
  * restated leaning.
9
+ *
10
+ * **Since v0.29 the arms live in ONE place, `src/fold/accumulator.ts`** (`RN-R43`,
11
+ * [ADR 0120](../../doc/decisions/0120-the-fold-is-one-accumulator-and-step-stays-pure.md)): an accumulator mutated in
12
+ * place WITHIN one call and materialised into a plain state, so nothing survives a call and `RN-R1` holds as before.
13
+ * This module keeps the types, `emptyState`, `isTerminal`, `nodeOf` and `foldLog`, and `fold` and `step` call into it.
14
+ * That module is internal: `index.ts` re-exports this one and never it.
9
15
  */
10
16
  import type { LogRecord, OutcomeRecord } from '../driver/log.js';
11
17
  import type { RegionEvent } from '../driver/events.js';
@@ -491,14 +497,35 @@ export type FoldInput = {
491
497
  };
492
498
  export declare function emptyState(): RunState;
493
499
  /**
494
- * One step. **Returns a new state; never mutates the one it was given** — which is
495
- * what lets `RN-R2` hold (folding a prefix then one more record equals folding the
500
+ * One step. **Never mutates the state it was given, and returns a NEW one wherever the input moves something** — and
501
+ * that state ITSELF where it moves nothing (below) — which is what lets `RN-R2` hold (folding a prefix then one more record equals folding the
496
502
  * whole) and what makes a fold usable while a run is still going.
503
+ *
504
+ * **Its work is O(the state it is handed), not O(the input)** — `RN-R43`, v0.29,
505
+ * [ADR 0120](../../doc/decisions/0120-the-fold-is-one-accumulator-and-step-stays-pure.md): it loads `prev` into the
506
+ * fold's one accumulator, applies the input and materialises a new plain state, and a pure function from one plain
507
+ * `RunState` to another cannot do less than copy the record it changes. So stepping every input of a long run costs
508
+ * O(run) per input; {@link fold} over the whole sequence is linear. A consumer that needs a run's state after every input
509
+ * reads it where a runtime keeps it, rather than stepping a copy of its own.
510
+ *
511
+ * **Each arm behaves on ANY state it is handed exactly as it did before v0.29** — not only on one a fold can reach. The
512
+ * outcome is recomputed only by the arms that recomputed it, so a hand-built state whose `outcome` disagrees with its
513
+ * cells keeps that outcome across `paused`, `unpaused`, `gated`, `attempted` and a `resumed` that ends no stop; and an
514
+ * input that moves nothing — a second cancel request, or one after the run is already `cancelled`, a second stop or
515
+ * pause, an unpause of a run not paused, a dispatch or an ask of a
516
+ * task already in `nodes`, a region instance already entered, an attempt of a task not in `nodes` — hands back `prev`
517
+ * ITSELF. A cell no input touched is `prev`'s own object in the result. (Its one stated limit, a region path naming an
518
+ * `Object.prototype` member, which no driver appends, is `src/fold/accumulator.ts`'s header's.)
497
519
  */
498
520
  export declare function step(prev: RunState, input: FoldInput): RunState;
499
521
  /**
500
522
  * The fold. `RN-R2`: total over any prefix, so `fold(log.slice(0, n))` is defined
501
- * for every `n` and `step(fold(prefix), next) === fold(prefix + next)`.
523
+ * for every `n` and `step(fold(prefix), next)` deep-equals `fold(prefix + next)`.
524
+ *
525
+ * **Linear in its inputs** — `RN-R43`, v0.29, [W194](../../doc/work-items/w194-the-fold-is-one-accumulator.md): ONE
526
+ * accumulator (`src/fold/accumulator.ts`, internal) has every input applied to it in place and is materialised into a
527
+ * plain state once, at the end. Before v0.29 this was a `step` per input, each copying the run, so a fold was quadratic
528
+ * in its run — 30 s for a 4,000-item map (P39 fact 1). What it returns is unchanged on every sequence whose region paths are branch paths (P39 decision 5; `RN-R43`'s one stated limit).
502
529
  *
503
530
  * `RN-R3`: read in **append order**, never sorted. Two settlements can share a
504
531
  * millisecond; `schedulerTimestamp` is the scheduler's clock and `observedAt` is
@@ -65,7 +65,7 @@ import { FyflowScheduler } from 'fyflow-scheduler';
65
65
  import type { ResourceGroup, WorkerManager } from 'fyflow-scheduler';
66
66
  import { Driver } from '../driver/driver.js';
67
67
  import type { Checkpoint, CheckpointHeader, DriverOptions, RunStopOptions } from '../driver/driver.js';
68
- import type { RunState } from '../fold/run-state.js';
68
+ import type { NodeRun, RunOutcome, RunState } from '../fold/run-state.js';
69
69
  import type { Finding, FlowDefinition, JsonSchema, RegistryCode, RegistryDocument, ResolvedPool, ResolvedRegistry, RetryPolicy, TaskNode } from '../definition/types.js';
70
70
  import type { PoolFactory } from './bootstrap.js';
71
71
  /**
@@ -291,12 +291,17 @@ export interface RunHandle {
291
291
  * element: the stopped driver is deaf, so no settlement could move the fold again, and the runtime waits on the cancels
292
292
  * the driver sent through this run's view of the scheduler instead (`RN-R33`, ADR 0105, amending ADR 0058 rule 3).
293
293
  *
294
- * It is the SAME expression a consumer would have written, moved: the runtime keeps ONE
295
- * `RunState` and `step`s it per fold input, which is what `fold` is
296
- * (`for (const i of inputs) s = step(s, i)`), and `RN-R2` is the rule that says a stepped prefix
297
- * equals a fold of that prefix. `test/scheduler/runtime.test.ts` holds `await done` equal to
298
- * `fold(driver.foldInputs())` — the caller's own expression over the driver's own sequence, which
299
- * is a second route rather than a second definition.
294
+ * It is the SAME expression a consumer would have written, moved: the runtime keeps the fold's ONE accumulator OPEN —
295
+ * W194's, `fold`'s own definition (`RN-R43`) — and applies each fold input to it, which is what `fold` does over a
296
+ * whole sequence; `RN-R2` is the rule that says a prefix applied equals a fold of that prefix. *(Until W196 it kept ONE
297
+ * `RunState` and `step`ped it per input — O(run) a step since W194, so O(run²) a run; ADR 0122 amends ADR 0058 rule 5.)*
298
+ * `test/scheduler/runtime.test.ts` holds `await done` equal to `fold(driver.foldInputs())` — the caller's own
299
+ * expression over the driver's own sequence, which is a second route rather than a second definition.
300
+ *
301
+ * **It resolves with the SAME frozen object {@link RunHandle.state} returns at that moment** — P39 (f), `RN-R44`: one
302
+ * copy and one definition, so `await done === handle.state()` once it has settled (but for an `unpause` after it — see
303
+ * `state`). **BREAKING in 0.9.0 for code that
304
+ * MUTATES `await done`**: it is frozen at every depth, and a write throws in strict code.
300
305
  */
301
306
  readonly done: Promise<RunState>;
302
307
  /**
@@ -329,6 +334,33 @@ export interface RunHandle {
329
334
  * store holds whichever is written first (`RN-R18`).
330
335
  */
331
336
  readonly header: CheckpointHeader;
337
+ /**
338
+ * **The run's state NOW — a frozen copy, cached until the next fold input that moves it** — `RN-R44`,
339
+ * [ADR 0122](../../doc/decisions/0122-a-runtimes-live-state-is-a-frozen-copy-on-its-handle.md), P39 (b). Equal to
340
+ * `fold(driver.foldInputs())` at a quiescent point (`RN-R12`: read it on a `queueMicrotask` after an `onFoldInput`, or
341
+ * outside the driver's turn — mid-turn it is a prefix, which the fold is total over and which may read terminal while
342
+ * the run is not). **Frozen at every depth** — a write to it throws in strict code and reaches nothing the runtime keeps
343
+ * — and **the SAME object on every read until an input moves the state**, so reading it twice costs one copy. It is NOT
344
+ * a live view (ADR 0007's hazard): a read is the state at that moment, and the next input makes a new one beside it.
345
+ *
346
+ * **Its cost is O(run) for the first read after each input that moved the state** — a materialisation of every
347
+ * `nodes` entry — so reading it after EVERY input costs O(run²) over a run (P39 prediction 4). Read
348
+ * {@link RunHandle.outcome} and {@link RunHandle.node} per input; read this when you draw the run. On a RESUMED run it
349
+ * holds the stored events too: they are applied before the first delivered input, which `resume()` always delivers
350
+ * before it returns (`RN-R22`). After `done` settles it is the object `done` resolved with — until an `unpause` of a run
351
+ * that finished while paused, the one run control a finished run takes (`RN-R35`): `state()` follows that element and
352
+ * `done` keeps the state at settlement. What `Object.freeze` does not stop — a typed array's elements, an
353
+ * `ArrayBuffer`'s bytes, a `Date`'s time, a `Map`'s or a `Set`'s membership — stays writable through a read and reaches
354
+ * later reads: the stated limit (`RN-R44`). **BREAKING in 0.9.0 for code that BUILDS a `RunHandle`** (P39 (d)) — with `outcome` and `node`.
355
+ */
356
+ state(): RunState;
357
+ /** **The run's outcome now, in O(1)** — `state().outcome` without the copy (`RN-R44`); the read to make after every input. */
358
+ outcome(): RunOutcome;
359
+ /**
360
+ * **One task's entry now, in O(1) in the run, or `null`** — `state().nodes[taskId]`, the same frozen object, without
361
+ * materialising the rest (`RN-R44`); the first read of an entry freezes it once, O(the entry). `null` for a task the run never dispatched or asked, whatever the id names (`toString` included).
362
+ */
363
+ node(taskId: string): NodeRun | null;
332
364
  }
333
365
  export interface Runtime {
334
366
  /** **Exactly the object this runtime hands its own drivers** (decision 14) — spread it into `new Driver(…)` beside a `runId`, a `scheduler` and a `joinPolicy` and you have built what `start` builds. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fyflow-orchestration",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Flow orchestration over fyflow-scheduler: sequence, decision, fan-out and join",
5
5
  "keywords": [
6
6
  "workflow",