fyflow-orchestration 0.9.1 → 0.11.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.
@@ -11,6 +11,7 @@ import {
11
11
  derivePool,
12
12
  emptyState,
13
13
  groupsNamedBy,
14
+ inputOf,
14
15
  isDerivation,
15
16
  isTerminal,
16
17
  languagesOf,
@@ -25,7 +26,7 @@ import {
25
26
  retryPolicyProblem,
26
27
  validateDefinition,
27
28
  workersOf
28
- } from "../chunks/chunk-I3KDEOLB.js";
29
+ } from "../chunks/chunk-5CVSWDCB.js";
29
30
 
30
31
  // src/scheduler/bootstrap.ts
31
32
  import { WorkerManager } from "fyflow-scheduler";
@@ -350,6 +351,12 @@ function createRuntime(document, code = {}, options = {}) {
350
351
  return p;
351
352
  }
352
353
  } : view;
354
+ const reading = {
355
+ ...registry,
356
+ joinPolicy: startOptions.joinPolicy ?? liveToken,
357
+ ...options.defaultRetryPolicy !== void 0 ? { defaultRetryPolicy: options.defaultRetryPolicy } : {},
358
+ ...options.validateBoundaries !== void 0 ? { validateBoundaries: options.validateBoundaries } : {}
359
+ };
353
360
  const driver = new Driver(flow, {
354
361
  runId,
355
362
  scheduler: scheduler2,
@@ -408,7 +415,11 @@ function createRuntime(document, code = {}, options = {}) {
408
415
  catchUp();
409
416
  const entry = acc.entry(taskId);
410
417
  return entry === void 0 ? null : freezeDeep(entry);
411
- }
418
+ },
419
+ // **`RN-R45` — the reading, over this run's own events** (W205, P42 decision 7): the export, handed the options this
420
+ // function built the run's driver with — `registry`, its `joinPolicy`, and the two runtime-wide ones — so a reading
421
+ // builds the driver `start` built. The live driver is only READ (`checkpoint()`, a copy).
422
+ inputOf: (target) => inputOf(flow, reading, driver.checkpoint(), target)
412
423
  };
413
424
  }
414
425
  }
@@ -3,6 +3,7 @@ export * from "./src/definition/regions.js";
3
3
  export * from "./src/definition/validate.js";
4
4
  export * from "./src/definition/schema.js";
5
5
  export * from "./src/definition/registry.js";
6
+ export * from "./src/definition/definition-schema.js";
6
7
  export * from "./src/definition/witness.js";
7
8
  export * from "./src/definition/wiring.js";
8
9
  export * from "./src/definition/assist.js";
@@ -0,0 +1,23 @@
1
+ import { type Registry } from './types.js';
2
+ /**
3
+ * **What `definitionSchema()` returns** — a plain, JSON-serialisable JSON Schema draft 2020-12 document, owned by the
4
+ * caller: a new value on every call, so mutating one changes no other.
5
+ */
6
+ export type DefinitionSchema = {
7
+ $schema: string;
8
+ } & Record<string, unknown>;
9
+ /**
10
+ * **A flow definition's shape as a JSON Schema, draft 2020-12** — P43 (a1), ADR 0132; the registry form W211's. A
11
+ * plain object, a NEW one on every call, JSON-serialisable.
12
+ *
13
+ * - **No argument: the STATIC shape.** It carries every rule a node, an edge or a value decides by its own shape and
14
+ * names `validateDefinition` as the authority for the rest; what it refuses, `validateDefinition` refuses under
15
+ * EVERY registry.
16
+ * - **A registry: the shape SPECIALISED to it** (`spec/flow-definition.md` section 1, v0.51). The registry is first held
17
+ * to `checkRegistry` — the check `new Driver` runs over the registry it is handed — and one that check refuses THROWS
18
+ * that refusal, never specialised. Then the static shape gains what the registry decides of ONE node: a task's
19
+ * `type`, `pool`, `settings`, a transform-only type's `transform`, `workerGroups`, `transform.lang`, a predicate
20
+ * name, a gate's `type`, `settings` and — for a STATIC gate type — `route` (`specialise`, below). What it refuses,
21
+ * `validateDefinition(flow, registry)` refuses against THAT registry.
22
+ */
23
+ export declare function definitionSchema(registry?: Registry): DefinitionSchema;
@@ -38,10 +38,11 @@ export type TerminalState = 'done' | 'failed' | 'user_action' | 'cancelled';
38
38
  * | `"$payload"` | **the value the RUN entered with** — what `start()` was handed, from any node and any edge (v0.44, [ADR 0117](../../doc/decisions/0117-a-reference-may-name-what-reached-the-run.md)). Always there, so no `?` and no array form. `$` is outside `FD-V16`'s charset, and the whole `$` prefix is reserved: `"$item"` — a map instance's item — is W191's, and any other `$` spelling is `FD-V26` |
39
39
  * | `"score"` | that node's output **on this node's own branch path**. Exactly one, or the run stops (`FD-R3`) |
40
40
  * | `["score"]` | that node's outputs as an **array**, in ascending segment-wise-numeric `branchPath` order. Legal only where the reader is outside a region containing the named node (`FD-V28`) |
41
+ * | `"score!error"` | **that node's ERROR** rather than its result, on either form and with `?` last (`["score!error?"]`): the effective outcome's `error` string, `null` where it succeeded — v0.49, [ADR 0131](../../doc/decisions/0131-a-reference-may-name-a-nodes-error.md). Aimed as `"score"` is; `!` is outside `FD-V16`'s charset |
41
42
  *
42
43
  * **The array form uses JSON's own array syntax rather than a sigil**, so the
43
44
  * grammar invents tokens only for what has no node id: `"."`, the `?` modifier
44
- * (v0.13), and — v0.44 — the `$` prefix. *(This read *"exactly one invented token"*
45
+ * (v0.13), — v0.44 — the `$` prefix, and — v0.49 — the `!error` part. *(This read *"exactly one invented token"*
45
46
  * until v0.13 and stayed so until W190.)* A `string` covers every token, so the
46
47
  * TYPE does not move; `referenceTarget`'s parse does.
47
48
  */
@@ -19,6 +19,19 @@
19
19
  */
20
20
  import type { Finding, FlowDefinition, GateType, GateTypes, JsonSchema, StaticGateType, LanguageAdapter, Languages, NodeTypeEntry, NodeTypes, Pools, Registry, WorkerEntry, Workers } from './types.js';
21
21
  export { readKeyPath } from './condition.js';
22
+ declare const KINDS: readonly ["task", "fanout", "map", "join", "end", "gate", "ask"];
23
+ /**
24
+ * **`FD-V16`'s charset — a flow's and a node's id** ([spec/task-id.md](../../spec/task-id.md)'s) — v0.50 (W210,
25
+ * [ADR 0132](../../doc/decisions/0132-a-definitions-shape-is-a-schema-built-from-the-validator.md)), so
26
+ * `definitionSchema()` builds its `pattern` from the validator's expression. **A COPY** — `compile()` on it, or a
27
+ * consumer's edit, moves no verdict (W210's conformity reader, S2: the live object did).
28
+ */
29
+ declare const ID_RE_COPY: RegExp;
30
+ /** **The seven node kinds `FD-V24` admits** — v0.50 (W210, ADR 0132), for `definitionSchema()`'s `kind` enum; a FROZEN copy. */
31
+ declare const KINDS_COPY: readonly (typeof KINDS)[number][];
32
+ export { ID_RE_COPY as ID_RE, KINDS_COPY as KINDS };
33
+ /** **The terminal states an edge's `on` may name** (`FD-V18`) — a copy of the validator's own list, v0.50 (W210, ADR 0132). */
34
+ export declare const ROUTABLE_STATES: readonly string[];
22
35
  /**
23
36
  * **The six keys a `task` declares that an `ask` REFUSES, each by `FD-V47` and by nothing else** — v0.42, W179,
24
37
  * P35 stop-and-ask (a) and decision 6. Every one is read by a task's WORKER or its scheduler task — a type and its
@@ -49,6 +62,17 @@ export declare const TASK_KEYS_DECLARED: readonly string[];
49
62
  * P16 decision 3's silent-key hole one row over.
50
63
  */
51
64
  export declare const NODE_KEYS_DECLARED: Readonly<Record<string, readonly string[]>>;
65
+ /**
66
+ * **The REQUIRED half of each kind's keys, the edge's and the document's** — `FD-V23`'s three lists, copied, v0.50
67
+ * (W210, [ADR 0132](../../doc/decisions/0132-a-definitions-shape-is-a-schema-built-from-the-validator.md)):
68
+ * `definitionSchema()` reads its `required` from these, so a key the validator starts requiring is one the schema
69
+ * requires, and a key it stops requiring one the schema stops requiring. Every document key is required.
70
+ */
71
+ export declare const NODE_KEYS_REQUIRED: Readonly<Record<string, readonly string[]>>;
72
+ /** The edge's required keys — `FD-V23`'s *an edge without `to`*, copied (v0.50, W210). */
73
+ export declare const EDGE_KEYS_REQUIRED: readonly string[];
74
+ /** The document's keys, every one required (`FD-V23`) and none other declared (`FD-V17`) — copied, v0.50 (W210). */
75
+ export declare const DOC_KEYS_DECLARED: readonly string[];
52
76
  /**
53
77
  * **The two keys a `retryPolicy` carries, and no others** — `FD-V30`. The scheduler's own
54
78
  * constructor at 0.5.2 destructures exactly these; a key beside them is a typo the scheduler
@@ -324,6 +348,12 @@ export declare function settingsProblem(schema: JsonSchema | undefined, defaults
324
348
  * reads `conditional`. A `"."` may not carry one — it resolves to the edge's own
325
349
  * value, which is always there — and the remainder after the suffix is stripped must
326
350
  * still be an id, so `"?"`, `"n??"` and `".?"` are all `null` with no extra clause.
351
+ *
352
+ * **And since v0.49 the `!error` PART** — [ADR 0131](../../doc/decisions/0131-a-reference-may-name-a-nodes-error.md),
353
+ * P43 (c1): `"n!error"` names node `n`'s ERROR rather than its result, in both forms and with `?` (`"n!error?"`,
354
+ * `["n!error"]`, `["n!error?"]`). It is a fourth field, `part`, beside `form` and `conditional` and orthogonal to both —
355
+ * **present only on that form** — because it changes WHAT is read of the outputs and never which outputs are named:
356
+ * `FD-V27`-`FD-V29` aim it as the plain reference and the driver resolves it through the same index (P43 decision 5).
327
357
  */
328
358
  export declare function referenceTarget(ref: unknown): {
329
359
  form: 'self';
@@ -337,11 +367,20 @@ export declare function referenceTarget(ref: unknown): {
337
367
  form: 'item';
338
368
  node: null;
339
369
  conditional: false;
340
- } | {
370
+ } | AimedReference | null;
371
+ /**
372
+ * **A reference that AIMS at a node** — `"n"` or `["n"]`, either with `?`, and since v0.49 either naming the node's ERROR
373
+ * (`"n!error"`, `["n!error?"]`, …) — [ADR 0131](../../doc/decisions/0131-a-reference-may-name-a-nodes-error.md), P43 (c1).
374
+ * **`part` is PRESENT only on the error form**, so every spelling before v0.49 parses to the object it parsed to (P43
375
+ * prediction 3), and a reader that does not ask for `part` reads the error form as the node it aims at — which is what
376
+ * `FD-V27`-`FD-V29` are meant to do (P43 decision 5) and what a RESOLVER must not (`resolveReference`, the wiring check).
377
+ */
378
+ export type AimedReference = {
341
379
  form: 'one' | 'all';
342
380
  node: string;
343
381
  conditional: boolean;
344
- } | null;
382
+ part?: 'error';
383
+ };
345
384
  export declare function validateDefinition(doc: FlowDefinition, registry: Registry): Finding[];
346
385
  /** Convenience: the rule ids violated, deduplicated and sorted. */
347
386
  export declare function violatedRules(findings: Finding[]): string[];
@@ -127,7 +127,17 @@ export interface WiringVerdict {
127
127
  /** The relation's own reason, in the words `fact-3-controls.log` prints. */
128
128
  why: string;
129
129
  sample?: WiringSample;
130
+ /**
131
+ * **Why a `transform-fails` verdict failed — present on that kind and ABSENT on every other** (v0.48,
132
+ * [W208](../../doc/work-items/w208-a-transforms-cause-and-when-an-ask-was-asked.md), P43 (d1), decision 6): `'threw'` where the transform did not produce a result — the language's worker half failing to import, having
133
+ * no default export, its `compile` throwing or its `evaluate` throwing, alike — and `'schema'` where it produced one
134
+ * and the consumer's `inputSchema` refused it. `why` says the same in words and is unmoved; this says it as data.
135
+ * Additive in 0.11.0: `VerdictKind`, `VERDICT_KINDS` and every count are unmoved.
136
+ */
137
+ cause?: TransformFailureCause;
130
138
  }
139
+ /** What a `transform-fails` verdict's {@link WiringVerdict.cause} names: the transform threw, or its result failed the consumer's schema. */
140
+ export type TransformFailureCause = 'threw' | 'schema';
131
141
  export interface WiringReport {
132
142
  flowId: string;
133
143
  verdicts: WiringVerdict[];
@@ -12,7 +12,7 @@
12
12
  * driver turns out to need a second copy of run truth, that is a finding about the
13
13
  * spec rather than a thing to build.
14
14
  */
15
- import type { Finding, FlowDefinition, GateTypes, Languages, NodeTypes, Pools, TerminalState, Workers } from '../definition/types.js';
15
+ import type { Finding, FlowDefinition, FlowNode, GateTypes, Languages, NodeTypes, Pools, TerminalState, Workers } from '../definition/types.js';
16
16
  import { type SchemaFinding } from '../definition/schema.js';
17
17
  import { type LogRecord, type OutcomeRecord } from './log.js';
18
18
  import { type PredicateRegistry } from './routing.js';
@@ -377,6 +377,68 @@ export declare class ResumeRefused extends Error {
377
377
  * function, two callers, so the two cannot refuse different checkpoints.
378
378
  */
379
379
  export declare function resumeRefusal(definition: FlowDefinition, checkpoint: unknown, runId?: string): ResumeRefused | null;
380
+ /**
381
+ * **What a reading asks about** — `RN-R45`, P42 decision 2 and stop-and-ask (c1). A node id and a branch path, for ANY node
382
+ * kind the definition has; a node id ALONE, which asks for every path the run reached it on (decision 4); or a task id —
383
+ * a task's or an ask's — which names both, as `spec/task-id.md` mints it (attempt `0`).
384
+ */
385
+ export type InputTarget = {
386
+ readonly taskId: string;
387
+ } | {
388
+ readonly nodeId: string;
389
+ readonly branchPath?: string;
390
+ };
391
+ /**
392
+ * **Which value a node was handed** — `RN-R45`, P42 decision 3: a task's `envelope` (`{ settings, input, transform? }`,
393
+ * `FD-C5`, `FD-C6` — what `dispatch` built for `createTask`, before any transform); the value `enter` composed for an ask,
394
+ * a gate, a fanout, a map or an end (`received`); and for a JOIN, an `arrival` at an arriving branch's path — what that
395
+ * branch carried in, a LATE arrival after an early firing included — or the `collection` it handed on when it fired, at
396
+ * its opener's path.
397
+ */
398
+ export type HandedAs = 'envelope' | 'received' | 'arrival' | 'collection';
399
+ /** **One thing a node was handed, on one branch path** — `RN-R45`. */
400
+ export interface NodeInput {
401
+ readonly nodeId: string;
402
+ readonly kind: FlowNode['kind'];
403
+ /** The branch path it was handed on — a task's and an ask's the one their id carries; a join's arrival the arriving branch's. */
404
+ readonly branchPath: string;
405
+ readonly handed: HandedAs;
406
+ /**
407
+ * The value, AS THE READING'S DRIVER BUILT IT: not copied again and not frozen beyond what the composition already
408
+ * froze — a value a `needs` reference composed is that driver's shared frozen copy (ADR 0126), the rest is the caller's
409
+ * own. It reaches nothing a live run holds: the reading's driver is its own and is dropped when the reading returns.
410
+ */
411
+ readonly value: unknown;
412
+ }
413
+ /**
414
+ * **Why a reading has no answer** — `RN-R45`, P42 decision 5. Returned, never thrown.
415
+ *
416
+ * | reason | when | `index` |
417
+ * |---|---|---|
418
+ * | `format` | the run is not `{ header, events[] }`, its `header.format` is not {@link CHECKPOINT_FORMAT}, its header's three fields are not format 1's types, its `runId` is not a run id (`ID-R2`), or its entry payload does not clone — `resumeRefusal`'s header half, the one function | `null` |
419
+ * | `flow` | `header.flowId` or `header.version` is not the definition's — no migration | `null` |
420
+ * | `unknown` | the target cannot name anything in this run: a node id the definition lacks, a branch path that is not `spec/task-id.md`'s grammar, or a task id that does not parse, names another run, an attempt other than `0`, or a node that is neither a task nor an ask | `null` |
421
+ * | `not-reached` | the replay did not hand the target anything within the stored prefix a reading verifies — which ends at the first `halted`, `stopped` or `cancelRequested` element, or at the stored events' end | the prefix's length |
422
+ * | `diverged` | the replay produced something other than a stored element BEFORE it reached the target — `RN-R20`'s refusal, by its position | that position |
423
+ */
424
+ export type InputRefusalReason = 'format' | 'flow' | 'unknown' | 'not-reached' | 'diverged';
425
+ /** **What a reading returns** — every answer for its target, in the order the replay reached them, or why there is none. */
426
+ export type InputReading = {
427
+ readonly ok: true;
428
+ readonly inputs: readonly NodeInput[];
429
+ } | {
430
+ readonly ok: false;
431
+ readonly reason: InputRefusalReason;
432
+ readonly index: number | null;
433
+ readonly message: string;
434
+ };
435
+ /**
436
+ * **What a reading builds its driver from** — the `DriverOptions` a run's routing reads, and nothing else (`RN-R45`, P42
437
+ * decision 1): the registry's members — `runtime.registry` IS this minus `joinPolicy` — and the three options a runtime
438
+ * adds. **No callback, no scheduler, no `createTask`, no run id**: a member not named here is not read, so a reading cannot
439
+ * call a consumer. `joinPolicy` omitted is `liveToken`, as `runtime.start` and `runtime.resume` default it.
440
+ */
441
+ export type InputOfOptions = Pick<DriverOptions, 'predicates' | 'workers' | 'nodeTypes' | 'pools' | 'groups' | 'languages' | 'gateTypes' | 'joinPolicy' | 'defaultRetryPolicy' | 'validateBoundaries'>;
380
442
  /**
381
443
  * A settlement carrying THIS driver's run id for a task it never dispatched —
382
444
  * `ID-R4`, and the inbound counterpart of `ID-R1`
@@ -1054,7 +1116,8 @@ export declare class Driver {
1054
1116
  * - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
1055
1117
  * 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
1056
1118
  * a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
1057
- * `needs` composition's per-key copy), and as its OWN copy only where `kept` says that element is the caller's
1119
+ * `needs` composition's value — since W202 an element of a reference's SHARED copy, which this keeps alive while held),
1120
+ * and as its OWN copy only where `kept` says that element is the caller's
1058
1121
  * (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
1059
1122
  * **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
1060
1123
  * `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
@@ -1397,10 +1460,47 @@ export declare class Driver {
1397
1460
  private replayStored;
1398
1461
  /** One fed element — a settlement, a resolution, an earlier resume, a stop, a pause, an unpause or an attempt — at stored position `i`. */
1399
1462
  private feedOne;
1400
- /** The listener's boundary, for the replay: a refusal routing raises stops the run (`OR-R10`); a refused resume passes through. */
1463
+ /**
1464
+ * The listener's boundary, for the replay: a refusal routing raises stops the run (`OR-R10`); a refused resume passes
1465
+ * through — and so does a reading's cut (`InputCut`, W205), which is no refusal and halts nothing.
1466
+ */
1401
1467
  private feed;
1402
1468
  /** `RN-R20`'s refusal, naming the position and both elements there. */
1403
1469
  private diverged;
1470
+ /**
1471
+ * **A cut site offers what a node was handed** (`RN-R45`, W205). An offer for another path is dropped. `own` is the
1472
+ * position of the node's OWN element where it is already appended — a task's `dispatched`, a join's firing — and `null`
1473
+ * where it is the NEXT append, which `append` confirms (`Reading.pending`). Either way it is TAKEN only at a STORED
1474
+ * position, so a composition whose element lies past the prefix the reading verifies is never answered.
1475
+ */
1476
+ private offer;
1477
+ /** **An offer taken** — kept, and for a target with a path the replay ends HERE: nothing after it is replayed (`InputCut`). */
1478
+ private take;
1479
+ /**
1480
+ * **A READING — what a node was handed, by replaying the run to that node's composition** — `RN-R45`, W205,
1481
+ * [ADR 0128](../../doc/decisions/0128-what-a-node-was-handed-is-read-by-replaying-the-run-to-its-composition.md), P42
1482
+ * decisions 1-7. The export {@link inputOf} and `RunHandle.inputOf` are this and nothing else. In order:
1483
+ *
1484
+ * 1. **The header**, refused by `resumeRefusal`'s own header half (`format`, `flow`) — never its halt half.
1485
+ * 2. **The target**, refused `unknown` where it cannot name anything in this run.
1486
+ * 3. **A driver built exactly as `runtime.resume` builds one** (`RN-R25`) — the definition, the registry's members, a
1487
+ * `joinPolicy` defaulting to `liveToken` — with NO callback and a scheduler that is handed nothing (`DEAF_SCHEDULER`).
1488
+ * 4. **The replay** (`readStep`) over the stored PREFIX it can verify, the cut armed: it stops at the target's
1489
+ * composition and returns what it found.
1490
+ *
1491
+ * Private and STATIC, so the export and the handle reach one road, and a test reaches the reading's own driver — its
1492
+ * count — by a cast (ADR 0121 rule 7): `driver` is `null` where a refusal came before one was built.
1493
+ */
1494
+ private static read;
1495
+ /**
1496
+ * **The reading's replay** — `resume()`'s first half (`resumeStep` to `replayStored`) with the cut armed, and nothing of
1497
+ * its second: no in-flight set, no `resumed`, nothing handed on. **The prefix it verifies ENDS at the first `halted`,
1498
+ * `stopped` or `cancelRequested` element** (P42 decision 5): after one a halted driver composes nothing more, whether the
1499
+ * replay regenerates it (a predicate's throw) or cannot (a consumer's callback, a refused result, an owner's call) —
1500
+ * what follows is `not-reached`, never `diverged` (P42 retro (a1) corrected the reason). Everything
1501
+ * before is compared as `RN-R20` compares it.
1502
+ */
1503
+ private readStep;
1404
1504
  /**
1405
1505
  * **What is IN FLIGHT: dispatched, and no settlement in the log** — `RN-R21`, in dispatch order. A
1406
1506
  * `user_action` settlement IS a settlement: a parked task is waiting on a person, not on a worker, and
@@ -1705,7 +1805,8 @@ export declare class Driver {
1705
1805
  * described **today** before this method existed, and this adds **no retention**.
1706
1806
  * *(Q19 is RESOLVED since W53 —
1707
1807
  * [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
1708
- * — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
1808
+ * — and `RN-R13` names `outputsCache` by id: it holds results **by reference** — and since W202 (ADR 0126) one shared
1809
+ * COPY per output a reference has read, the one thing in it that is not the log's object,
1709
1810
  * asserted by IDENTITY at ladder L2 rather than by a byte count, because a
1710
1811
  * reachability SET cannot tell an alias from a copy. W53's readers found that
1711
1812
  * from both ends — the rule's stated test returned a third name on a `needs`
@@ -1733,9 +1834,14 @@ export declare class Driver {
1733
1834
  * node or an edge that declares a `needs` asks, and a run that declares none never builds it.
1734
1835
  */
1735
1836
  private outputs;
1837
+ /**
1838
+ * The projection, made empty on first need and NOT caught up — `outputs()` catches it up; a `"$payload"` read, which
1839
+ * reads no output, only needs somewhere to keep its shared copy (W202).
1840
+ */
1841
+ private cache;
1736
1842
  /**
1737
1843
  * **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
1844
+ * was just read. An entry is `{ path, result, error }` with `result` the record's own object (`RN-R13`) and `error` its string or `null` (v0.49, ADR 0131); a node's list is kept
1739
1845
  * in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
1740
1846
  *
1741
1847
  * **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
@@ -1747,6 +1853,10 @@ export declare class Driver {
1747
1853
  * that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
1748
1854
  * so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
1749
1855
  * none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
1856
+ *
1857
+ * **Both a replacement and a removal let go of the entry OBJECT, and with it the entry's shared copy** (W202, ADR 0126,
1858
+ * `OutputEntry.shared`): a replaced output's next read makes a new copy of the new result, and a removed one's copy is
1859
+ * held nowhere — `RN-R13`'s v0.33 term, released with what it belongs to (ADR 0046).
1750
1860
  */
1751
1861
  private place;
1752
1862
  /**
@@ -1777,7 +1887,8 @@ export declare class Driver {
1777
1887
  /**
1778
1888
  * The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
1779
1889
  * it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
1780
- * never a copy — `tasks` is one slot per task id
1890
+ * not a copy of them; since W202 an entry a reference has read also holds its ONE shared copy (`OutputEntry.shared`,
1891
+ * `RN-R13` v0.33), and `payload` the run's payload's — `tasks` is one slot per task id
1781
1892
  * read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
1782
1893
  * node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
1783
1894
  * read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
@@ -1808,7 +1919,7 @@ export declare class Driver {
1808
1919
  *
1809
1920
  * **Extracted by [W42](../../doc/work-items/w42-what-a-decision-sees.md) rather
1810
1921
  * than copied.** An EDGE's `needs` resolves by exactly these rules — same grammar,
1811
- * same visibility predicate, same order, same per-key copy — and the ONE difference
1922
+ * same visibility predicate, same order, same shared copy (since W202, ADR 0126) — and the ONE difference
1812
1923
  * is that an edge has no `"."` form, which is a property of the caller rather than
1813
1924
  * of this expression. *Two callers, one resolution, so a change to what a reference
1814
1925
  * means cannot mean two different things to a worker and to a predicate*, which is
@@ -1819,12 +1930,21 @@ export declare class Driver {
1819
1930
  * @throws UnresolvedReference — `FD-R3`.
1820
1931
  */
1821
1932
  private resolveReference;
1933
+ /** **An output entry's shared copy**, made at its first read and kept on the entry (`OutputEntry.shared`) — counted. */
1934
+ private sharedOf;
1935
+ /**
1936
+ * **`"$payload"`'s value in a run: the run's ONE shared copy** — W202, ADR 0126, P41 decision 2: `sharedCopy` of the
1937
+ * header's `entryPayload` (`runPayload()`), made at the first `"$payload"` read of the run — or of a resume, whose driver
1938
+ * is a new one — and handed to every reader at both sites. Counted once. `FD-R4` does not come here: it composes from
1939
+ * the caller's own value, before a header exists, and keeps its clone.
1940
+ */
1941
+ private sharedPayload;
1822
1942
  /**
1823
1943
  * **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
1824
1944
  * caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
1825
1945
  * reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
1826
- * header exists. Read only where a node or an edge composes the token — a run declaring none never clones it
1827
- * (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
1946
+ * header exists. Read only where a node or an edge composes the token — a run declaring none never copies it
1947
+ * (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.
1828
1948
  */
1829
1949
  private runPayload;
1830
1950
  /**
@@ -1850,8 +1970,8 @@ export declare class Driver {
1850
1970
  * received anyway* is `ctx.outcome`, which it already has. So there is no
1851
1971
  * `edgeValue` parameter here and none is derivable — see the call site. **A
1852
1972
  * `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
1853
- * the NODE received but read from the run's header, `runPayload()` above, its own
1854
- * copy per key per read.
1973
+ * the NODE received but read from the run's header — since v0.47 the run's ONE shared
1974
+ * copy of it, `sharedPayload()` above, the object a worker's `"$payload"` key is handed (ADR 0126).
1855
1975
  *
1856
1976
  * @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
1857
1977
  */
@@ -2153,3 +2273,33 @@ export declare class Driver {
2153
2273
  effectiveOutcomeOf(taskId: string): OutcomeRecord | null;
2154
2274
  terminalStateOf(taskId: string): TerminalState | null;
2155
2275
  }
2276
+ /**
2277
+ * **What a node was HANDED, read after the fact by replaying the run to that node's composition** — `RN-R45`, W205,
2278
+ * [ADR 0128](../../doc/decisions/0128-what-a-node-was-handed-is-read-by-replaying-the-run-to-its-composition.md), P42
2279
+ * (a1)-(d1) and decisions 1-6. `run` is a run's stored header and events — `driver.checkpoint()`, or what a store appended
2280
+ * (`RN-R14`, `RN-R18`); `options` the registry and options the run's driver was built with (`runtime.registry`, plus its
2281
+ * `joinPolicy` where it was not `liveToken`).
2282
+ *
2283
+ * **It IS the driver's composition, not a second one**: a driver built for the stored run, deaf — no callback, nothing
2284
+ * handed to a scheduler — replays the stored events through `enter`, `compose` and `dispatch` exactly as a resume does and
2285
+ * stops at the target's composition. So an answer is EXACT, and it costs the run's prefix up to the target — O(run) at
2286
+ * worst, never O(the answer) — and a reading re-runs the run's predicates and gate types, as a resume does (ADR 0002 rule
2287
+ * 4, Q9: an impure one diverges, or answers what its re-run computes).
2288
+ *
2289
+ * | target | kind | `inputs` |
2290
+ * |---|---|---|
2291
+ * | `{ taskId }`, or `{ nodeId, branchPath }` | a task | ONE `envelope`: `{ settings, input, transform? }` — its one dispatch's, retries and a resolution included; BEFORE its transform (ADR 0051) |
2292
+ * | the same | an ask | ONE `received` — equal to its `asked` element's `input` |
2293
+ * | `{ nodeId, branchPath }` | a gate, fanout, map or end | ONE `received` — what `enter` composed for it on that path |
2294
+ * | `{ nodeId, branchPath }` | a join, at an ARRIVING branch's path | ONE `arrival` — what that branch carried in, a late arrival included |
2295
+ * | `{ nodeId, branchPath }` | a join, at its OPENER's path | ONE `collection` — what it handed on when it fired |
2296
+ * | `{ nodeId }` | any | EVERY one of the above the replay reached, in the order it reached them — one replay to the stored end |
2297
+ *
2298
+ * A path target answers the FIRST composition on its path: an end shared by two sibling regions can be reached twice on one
2299
+ * path (`end` is exempt from `FD-V19`), and only the node id alone lists both.
2300
+ *
2301
+ * **Never throws for the run or the target** — `InputReading`'s refusals; it throws what `new Driver` throws for a
2302
+ * definition or a registry that cannot run (`InvalidDefinition`, `MissingJoinPolicy`, a `RegistryError`), as a resume does.
2303
+ * **What it does not detect**: a definition or a registry changed AFTER the target — the cut verifies the prefix it replays.
2304
+ */
2305
+ export declare function inputOf(flow: FlowDefinition, options: InputOfOptions, run: Checkpoint, target: InputTarget): InputReading;
@@ -124,6 +124,14 @@ export interface NodeRun {
124
124
  * additive: a consumer building a `NodeRun` literal is unaffected.
125
125
  */
126
126
  input?: unknown;
127
+ /**
128
+ * **WHEN an ask was asked** — `RN-R36`, v0.35,
129
+ * [ADR 0130](../../doc/decisions/0130-a-regenerated-asked-element-keeps-the-stored-runs-time.md), W208, P43 (e1): its
130
+ * `asked` element's `observedAt`, KEPT after the answer as {@link NodeRun.input} is. **Present only on an ask's node, and
131
+ * only where its element carried one** — an element written before 0.11.0 carries none, so its entry has none. OPTIONAL
132
+ * and additive.
133
+ */
134
+ askedAt?: number;
127
135
  }
128
136
  export interface RegionRun {
129
137
  /** Branch paths opened, in the order the driver emitted them (`JP-R5`). */
@@ -351,11 +359,20 @@ export type FoldInput = {
351
359
  * (`RN-R19`): the replay re-enters the ask and compares the input it composes with the stored one (`RN-R20`). **The
352
360
  * first kind whose payload is a value of ANY shape**, so it is deep-copied on the way out (`freezeInput`, `RN-R17`).
353
361
  * **A new kind, BREAKING in 0.8.0 for a consumer switching on `kind` exhaustively** (P35 decision 8).
362
+ *
363
+ * **`observedAt` — WHEN the ask was asked** (v0.35, `RN-R37`,
364
+ * [ADR 0130](../../doc/decisions/0130-a-regenerated-asked-element-keeps-the-stored-runs-time.md), W208, P43 (e1)): the
365
+ * driver's injected `now` where the branch entered the ask — the clock an outcome record's `observedAt` is read from.
366
+ * **A replay KEEPS the stored run's** (`RN-R19`): within the stored prefix the regenerated element takes the stored
367
+ * element's `observedAt`, or carries none where the stored one carries none, so it compares equal (`RN-R20`); past the
368
+ * stored tail it is `now()`. **OPTIONAL**: an element written before 0.11.0 has none, and a consumer building one
369
+ * literally is unaffected. `CHECKPOINT_FORMAT` does not move.
354
370
  */
355
371
  | {
356
372
  kind: 'asked';
357
373
  taskId: string;
358
374
  input: unknown;
375
+ observedAt?: number;
359
376
  } | {
360
377
  kind: 'pathEnded';
361
378
  reason: 'end' | 'no-eligible-edge';
@@ -64,7 +64,7 @@
64
64
  import { FyflowScheduler } from 'fyflow-scheduler';
65
65
  import type { ResourceGroup, WorkerManager } from 'fyflow-scheduler';
66
66
  import { Driver } from '../driver/driver.js';
67
- import type { Checkpoint, CheckpointHeader, DriverOptions, RunStopOptions } from '../driver/driver.js';
67
+ import type { Checkpoint, CheckpointHeader, DriverOptions, InputReading, InputTarget, RunStopOptions } from '../driver/driver.js';
68
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';
@@ -361,6 +361,22 @@ export interface RunHandle {
361
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
362
  */
363
363
  node(taskId: string): NodeRun | null;
364
+ /**
365
+ * **What a node of this run was HANDED, read by replaying the run so far to that node's composition** — `RN-R45`, W205,
366
+ * [ADR 0128](../../doc/decisions/0128-what-a-node-was-handed-is-read-by-replaying-the-run-to-its-composition.md), P42
367
+ * decision 7: the root export {@link inputOf} over THIS run's own `driver.checkpoint()`, with the registry, `joinPolicy`
368
+ * and options this runtime built the run's driver with — one function, so a live answer and a stored one cannot differ.
369
+ * A task's answer is its dispatch's envelope `{ settings, input, transform? }`; an ask's, a gate's, a fanout's, a map's
370
+ * and an end's what reached it; a join's an arrival at an arriving branch's path or its collection at its opener's; a
371
+ * node id ALONE every path the run reached it on. A target not yet reached is `not-reached`.
372
+ *
373
+ * **Synchronous, O(the run's prefix), never cached**: each call copies the run's events (`checkpoint()`) and replays them
374
+ * on a driver of its own, deaf — nothing is appended to this run, nothing handed to a scheduler, no callback called, and
375
+ * no object of this run's reachable through an answer. Read it at a quiescent point (`RN-R12`); mid-step it reads the
376
+ * prefix appended so far. On a released run (`done` settled) it answers from what the run delivered. **BREAKING in
377
+ * 0.10.0 for code that BUILDS a `RunHandle`** (P42 decision 9) — a new member, as `state` was in 0.9.0.
378
+ */
379
+ inputOf(target: InputTarget): InputReading;
364
380
  }
365
381
  export interface Runtime {
366
382
  /** **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.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Flow orchestration over fyflow-scheduler: sequence, decision, fan-out and join",
5
5
  "keywords": [
6
6
  "workflow",
@@ -124,6 +124,7 @@
124
124
  },
125
125
  "license": "MIT",
126
126
  "devDependencies": {
127
+ "@cfworker/json-schema": "4.1.1",
127
128
  "@types/node": "^24.13.3",
128
129
  "esbuild": "^0.25.12",
129
130
  "fyflow-scheduler": "0.6.0",