fyflow-orchestration 0.10.0 → 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.
- package/AGENTS.md +173 -5
- package/README.md +1 -1
- package/dist/browser/chunks/{chunk-NA6QQAZ2.js → chunk-5CVSWDCB.js} +79 -12
- package/dist/browser/index.js +539 -3
- package/dist/browser/scheduler/index.js +1 -1
- package/dist/node/chunks/{chunk-NA6QQAZ2.js → chunk-5CVSWDCB.js} +79 -12
- package/dist/node/index.js +539 -3
- package/dist/node/scheduler/index.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/src/definition/definition-schema.d.ts +23 -0
- package/dist/types/src/definition/types.d.ts +2 -1
- package/dist/types/src/definition/validate.d.ts +41 -2
- package/dist/types/src/definition/wiring.d.ts +10 -0
- package/dist/types/src/driver/driver.d.ts +1 -1
- package/dist/types/src/fold/run-state.d.ts +17 -0
- package/package.json +2 -1
package/AGENTS.md
CHANGED
|
@@ -347,6 +347,7 @@ that object and nothing else — there is no wrapper and no reserved key.
|
|
|
347
347
|
| `"someNode"` | that node's result, **on your own branch** |
|
|
348
348
|
| `["someNode"]` | that node's results as an **array**, in ascending branch-path order. Legal only when you are OUTSIDE a region that node is in — which is what makes it more than one |
|
|
349
349
|
| **a `?` on either of those** — `"someNode?"`, `["someNode?"]` | the same thing, except that **nothing to give leaves the key out** (or gives `[]` for the array form) instead of stopping the run. This is how you say *if it ran* |
|
|
350
|
+
| **`!error` after the node id, on any of those** — `"someNode!error"`, `["someNode!error?"]`, … | that node's **ERROR** instead of its result: the string its failure was reduced to, `null` where it succeeded (since 0.11.0, below) |
|
|
350
351
|
|
|
351
352
|
**What a reference hands you is ONE shared, frozen copy — read it, never write it** — from 0.10.0 ([ADR 0126](doc/decisions/0126-a-references-value-is-one-frozen-copy-shared-by-every-reader.md),
|
|
352
353
|
`spec/flow-definition.md` v0.47). The value under a `"someNode"` or `["someNode"]` key (either with `?`) is ONE deep copy
|
|
@@ -691,6 +692,83 @@ const [finding] = validateDefinition(outside, registry);
|
|
|
691
692
|
if (finding?.rule !== 'FD-V49' || !finding.detail.includes('move this node inside the map')) throw new Error(`expected FD-V49, got ${finding?.rule}`);
|
|
692
693
|
```
|
|
693
694
|
|
|
695
|
+
### `"someNode!error"` — what FAILED, from the nodes after it
|
|
696
|
+
|
|
697
|
+
**`"lookup!error"` names node `lookup`'s ERROR** — the outcome's `error`, the string the scheduler reduced the failure to
|
|
698
|
+
(section 6) — where `"lookup"` names its result (`spec/flow-definition.md` v0.49,
|
|
699
|
+
[ADR 0131](doc/decisions/0131-a-reference-may-name-a-nodes-error.md), since 0.11.0). A failed task's result is `null`, so
|
|
700
|
+
until it a node after a failure edge — an ask asking a person *what now?* above all — could not say what failed.
|
|
701
|
+
|
|
702
|
+
| | |
|
|
703
|
+
|---|---|
|
|
704
|
+
| what it is | the effective outcome's `error`: the record's, or a RESOLUTION's where a person resolved the node (`resolve(id, 'failed', { error })` on a parked task, a declined ask; `'resolved as failed'` where they gave none). **`null` where the output SUCCEEDED** — the key is present, holding `null` |
|
|
705
|
+
| its forms | the plain reference's four: `"lookup!error"`, `"lookup!error?"`, `["lookup!error"]`, `["lookup!error?"]` — the `?` goes LAST. It names the same node's same outputs as `"lookup"`, so `FD-V27`-`FD-V29` and the `?` and array rules are the plain form's, and the fixes `FD-V28` and `FD-V29` name keep the `!error` (`FD-V27` names the node, as for `"lookup"`) |
|
|
706
|
+
| where it is legal | wherever `"lookup"` is: a `task`'s, a `map`'s or an `ask`'s `needs`, and an edge's (`ctx.needs`, section 4) |
|
|
707
|
+
| what it is NOT | a path: the error is a field of the OUTCOME beside `result`, and `error` is the one part a reference may name. `"lookup?!error"`, `"!error"` and `"lookup!err"` are `FD-V26`, each naming its fix |
|
|
708
|
+
| what it costs | nothing to copy — a string or `null`, never the shared frozen copy of the result (above) |
|
|
709
|
+
| in the wiring check | the key composes as `{ type: ['string', 'null'] }` with the sample `''`, whatever the producer declares (section 15) |
|
|
710
|
+
| `referenceTarget` | the error form parses to the plain form's object plus `part: 'error'`, present ONLY there (`AimedReference`), so every other spelling parses as before. A resolver of your own must read `part`; an aiming rule may ignore it |
|
|
711
|
+
|
|
712
|
+
**The default — a failure edge handing `{ error }` to its successor — was declined** (P43 (c2)): it would change what every
|
|
713
|
+
existing failure edge's successor receives. You name the error where you want it.
|
|
714
|
+
|
|
715
|
+
The app's case — `lookup` fails, an ask after its failure edge shows the person why, and the node both roads reach reads
|
|
716
|
+
the error beside the result:
|
|
717
|
+
|
|
718
|
+
```ts run
|
|
719
|
+
import { Driver, fold, liveToken, validateDefinition, type FlowDefinition } from 'fyflow-orchestration';
|
|
720
|
+
|
|
721
|
+
const added: { id: string; payload: { input: unknown } }[] = [];
|
|
722
|
+
const listeners = new Map<string, ((e: { detail: unknown }) => void)[]>();
|
|
723
|
+
const scheduler = {
|
|
724
|
+
addTask(task: unknown) { added.push(task as { id: string; payload: { input: unknown } }); },
|
|
725
|
+
addEventListener(type: string, fn: (e: { detail: unknown }) => void) { listeners.set(type, [...(listeners.get(type) ?? []), fn]); },
|
|
726
|
+
};
|
|
727
|
+
const handedInput = (id: string) => added.find((t) => t.id === id)?.payload.input;
|
|
728
|
+
const fire = (type: string, detail: unknown) => { for (const fn of listeners.get(type) ?? []) fn({ detail }); };
|
|
729
|
+
|
|
730
|
+
const flow: FlowDefinition = {
|
|
731
|
+
flowId: 'lookup-or-ask', version: 1, entry: 'lookup',
|
|
732
|
+
nodes: [
|
|
733
|
+
// `optional`, so a failure SETTLES failed and routes the failure edge (a non-optional task would park, section 7).
|
|
734
|
+
{ id: 'lookup', kind: 'task', type: 'Work', optional: true, edges: [{ on: ['failed'], to: 'resolve' }, { to: 'publish' }] },
|
|
735
|
+
{ id: 'resolve', kind: 'ask', needs: { why: 'lookup!error' },
|
|
736
|
+
answerSchema: { type: 'object', required: ['value'], properties: { value: { type: 'string' } } },
|
|
737
|
+
edges: [{ on: ['failed'], to: 'skipped' }, { to: 'publish' }] },
|
|
738
|
+
{ id: 'publish', kind: 'task', type: 'Work', needs: { found: 'lookup', why: 'lookup!error', given: 'resolve?' }, edges: [{ to: 'done' }] },
|
|
739
|
+
{ id: 'skipped', kind: 'end', outcome: 'skipped' },
|
|
740
|
+
{ id: 'done', kind: 'end', outcome: 'completed' },
|
|
741
|
+
],
|
|
742
|
+
};
|
|
743
|
+
const types = { workers: { w: { url: 'worker.js' } }, nodeTypes: { Work: { worker: 'w' } } };
|
|
744
|
+
const registry = { predicates: [], ...types };
|
|
745
|
+
if (validateDefinition(flow, registry).length) throw new Error('"lookup!error" is legal on an ask and on a task');
|
|
746
|
+
|
|
747
|
+
const driver = new Driver(flow, { runId: 'r1', scheduler, createTask: (c) => c, predicates: new Map(), joinPolicy: liveToken, ...types });
|
|
748
|
+
driver.start({ invoice: 'INV-?' });
|
|
749
|
+
fire('task.failed', { id: 'r1:lookup:-:0', state: 'failed', result: null, error: 'lookup timed out after 30 s' });
|
|
750
|
+
|
|
751
|
+
// The ask parks with the error: what it shows the person.
|
|
752
|
+
const asked = driver.foldInputs().find((e) => e.kind === 'asked');
|
|
753
|
+
console.log(asked?.kind === 'asked' && asked.input); // { why: 'lookup timed out after 30 s' }
|
|
754
|
+
if (JSON.stringify(asked?.kind === 'asked' && asked.input) !== '{"why":"lookup timed out after 30 s"}') throw new Error('the ask is handed the error');
|
|
755
|
+
|
|
756
|
+
driver.resolve('r1:resolve:-:0', 'done', { result: { value: 'INV-9' }, actor: 'ana' });
|
|
757
|
+
console.log(handedInput('r1:publish:-:0')); // { found: null, why: 'lookup timed out after 30 s', given: { value: 'INV-9' } }
|
|
758
|
+
if (JSON.stringify(handedInput('r1:publish:-:0')) !== '{"found":null,"why":"lookup timed out after 30 s","given":{"value":"INV-9"}}') {
|
|
759
|
+
throw new Error(`publish received ${JSON.stringify(handedInput('r1:publish:-:0'))}`);
|
|
760
|
+
}
|
|
761
|
+
if (fold(driver.foldInputs()).outcome !== 'running') throw new Error('publish is in flight');
|
|
762
|
+
|
|
763
|
+
// A part other than `error`, or the `?` in the wrong place, is refused naming the fix.
|
|
764
|
+
const refuse = (ref: unknown) => validateDefinition({ ...flow, nodes: flow.nodes.map((n) =>
|
|
765
|
+
(n.id === 'publish' ? { ...n, needs: { why: ref } } : n)) } as FlowDefinition, registry).map((f) => `${f.rule}: ${f.detail}`);
|
|
766
|
+
for (const [ref, fix] of [['lookup!err', 'write "lookup!error"'], ['lookup?!error', 'write "lookup!error?"']] as const) {
|
|
767
|
+
const [finding] = refuse(ref);
|
|
768
|
+
if (!finding?.startsWith('FD-V26') || !finding.includes(fix)) throw new Error(`${ref}: ${finding}`);
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
694
772
|
### Validation happens at construction, not at routing time
|
|
695
773
|
|
|
696
774
|
`new Driver(...)` validates the whole definition before anything is queued. A typo cannot become a
|
|
@@ -724,6 +802,82 @@ It would have said *every path from a branch entry reaches the join* — which w
|
|
|
724
802
|
that routes a branch away from its join invalid input, and that case is the one this library exists
|
|
725
803
|
to get right (`ADR 0003`). Do not add it.
|
|
726
804
|
|
|
805
|
+
### The definition's SHAPE, as data — `definitionSchema()` and `definitionSchema(registry)`
|
|
806
|
+
|
|
807
|
+
**`definitionSchema()` returns everything above as a JSON Schema, draft 2020-12** (`spec/flow-definition.md` v0.50,
|
|
808
|
+
[ADR 0132](doc/decisions/0132-a-definitions-shape-is-a-schema-built-from-the-validator.md), since 0.11.0): every node kind
|
|
809
|
+
and its keys, which are required, the edges of each kind, the three `when` shapes, the `needs` grammar (`"lookup!error"`
|
|
810
|
+
included), a `retryPolicy`, a `transform` and an `answerSchema` — each key with a `description`. Hand it to any 2020-12
|
|
811
|
+
validator, or to an agent writing a flow, BEFORE the findings. It is BUILT from the validator's own key sets, so its
|
|
812
|
+
keys cannot drift from them (the reference tokens and the operator partition are written beside them and held to the
|
|
813
|
+
validator by its tests); it is a new plain object on every call, yours to change. A join's `of` and a fanout's branch
|
|
814
|
+
entries are held to the id charset only where they are strings — the validator does no more today
|
|
815
|
+
([Q774](doc/questions/q774.md)).
|
|
816
|
+
|
|
817
|
+
| | |
|
|
818
|
+
|---|---|
|
|
819
|
+
| it refuses | what a node, an edge or a value gets wrong in its OWN shape: an undeclared or missing key, a wrong type, an id outside `[A-Za-z0-9_-]{1,64}`, an unknown kind, a malformed reference, `when`, `retryPolicy` or `transform`, a task's key on an ask, an `answerSchema` outside the subset |
|
|
820
|
+
| it does not | anything that needs ANOTHER node or the REGISTRY — whether `to` names a node, reachability, regions and joins, where a reference points, every registered name (types, pools, groups, languages, predicates, gate types and their routes) — the last is what `definitionSchema(registry)` adds, below |
|
|
821
|
+
| the authority | **`validateDefinition(flow, registry)`**, or `runtime.check(flow)` on a runtime — for all of that and for the whole. What the schema refuses, `validateDefinition` refuses under every registry; a document it accepts may still be refused there. The root `description` says so |
|
|
822
|
+
| not | a check the library runs (nothing evaluates it), nor in [ADR 0053](doc/decisions/0053-the-schema-validator-is-a-built-in-subset-and-a-keyword-outside-it-is-refused.md)'s subset — it uses `$defs`, `$ref`, `oneOf`, `if`/`then`, `pattern` |
|
|
823
|
+
|
|
824
|
+
**`definitionSchema(registry)` is the same shape SPECIALISED to the `Registry` you validate against** (`spec/flow-definition.md`
|
|
825
|
+
v0.51, since 0.11.0) — what an agent most often gets wrong, as data: a task's `type` is one of your node types, and per
|
|
826
|
+
type (an `if`/`then`) its `pool`, its `settings` against its worker's `settingsSchema`, and a `transform` where the type only
|
|
827
|
+
transforms; `workerGroups`, `transform.lang` and a predicate name from your sets; a gate's `type`, and per gate type its
|
|
828
|
+
`settings` and — for a static type — its edges' `route` names. A registry `checkRegistry` refuses (what `new Driver` would
|
|
829
|
+
refuse) is never specialised: the call throws that refusal. What it refuses, `validateDefinition(flow, registry)` refuses.
|
|
830
|
+
|
|
831
|
+
| | |
|
|
832
|
+
|---|---|
|
|
833
|
+
| it cannot cleanly carry | the `settings` check over the EFFECTIVE settings (your type's defaults merged under the node's): a key the defaults supply is not required of the node, and a default's own value — and a top-level keyword of your settingsSchema other than `properties`, `required` and `additionalProperties` — is not checked; a `minLength` above 1 inside one of your schemas, emitted as half of it rounded up (your schemas count UTF-16 units, JSON Schema counts code points); a DERIVED gate type's routes (`switch`'s are its cases); every route wired EXACTLY ONCE; a gate type's `settingsProblem` and a language's `compile` |
|
|
834
|
+
| annotation only | each node type's worker `inputSchema` — at `$defs.inputSchema` in its type's branch, constraining nothing (it is runtime input) |
|
|
835
|
+
| the authority | as above, against that registry. The root `description` names every row of this table |
|
|
836
|
+
| mind | a validator that tests a property with `in` (`@cfworker/json-schema` 4.1.1 does) misreads a schema property of yours NAMED as an `Object.prototype` member — `constructor`, `toString`, `__proto__` — and refuses or throws where 2020-12 accepts; name your settings otherwise |
|
|
837
|
+
|
|
838
|
+
```ts run
|
|
839
|
+
import { definitionSchema, validateDefinition, type FlowDefinition } from 'fyflow-orchestration';
|
|
840
|
+
|
|
841
|
+
const schema = definitionSchema();
|
|
842
|
+
console.log(schema.$schema); // https://json-schema.org/draft/2020-12/schema
|
|
843
|
+
if (schema.$schema !== 'https://json-schema.org/draft/2020-12/schema') throw new Error('a 2020-12 document');
|
|
844
|
+
if (!String(schema.description).includes('validateDefinition')) throw new Error('it names the authority for the rest');
|
|
845
|
+
|
|
846
|
+
type Def = { properties: Record<string, { description?: string }>; required: string[]; additionalProperties: boolean };
|
|
847
|
+
const defs = schema.$defs as Record<string, Def> & { reference: { anyOf: { pattern?: string }[] } };
|
|
848
|
+
console.log(defs.askNode.required); // [ 'id', 'kind', 'edges' ]
|
|
849
|
+
if (defs.askNode.additionalProperties !== false || 'type' in defs.askNode.properties) throw new Error('an ask declares no type');
|
|
850
|
+
if (!defs.taskNode.properties.retryPolicy?.description) throw new Error('every key is described');
|
|
851
|
+
|
|
852
|
+
// The reference grammar, as a pattern any validator can apply — the error form accepted, a misplaced "?" not.
|
|
853
|
+
const reference = new RegExp(defs.reference.anyOf[0]!.pattern!);
|
|
854
|
+
if (!reference.test('lookup!error?') || reference.test('lookup?!error')) throw new Error('the "!error" grammar');
|
|
855
|
+
|
|
856
|
+
// A shape the schema accepts can still be refused: an edge to a node that does not exist is the VALIDATOR's.
|
|
857
|
+
const dangling = { flowId: 'f', version: 1, entry: 'a', nodes: [{ id: 'a', kind: 'task', type: 'Work', edges: [{ to: 'ghost' }] }] } as FlowDefinition;
|
|
858
|
+
const rules = validateDefinition(dangling, { predicates: [], workers: { w: { url: 'worker.js' } }, nodeTypes: { Work: { worker: 'w' } } }).map((f) => f.rule);
|
|
859
|
+
if (rules.join() !== 'FD-V2') throw new Error(`the graph is validateDefinition's: ${rules.join()}`);
|
|
860
|
+
if (definitionSchema() === schema) throw new Error('a new object per call');
|
|
861
|
+
|
|
862
|
+
// Specialised to a registry: its node types (the built-in Transform beside yours), each type's pools and settings.
|
|
863
|
+
const registry = {
|
|
864
|
+
predicates: ['isUrgent'],
|
|
865
|
+
workers: { w: { url: 'worker.js', settingsSchema: { type: 'object', properties: { depth: { type: 'integer', minimum: 1 } }, additionalProperties: false } } },
|
|
866
|
+
nodeTypes: { Work: { worker: 'w' } },
|
|
867
|
+
};
|
|
868
|
+
const specialised = definitionSchema(registry);
|
|
869
|
+
type Task = { properties: { type: { enum: string[] } }; allOf: { then: { properties: { pool: { enum: string[] }; settings?: unknown } } }[] };
|
|
870
|
+
const task = (specialised.$defs as { taskNode: Task }).taskNode;
|
|
871
|
+
console.log(task.properties.type.enum); // [ 'Work', 'Transform' ]
|
|
872
|
+
console.log(task.allOf[0]!.then.properties.pool.enum); // [ 'Work' ]
|
|
873
|
+
if (task.allOf[0]!.then.properties.settings === undefined) throw new Error('Work\'s settings are held to its worker\'s schema');
|
|
874
|
+
if (!String(specialised.description).includes('EXACTLY ONCE')) throw new Error('it names what it cannot carry');
|
|
875
|
+
// A registry new Driver would refuse is never specialised.
|
|
876
|
+
let refused = '';
|
|
877
|
+
try { definitionSchema({ ...registry, nodeTypes: { Lost: { worker: 'missing' } } }); } catch (e) { refused = (e as Error).name; }
|
|
878
|
+
if (refused !== 'WorkerMissing') throw new Error(`a refused registry throws its refusal: ${refused}`);
|
|
879
|
+
```
|
|
880
|
+
|
|
727
881
|
---
|
|
728
882
|
|
|
729
883
|
## 4. Decisions and predicates
|
|
@@ -860,7 +1014,7 @@ flagged low confidence* was not expressible until v0.12 —
|
|
|
860
1014
|
answering [Q8](doc/questions/q8.md).
|
|
861
1015
|
|
|
862
1016
|
**An edge may carry `needs`, and its predicate sees the result under `ctx.needs`.** Same grammar as
|
|
863
|
-
a node's (section 3) — a node id, or a one-element array holding one,
|
|
1017
|
+
a node's (section 3) — a node id, or a one-element array holding one, since 0.8.0 `"$payload"` and `"$item"`, and since 0.11.0 a node's error, `"n!error"` —
|
|
864
1018
|
**minus `"."`**, which an edge has no use for: *what a predicate would have received anyway* is `ctx.outcome`. `"$payload"`
|
|
865
1019
|
and `"$item"` are not refused there: a predicate has no other road to the run's payload or to its map instance's item
|
|
866
1020
|
(section 3's two subsections route on them).
|
|
@@ -1468,10 +1622,17 @@ keeps waiting, and the run is not stuck.
|
|
|
1468
1622
|
([W180](doc/work-items/w180-asking-and-answering-in-a-run.md), [ADR 0109](doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md), `spec/run-state.md` `RN-R36`/`RN-R37`):
|
|
1469
1623
|
|
|
1470
1624
|
- **Nothing is handed to the scheduler** — the scheduler never sees the ask's id. The driver appends ONE fold input,
|
|
1471
|
-
`{ kind: 'asked', taskId, input }`; the fold reads the node `suspended` and the run `awaiting_action`, exactly the
|
|
1625
|
+
`{ kind: 'asked', taskId, input, observedAt? }`; the fold reads the node `suspended` and the run `awaiting_action`, exactly the
|
|
1472
1626
|
words a parked task gives (so `done` waits, below), and the node's entry in `nodes` carries **`input`** — what reached
|
|
1473
1627
|
the ask, its `needs` composed — kept after the answer. Tell an ask from a parked failure by the node's KIND, or by the
|
|
1474
1628
|
`asked` element, never by an error string. In a fan-out the ask's branch is LIVE, so **its join waits for the person**.
|
|
1629
|
+
- **WHEN it was asked** (since 0.11.0, [ADR 0130](doc/decisions/0130-a-regenerated-asked-element-keeps-the-stored-runs-time.md)):
|
|
1630
|
+
the element's `observedAt` is the driver's `now` — the clock an outcome record's `observedAt` reads — and the node's
|
|
1631
|
+
entry carries it as **`askedAt`**, kept after the answer as `input` is. **A resume KEEPS the stored time**: the replay
|
|
1632
|
+
regenerates the element and takes its stored `observedAt`, so a wait measured across a resume does not restart; past
|
|
1633
|
+
the stored tail it is the resumed driver's `now`. An `asked` element written before 0.11.0 has none, and its run
|
|
1634
|
+
resumes with none. **A driver older than 0.11.0 refuses a newer checkpoint `diverged` at its first ask** — resume a
|
|
1635
|
+
run on a driver at least as new as the one that wrote it.
|
|
1475
1636
|
- **The answer is `driver.resolve(taskId, …)`** — the same call as below. `'done'` with `result` the answer, held to the
|
|
1476
1637
|
ask's `answerSchema` (refused as `BoundaryRefused` with NOTHING appended and the ask still parked — answer again;
|
|
1477
1638
|
`validateBoundaries: false` turns it off); `'failed'` with an `error` is the person DECLINING, held to nothing.
|
|
@@ -1484,7 +1645,7 @@ keeps waiting, and the run is not stuck.
|
|
|
1484
1645
|
run's log, once per ask — that element keeps what reached the ask, whatever its size. A predicate's `ctx.outcome` for an ask has `attempts: 0` and
|
|
1485
1646
|
**`schedulerTimestamp`, `startTime`, `endTime` and `executionTime` all `null`** — no scheduler was involved.
|
|
1486
1647
|
- **BREAKING in 0.8.0** (P35 decision 8): `FoldInput` gains the kind `'asked'`, and `OutcomeRecord.schedulerTimestamp`
|
|
1487
|
-
is `number | null` — `null` only on an ask's effective outcome. `NodeRun.input` is optional and additive.
|
|
1648
|
+
is `number | null` — `null` only on an ask's effective outcome. `NodeRun.input` is optional and additive — as, since 0.11.0, are the element's `observedAt` and `NodeRun.askedAt`.
|
|
1488
1649
|
- **An answer given from a callback on the driver's own step** — `onFoldInput` on the `asked` element included — is
|
|
1489
1650
|
checked at once (a refusal is thrown or `false` returned right there) and, accepted, `resolve()` returns `true` and the
|
|
1490
1651
|
resolution is appended when the step ends: so the run's checkpoint resumes. A second answer while it is pending is
|
|
@@ -1543,6 +1704,9 @@ console.log(added, parked.outcome, parked.nodes['r1:approve:-:0'].input);
|
|
|
1543
1704
|
// [ 'r1:draft:-:0' ] awaiting_action { text: 'a draft' }
|
|
1544
1705
|
if (added.join() !== 'r1:draft:-:0' || parked.outcome !== 'awaiting_action') throw new Error('the ask should park with nothing dispatched');
|
|
1545
1706
|
if (JSON.stringify(parked.nodes['r1:approve:-:0'].input) !== '{"text":"a draft"}') throw new Error('the ask keeps what reached it');
|
|
1707
|
+
// And WHEN it was asked: the element's `observedAt` (the driver's clock, `Date.now()` by default) is the entry's `askedAt`.
|
|
1708
|
+
const askedEl = driver.foldInputs().find((e) => e.kind === 'asked');
|
|
1709
|
+
if (askedEl?.kind !== 'asked' || typeof askedEl.observedAt !== 'number' || parked.nodes['r1:approve:-:0'].askedAt !== askedEl.observedAt) throw new Error('the ask keeps when it was asked');
|
|
1546
1710
|
|
|
1547
1711
|
// An answer its schema refuses is thrown at you BY NAME — nothing appended, the ask still parked.
|
|
1548
1712
|
let refused: unknown = null;
|
|
@@ -2035,7 +2199,7 @@ AFTER the target — or one that moves only an envelope, even at the target: the
|
|
|
2035
2199
|
answers with the definition you hand it. **It re-runs your predicates and gate types**, as a resume does. The value is
|
|
2036
2200
|
the reading's own — a value a `needs` reference composed is frozen, as every reader's is (section 3), the rest yours —
|
|
2037
2201
|
and nothing of a live run is reachable through it. **The input a task's worker sees AFTER its transform is not answered**
|
|
2038
|
-
([Q760](doc/questions/q760.md)): hand the envelope's `input` to `tryTransform` (section 15). **BREAKING in 0.10.0**:
|
|
2202
|
+
([Q760](doc/questions/q760.md), resolved by P43: the road serves): hand the envelope's `input` to `tryTransform` (section 15). **BREAKING in 0.10.0**:
|
|
2039
2203
|
`RunHandle` gains `inputOf`, which breaks only code that BUILDS a handle.
|
|
2040
2204
|
|
|
2041
2205
|
```ts run
|
|
@@ -2979,7 +3143,7 @@ if (!haltedRun.haltedBy?.startsWith('UncloneableResult')) throw new Error(`halte
|
|
|
2979
3143
|
| ~~**map-style fan-out**~~ — **BUILT** since [W33](doc/work-items/w33-a-map-at-run-time-arity.md); see section 3 |
|
|
2980
3144
|
| ~~**RUNNING an `ask`**~~ — a node that waits for a person, its answer at `resolve()` | **BUILT** since [W180](doc/work-items/w180-asking-and-answering-in-a-run.md): a run parks at an ask with nothing handed to the scheduler, and its answer is its result — section 7. Under a cancel, a stop, a pause and a resume an ask is a parked task — never in flight, so nothing is ever sent or handed on for it — since [W181](doc/work-items/w181-an-ask-under-run-control.md) (section 7) |
|
|
2981
3145
|
| a **live, mutable view** of a run's state, or the state handed to `onFoldInput` beside each input | **no** — [P39](doc/plans/p39-what-a-step-costs-and-where-a-live-run-state-is-read.md) (b), [ADR 0122](doc/decisions/0122-a-runtimes-live-state-is-a-frozen-copy-on-its-handle.md): `handle.state()` is a frozen COPY, cached until the run moves, and `handle.outcome()` / `handle.node()` are the O(1) reads — section 8 |
|
|
2982
|
-
| a task's INPUT kept in the run state — `NodeRun.input` for a task, or `pathEnded` carrying the value that reached an end | **no** — [ADR 0046](doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md): a run keeps what its tasks returned. **READ it instead**, since [W205](doc/work-items/w205-what-a-node-was-handed-read-by-replaying-the-run.md): `inputOf(flow, options, { header, events }, target)` or `handle.inputOf(target)` replays the run to that node — section 8. The input a worker sees AFTER its transform: **no** ([Q760](doc/questions/q760.md)) |
|
|
3146
|
+
| a task's INPUT kept in the run state — `NodeRun.input` for a task, or `pathEnded` carrying the value that reached an end | **no** — [ADR 0046](doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md): a run keeps what its tasks returned. **READ it instead**, since [W205](doc/work-items/w205-what-a-node-was-handed-read-by-replaying-the-run.md): `inputOf(flow, options, { header, events }, target)` or `handle.inputOf(target)` replays the run to that node — section 8. The input a worker sees AFTER its transform: **no** ([Q760](doc/questions/q760.md), resolved — the envelope's `input` through `tryTransform` serves) |
|
|
2983
3147
|
| a **join timeout** | nothing. A stuck join is detected by the `stuck` outcome, with no clock |
|
|
2984
3148
|
| **per-join policy choice** | one policy per driver |
|
|
2985
3149
|
| a **declared event family** for `fyflow-react-components` | `onRegionEvent`, `onFoldInput` and `joinStatusOf()` — see below |
|
|
@@ -4149,6 +4313,7 @@ whatever arcs reach the node, on the entry node too** (W166, P37 stop-and-ask (d
|
|
|
4149
4313
|
| `shape` | `'given'` — not an arc; `checkWiring` never produces it |
|
|
4150
4314
|
| `producer` | **absent** — the value came from no node (and `through` too) |
|
|
4151
4315
|
| `sample` | `{ source: 'given', value, result }` — the value you gave, and what the transform made of it; no `arity`. **Never `'example'` or `'witness'`** |
|
|
4316
|
+
| `cause` | **on `transform-fails` only** (since 0.11.0, every road — `checkWiring`'s arcs too): `'threw'` — the worker half did not import, had no default export, or its `compile` or `evaluate` threw — or `'schema'`, a result the node's `inputSchema` refused — the type `TransformFailureCause`, a root export. Absent on every other kind. Read it rather than `why`'s prefix |
|
|
4152
4317
|
| `kind` | `transform-passes`, or `transform-fails` with the finding against the node's worker `inputSchema` — or naming the error, where the expression does not compile or throws on this value; `unchecked` for a language, type, worker or worker schema the registry lacks, as on an arc, naming which — and for a `null` candidate, which takes away any transform the node declares too: there is nothing to run the value through, and holding that value to the `inputSchema` is `findings(schema, value)` |
|
|
4153
4318
|
|
|
4154
4319
|
**The value is `$` as you give it: no `needs` is composed over it**, because a `needs` composes what
|
|
@@ -4205,6 +4370,7 @@ if (entry!.kind !== 'transform-passes') throw new Error(`expected the given valu
|
|
|
4205
4370
|
const [wrong] = await tryTransform(registry, flow, 'read', rename, { input: { folder: 3 } });
|
|
4206
4371
|
console.log(`${wrong!.kind}: ${wrong!.why}`); // transform-fails: the transform's result on the given input fails the consumer's inputSchema — $.source expected string, ...
|
|
4207
4372
|
if (wrong!.kind !== 'transform-fails' || !wrong!.why.includes('$.source expected string')) throw new Error(`expected the finding, got ${wrong!.why}`);
|
|
4373
|
+
if (wrong!.cause !== 'schema') throw new Error(`a result the schema refused is cause 'schema', got ${wrong!.cause}`);
|
|
4208
4374
|
|
|
4209
4375
|
// THE INNER NODE. Without an input its one arc runs on `reader`'s declared EXAMPLE; given a value, it runs on that.
|
|
4210
4376
|
const join = { lang: 'jsonata', expr: '{ "text": $join(lines, " ") }' };
|
|
@@ -4217,6 +4383,8 @@ if (JSON.stringify(mine!.sample!.result) !== JSON.stringify({ text: 'typed by ha
|
|
|
4217
4383
|
// An expression the engine refuses: `transform-fails`, naming the throw — the compile error, on the given input.
|
|
4218
4384
|
const [broken] = await tryTransform(registry, flow, 'sum', { lang: 'jsonata', expr: '{{{' }, { input: { lines: [] } });
|
|
4219
4385
|
if (broken!.kind !== 'transform-fails' || !broken!.why.startsWith('the transform threw on the given input')) throw new Error(`expected the compile error, got ${broken!.why}`);
|
|
4386
|
+
// The same, as DATA (since 0.11.0): `cause` is 'threw' — no result was produced — where the finding above is 'schema'.
|
|
4387
|
+
if (broken!.cause !== 'threw' || entry!.cause !== undefined) throw new Error(`a throw is cause 'threw', and a passing verdict has none — got ${broken!.cause}, ${entry!.cause}`);
|
|
4220
4388
|
```
|
|
4221
4389
|
|
|
4222
4390
|
**BREAKING in 0.8.0** (P37 decision 10): **`SampleSource` gains `'given'`**, **`WiringShape` gains
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ worker returned, routing can **fan out** into parallel branches, and a **join**
|
|
|
17
17
|
The scheduler runs tasks; this library decides **which task runs next**. `fyflow-scheduler` has no
|
|
18
18
|
dependency graph of its own, so sequence, decision, fan-out and join live here.
|
|
19
19
|
|
|
20
|
-
Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.
|
|
20
|
+
Published on npm as `fyflow-orchestration` and on JSR as `@fyflow/orchestration`. The manifests read `0.11.0`.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -416,7 +416,10 @@ function evaluateDeclared(when, scope, unknownOp) {
|
|
|
416
416
|
// src/definition/validate.ts
|
|
417
417
|
var ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
418
418
|
var KINDS = ["task", "fanout", "map", "join", "end", "gate", "ask"];
|
|
419
|
+
var ID_RE_COPY = new RegExp(ID_RE.source, ID_RE.flags);
|
|
420
|
+
var KINDS_COPY = Object.freeze([...KINDS]);
|
|
419
421
|
var ROUTABLE = ["done", "failed"];
|
|
422
|
+
var ROUTABLE_STATES = [...ROUTABLE];
|
|
420
423
|
var DOC_KEYS = ["flowId", "version", "entry", "nodes"];
|
|
421
424
|
var NODE_KEYS = {
|
|
422
425
|
// `retryPolicy` and `workerGroups` since v0.16 (W58): what a node says about its OWN task,
|
|
@@ -451,6 +454,11 @@ var TASK_KEYS_DECLARED = [...NODE_KEYS.task.req, ...NODE_KEYS.task.opt];
|
|
|
451
454
|
var NODE_KEYS_DECLARED = Object.fromEntries(
|
|
452
455
|
Object.entries(NODE_KEYS).map(([kind, k]) => [kind, [...k.req, ...k.opt]])
|
|
453
456
|
);
|
|
457
|
+
var NODE_KEYS_REQUIRED = Object.fromEntries(
|
|
458
|
+
Object.entries(NODE_KEYS).map(([kind, k]) => [kind, [...k.req]])
|
|
459
|
+
);
|
|
460
|
+
var EDGE_KEYS_REQUIRED = [...EDGE_KEYS.req];
|
|
461
|
+
var DOC_KEYS_DECLARED = [...DOC_KEYS];
|
|
454
462
|
var RETRY_POLICY_KEYS = ["maxRetries", "backoffMs"];
|
|
455
463
|
function retryPolicyProblem(v) {
|
|
456
464
|
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
@@ -958,10 +966,35 @@ function referenceTarget(ref) {
|
|
|
958
966
|
}
|
|
959
967
|
return null;
|
|
960
968
|
}
|
|
969
|
+
var ERROR_PART = "!error";
|
|
961
970
|
function aimed(s, form) {
|
|
962
971
|
const conditional = s.endsWith("?");
|
|
963
|
-
const
|
|
964
|
-
|
|
972
|
+
const body = conditional ? s.slice(0, -1) : s;
|
|
973
|
+
const error = body.endsWith(ERROR_PART);
|
|
974
|
+
const node = error ? body.slice(0, -ERROR_PART.length) : body;
|
|
975
|
+
if (!ID_RE.test(node)) return null;
|
|
976
|
+
return error ? { form, node, conditional, part: "error" } : { form, node, conditional };
|
|
977
|
+
}
|
|
978
|
+
var spelled = (t, conditional) => `${t.node}${t.part === "error" ? ERROR_PART : ""}${conditional ? "?" : ""}`;
|
|
979
|
+
function bangProblem(ref) {
|
|
980
|
+
const array = Array.isArray(ref) && ref.length === 1;
|
|
981
|
+
const one = array ? ref[0] : ref;
|
|
982
|
+
if (typeof one !== "string" || one.startsWith("$") || !one.includes("!")) return null;
|
|
983
|
+
const wrap = (s) => JSON.stringify(array ? [s] : s);
|
|
984
|
+
const at = one.indexOf("!");
|
|
985
|
+
const before = one.slice(0, at);
|
|
986
|
+
const after = one.slice(at + 1);
|
|
987
|
+
const afterBare = after.endsWith("?") ? after.slice(0, -1) : after;
|
|
988
|
+
if (afterBare === "error" && before.endsWith("?") && ID_RE.test(before.slice(0, -1))) {
|
|
989
|
+
return `${JSON.stringify(ref)} carries "?" before "!error", and the suffix goes last: write ${wrap(`${before.slice(0, -1)}!error?`)}`;
|
|
990
|
+
}
|
|
991
|
+
if (before === "") {
|
|
992
|
+
return `${JSON.stringify(ref)} names no node before "!": write the node whose error you mean first, as ${wrap(`<node>!error${one.endsWith("?") ? "?" : ""}`)}`;
|
|
993
|
+
}
|
|
994
|
+
if (afterBare !== "error" && ID_RE.test(before)) {
|
|
995
|
+
return `${JSON.stringify(ref)} names "${after}" after "!", and the one part of a node's outcome a reference may name is its error: write ${wrap(`${before}!error${after.endsWith("?") ? "?" : ""}`)}, or ${wrap(`${before}${after.endsWith("?") ? "?" : ""}`)} for its result`;
|
|
996
|
+
}
|
|
997
|
+
return null;
|
|
965
998
|
}
|
|
966
999
|
var PAYLOAD_TOKEN = "$payload";
|
|
967
1000
|
var ITEM_TOKEN = "$item";
|
|
@@ -1252,10 +1285,13 @@ function validateDefinition(doc, registry) {
|
|
|
1252
1285
|
if (!ID_RE.test(k)) add("FD-V26", where, `needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
|
|
1253
1286
|
const target = referenceTarget(ref);
|
|
1254
1287
|
const dollar = target === null ? dollarProblem(ref) : null;
|
|
1288
|
+
const bang = target === null && dollar === null ? bangProblem(ref) : null;
|
|
1255
1289
|
if (dollar !== null) {
|
|
1256
1290
|
add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${dollar}`);
|
|
1291
|
+
} else if (bang !== null) {
|
|
1292
|
+
add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${bang}`);
|
|
1257
1293
|
} else if (target === null) {
|
|
1258
|
-
add("FD-V26", where, `needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "." , "$payload", "$item", a node id with an optional "?" suffix, or a one-element array holding one`);
|
|
1294
|
+
add("FD-V26", where, `needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "." , "$payload", "$item", a node id with an optional "!error" and an optional "?" suffix, or a one-element array holding one`);
|
|
1259
1295
|
}
|
|
1260
1296
|
}
|
|
1261
1297
|
}
|
|
@@ -1318,10 +1354,13 @@ function validateDefinition(doc, registry) {
|
|
|
1318
1354
|
if (!ID_RE.test(k)) add("FD-V26", at, `edge needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
|
|
1319
1355
|
const target = referenceTarget(ref);
|
|
1320
1356
|
const dollar = target === null ? dollarProblem(ref) : null;
|
|
1357
|
+
const bang = target === null && dollar === null ? bangProblem(ref) : null;
|
|
1321
1358
|
if (dollar !== null) {
|
|
1322
1359
|
add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${dollar}`);
|
|
1360
|
+
} else if (bang !== null) {
|
|
1361
|
+
add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${bang}`);
|
|
1323
1362
|
} else if (target === null) {
|
|
1324
|
-
add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "$payload", "$item", a node id with an optional "?" suffix or a one-element array holding one`);
|
|
1363
|
+
add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "$payload", "$item", a node id with an optional "!error" and an optional "?" suffix or a one-element array holding one`);
|
|
1325
1364
|
} else if (target.form === "self") {
|
|
1326
1365
|
add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ".", which an EDGE may not use: what a predicate would have received anyway is its node's own outcome, and \`ctx.outcome\` already carries it`);
|
|
1327
1366
|
}
|
|
@@ -1502,14 +1541,14 @@ function validateDefinition(doc, registry) {
|
|
|
1502
1541
|
add(
|
|
1503
1542
|
"FD-V29",
|
|
1504
1543
|
at,
|
|
1505
|
-
`${site}[${JSON.stringify(k)}] names "${
|
|
1544
|
+
`${site}[${JSON.stringify(k)}] names "${spelled(t, false)}" unconditionally, and a run can reach this node without "${target}" having run \u2014 a decision routes around it, or it is inside a region whose branch may be lost before it is entered. \`FD-V27\` checked that this node is reachable FROM it, which is weaker. Write "${spelled(t, true)}"${t.form === "all" ? " inside the array" : ""} if you mean *if it ran*, and read the key as ${t.form === "all" ? "a possibly-empty array" : "possibly absent"}; otherwise move the reference to a node every path reaches`
|
|
1506
1545
|
);
|
|
1507
1546
|
}
|
|
1508
1547
|
const many = regions.some((r) => r.opensWith === "map" && r.members.has(target) && !r.members.has(n.id));
|
|
1509
1548
|
if (many && t.form === "one") {
|
|
1510
|
-
add("FD-V28", at, `${site}[${JSON.stringify(k)}] names "${
|
|
1549
|
+
add("FD-V28", at, `${site}[${JSON.stringify(k)}] names "${spelled(t, false)}" as one value, but it is inside a map region this node is outside of, so it produces one output per ITEM; write ["${spelled(t, false)}"]`);
|
|
1511
1550
|
} else if (!many && t.form === "all") {
|
|
1512
|
-
add("FD-V28", at, `${site}[${JSON.stringify(k)}] names ["${
|
|
1551
|
+
add("FD-V28", at, `${site}[${JSON.stringify(k)}] names ["${spelled(t, false)}"] as a list, but it produces exactly one output on this node's branch path; write "${spelled(t, false)}"`);
|
|
1513
1552
|
}
|
|
1514
1553
|
}
|
|
1515
1554
|
}
|
|
@@ -2025,6 +2064,7 @@ var typesOf = (s) => {
|
|
|
2025
2064
|
var widen = (ts) => ts.flatMap((t) => t === "integer" ? ["integer"] : t === "number" ? ["number", "integer"] : [t]);
|
|
2026
2065
|
var canon = (v) => JSON.stringify(v);
|
|
2027
2066
|
var BRANCH_KEY = /^\d+(\.\d+)*$/;
|
|
2067
|
+
var ERROR_SCHEMA = Object.freeze({ type: ["string", "null"] });
|
|
2028
2068
|
function fits(P, C, path = "$") {
|
|
2029
2069
|
const p = P;
|
|
2030
2070
|
const c = C;
|
|
@@ -2419,6 +2459,11 @@ var Wiring = class {
|
|
|
2419
2459
|
value = a.sample?.value;
|
|
2420
2460
|
src = a.sample?.source ?? "witness";
|
|
2421
2461
|
dot.push(key);
|
|
2462
|
+
} else if (t.part === "error") {
|
|
2463
|
+
schema2 = ERROR_SCHEMA;
|
|
2464
|
+
const w = sampleOf(ERROR_SCHEMA);
|
|
2465
|
+
value = w.value;
|
|
2466
|
+
src = w.source;
|
|
2422
2467
|
} else {
|
|
2423
2468
|
const p = this.produced(t.node);
|
|
2424
2469
|
if (p.schema === void 0) return { ...base, unchecked: `needs.${key} names "${t.node}", which ${p.unchecked}` };
|
|
@@ -2538,6 +2583,7 @@ async function judgeTransform(v, node, transform, type, consumer, consumerWhy, l
|
|
|
2538
2583
|
for (const input of inputs) results.push(await c.evaluate(input, settings));
|
|
2539
2584
|
} catch (e) {
|
|
2540
2585
|
v.kind = "transform-fails";
|
|
2586
|
+
v.cause = "threw";
|
|
2541
2587
|
v.why = `the transform threw on the ${sourceWord(sample.source)}: ${e instanceof Error ? e.message : String(e)}`;
|
|
2542
2588
|
v.sample = { ...sample, result: e instanceof Error ? e.message : String(e) };
|
|
2543
2589
|
return;
|
|
@@ -2551,6 +2597,7 @@ async function judgeTransform(v, node, transform, type, consumer, consumerWhy, l
|
|
|
2551
2597
|
const bad = results.map((r, i) => [i, findings(consumer, r)]).find(([, f]) => f.length > 0);
|
|
2552
2598
|
if (bad) {
|
|
2553
2599
|
v.kind = "transform-fails";
|
|
2600
|
+
v.cause = "schema";
|
|
2554
2601
|
v.why = `the transform's result on the ${sourceWord(sample.source)}${perItem ? ` (item ${bad[0]})` : ""} fails the consumer's inputSchema \u2014 ${describeFindings(bad[1])}`;
|
|
2555
2602
|
} else {
|
|
2556
2603
|
v.kind = "transform-passes";
|
|
@@ -3046,6 +3093,11 @@ function sameData(a, b) {
|
|
|
3046
3093
|
const kb = Object.keys(rb).filter((k) => rb[k] !== void 0);
|
|
3047
3094
|
return ka.length === kb.length && ka.every((k) => Object.hasOwn(rb, k) && sameData(ra[k], rb[k]));
|
|
3048
3095
|
}
|
|
3096
|
+
function storedAskedAt(e) {
|
|
3097
|
+
if (e === void 0 || e === null || typeof e !== "object" || e.kind !== "asked") return void 0;
|
|
3098
|
+
const t = e.observedAt;
|
|
3099
|
+
return typeof t === "number" ? t : void 0;
|
|
3100
|
+
}
|
|
3049
3101
|
function isFed(e) {
|
|
3050
3102
|
return e.kind === "outcome" || e.kind === "resolution" || e.kind === "resumed" || e.kind === "interrupted" || e.kind === "paused" || e.kind === "unpaused" || e.kind === "attempted";
|
|
3051
3103
|
}
|
|
@@ -5408,7 +5460,7 @@ var Driver = class _Driver {
|
|
|
5408
5460
|
}
|
|
5409
5461
|
/**
|
|
5410
5462
|
* **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
|
|
5411
|
-
* was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
|
|
5463
|
+
* 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
|
|
5412
5464
|
* in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
|
|
5413
5465
|
*
|
|
5414
5466
|
* **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
|
|
@@ -5441,7 +5493,7 @@ var Driver = class _Driver {
|
|
|
5441
5493
|
}
|
|
5442
5494
|
return;
|
|
5443
5495
|
}
|
|
5444
|
-
const entry = { path: t.path, result: eff.result };
|
|
5496
|
+
const entry = { path: t.path, result: eff.result, error: eff.error };
|
|
5445
5497
|
if (t.entry !== null && list !== void 0) {
|
|
5446
5498
|
list[list.indexOf(t.entry)] = entry;
|
|
5447
5499
|
this.reindex(c, t.nodeId, t.entry, entry);
|
|
@@ -5616,6 +5668,9 @@ var Driver = class _Driver {
|
|
|
5616
5668
|
}
|
|
5617
5669
|
const c = this.outputsCache;
|
|
5618
5670
|
c.examined += t.form === "all" ? visible.length : 1;
|
|
5671
|
+
if (t.part === "error") {
|
|
5672
|
+
return t.form === "all" ? visible.map((e) => e.error) : visible[0].error;
|
|
5673
|
+
}
|
|
5619
5674
|
return t.form === "all" ? visible.map((e) => this.sharedOf(c, e)) : this.sharedOf(c, visible[0]);
|
|
5620
5675
|
}
|
|
5621
5676
|
/** **An output entry's shared copy**, made at its first read and kept on the entry (`OutputEntry.shared`) — counted. */
|
|
@@ -5782,7 +5837,10 @@ var Driver = class _Driver {
|
|
|
5782
5837
|
throw new DuplicateTaskId(`"${taskId}" was already asked in run "${runId}" \u2014 an ask's id is minted once per path (ID-R1)`);
|
|
5783
5838
|
}
|
|
5784
5839
|
this.asked.add(taskId);
|
|
5785
|
-
|
|
5840
|
+
const replay = this.replay;
|
|
5841
|
+
const at = this.inputs.length;
|
|
5842
|
+
const observedAt = replay !== null && at < replay.stored.length ? storedAskedAt(replay.stored[at]) : this.now();
|
|
5843
|
+
this.append(observedAt === void 0 ? { kind: "asked", taskId, input: cloneValue(received) } : { kind: "asked", taskId, input: cloneValue(received), observedAt });
|
|
5786
5844
|
if (this.shut()) return;
|
|
5787
5845
|
const region = this.regionOf(node.id);
|
|
5788
5846
|
if (region) {
|
|
@@ -6676,7 +6734,9 @@ var Accumulator = class _Accumulator {
|
|
|
6676
6734
|
state: "suspended",
|
|
6677
6735
|
result: null,
|
|
6678
6736
|
error: null,
|
|
6679
|
-
input: structuredClone(input.input)
|
|
6737
|
+
input: structuredClone(input.input),
|
|
6738
|
+
// `RN-R36` v0.35 (W208, ADR 0130): WHEN it was asked, where the element says — and only there.
|
|
6739
|
+
...input.observedAt !== void 0 ? { askedAt: input.observedAt } : {}
|
|
6680
6740
|
});
|
|
6681
6741
|
return this.withOutcome();
|
|
6682
6742
|
}
|
|
@@ -6744,7 +6804,8 @@ var Accumulator = class _Accumulator {
|
|
|
6744
6804
|
state: r.resolvedAs,
|
|
6745
6805
|
result: r.result,
|
|
6746
6806
|
error: r.error,
|
|
6747
|
-
...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
|
|
6807
|
+
...existing !== void 0 && "input" in existing ? { input: existing.input } : {},
|
|
6808
|
+
...existing?.askedAt !== void 0 ? { askedAt: existing.askedAt } : {}
|
|
6748
6809
|
});
|
|
6749
6810
|
}
|
|
6750
6811
|
this.runId = this.runId ?? runId;
|
|
@@ -6920,10 +6981,16 @@ export {
|
|
|
6920
6981
|
findings,
|
|
6921
6982
|
describeFindings,
|
|
6922
6983
|
readKeyPath,
|
|
6984
|
+
ID_RE_COPY,
|
|
6985
|
+
KINDS_COPY,
|
|
6986
|
+
ROUTABLE_STATES,
|
|
6923
6987
|
ASK_REFUSED_KEYS,
|
|
6924
6988
|
EDGE_KEYS_DECLARED,
|
|
6925
6989
|
TASK_KEYS_DECLARED,
|
|
6926
6990
|
NODE_KEYS_DECLARED,
|
|
6991
|
+
NODE_KEYS_REQUIRED,
|
|
6992
|
+
EDGE_KEYS_REQUIRED,
|
|
6993
|
+
DOC_KEYS_DECLARED,
|
|
6927
6994
|
RETRY_POLICY_KEYS,
|
|
6928
6995
|
retryPolicyProblem,
|
|
6929
6996
|
BUILT_IN_NODE_TYPES,
|