@jarenjs/flow 0.67.0 → 0.72.2
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/README.md +7 -1
- package/docs/FLOW-FORMAT.md +25 -11
- package/package.json +3 -3
- package/schemas/jaren-dag.authoring.schema.json +212 -0
- package/schemas/jaren-dag.draft-07.schema.json +13 -6
- package/schemas/jaren-dag.schema.json +13 -6
- package/schemas/jaren-fsm.authoring.schema.json +130 -0
- package/schemas/jaren-fsm.draft-07.schema.json +5 -2
- package/schemas/jaren-fsm.schema.json +49 -13
- package/src/dag.js +14 -6
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ const dag = compileDag({
|
|
|
139
139
|
nodes: {
|
|
140
140
|
rows: { kind: 'input' },
|
|
141
141
|
adults: { kind: 'query',
|
|
142
|
-
query: { $for: { r: '$[*]' }, $where: { $ge: ['$r.age', 18] }, $return: '$r' } },
|
|
142
|
+
query: [{ $for: { r: '$[*]' }, $where: { $ge: ['$r.age', 18] }, $return: '$r' }] },
|
|
143
143
|
view: { kind: 'jslt',
|
|
144
144
|
stylesheet: [{ match: '$', body: ['ul', {},
|
|
145
145
|
[{ $for: { p: '$[*]' }, $return: ['li', {}, '$p.name'] }]] }] },
|
|
@@ -155,6 +155,10 @@ const dag = compileDag({
|
|
|
155
155
|
await dag.run(people); // ['ul', {}, [['li', {}, 'ada'], …]] — a vnode, as JSON
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
The query's outer array keeps `adults` an array for zero, one or many
|
|
159
|
+
rows. Without it, the engine's one-result sequence becomes that row;
|
|
160
|
+
the next node's `$[*]` would iterate its members instead of rows.
|
|
161
|
+
|
|
158
162
|
Cycles, port rules and the exactly-one-output rule are **compile-time**
|
|
159
163
|
rejections (JF0xxx with `docPath`); a `task` node's handler is resolved
|
|
160
164
|
at compile too, and called as `handler({ with, input }, signal)` with
|
|
@@ -270,8 +274,10 @@ Every subpath a consumer can import, derived from the manifest by
|
|
|
270
274
|
| Import | Kind | Declarations |
|
|
271
275
|
|---|---|---|
|
|
272
276
|
| `@jarenjs/flow` | JavaScript | declared |
|
|
277
|
+
| `@jarenjs/flow/schemas/jaren-dag.authoring.schema.json` | schema | — |
|
|
273
278
|
| `@jarenjs/flow/schemas/jaren-dag.draft-07.schema.json` | schema | — |
|
|
274
279
|
| `@jarenjs/flow/schemas/jaren-dag.schema.json` | schema | — |
|
|
280
|
+
| `@jarenjs/flow/schemas/jaren-fsm.authoring.schema.json` | schema | — |
|
|
275
281
|
| `@jarenjs/flow/schemas/jaren-fsm.draft-07.schema.json` | schema | — |
|
|
276
282
|
| `@jarenjs/flow/schemas/jaren-fsm.schema.json` | schema | — |
|
|
277
283
|
| `@jarenjs/flow/package.json` | metadata | — |
|
package/docs/FLOW-FORMAT.md
CHANGED
|
@@ -261,7 +261,7 @@ extending it is a format revision, not an option.
|
|
|
261
261
|
"nodes": {
|
|
262
262
|
"rows": { "kind": "input" },
|
|
263
263
|
"adults": { "kind": "query",
|
|
264
|
-
"query": { "$for": { "r": "$[*]" }, "$where": { "$ge": ["$r.age", 18] }, "$return": "$r" } },
|
|
264
|
+
"query": [{ "$for": { "r": "$[*]" }, "$where": { "$ge": ["$r.age", 18] }, "$return": "$r" }] },
|
|
265
265
|
"names": { "kind": "jslt",
|
|
266
266
|
"stylesheet": [{ "match": "$", "body": ["ul", {},
|
|
267
267
|
[{ "$for": { "p": "$[*]" }, "$return": ["li", {}, "$p.name"] }]] }] },
|
|
@@ -312,7 +312,15 @@ A node's `$` is decided by its inbound edges:
|
|
|
312
312
|
|
|
313
313
|
A delivery whose `select` yields the empty sequence delivers `null`; a
|
|
314
314
|
`query`/`jslt` node whose own result is empty likewise yields `null` —
|
|
315
|
-
`undefined` is not a JSON value and never flows through a graph.
|
|
315
|
+
`undefined` is not a JSON value and never flows through a graph. The
|
|
316
|
+
query engine's singleton rule also applies: one result is that value,
|
|
317
|
+
while two or more results become an array. To deliver an array for every
|
|
318
|
+
cardinality, wrap the query in an array constructor, as `adults.query`
|
|
319
|
+
does above. Its downstream `$[*]` therefore always iterates rows. The
|
|
320
|
+
stylesheet also collects its `li` children in an explicit array: zero,
|
|
321
|
+
one and two adults yield respectively `["ul", {}, []]`,
|
|
322
|
+
`["ul", {}, [["li", {}, "ada"]]]`, and
|
|
323
|
+
`["ul", {}, [["li", {}, "ada"], ["li", {}, "lin"]]]` for those names.
|
|
316
324
|
Values pass **by reference**: nodes and hosts MUST NOT mutate what
|
|
317
325
|
they receive.
|
|
318
326
|
|
|
@@ -425,19 +433,21 @@ await dag.run(input, { runId: 'run-42' });
|
|
|
425
433
|
runs twice after a crash is the caller's bug, bluntly. The
|
|
426
434
|
mitigation is an idempotency key threaded through the node's
|
|
427
435
|
`with` props and honoured by the effectful system itself.
|
|
428
|
-
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
436
|
+
- A standalone checkpoint store owns run identity: it must bind the run
|
|
437
|
+
id to the workflow, input and `taskVersions` before returning saved
|
|
438
|
+
values. `compileDag` validates the registry but cannot infer the
|
|
439
|
+
provenance of an arbitrary host store. Use the `@jarenjs/db` DAG job
|
|
440
|
+
runner for persisted identity comparison before node-value loading. Values recorded for node ids the current document does not
|
|
432
441
|
declare (or no longer declares `checkpoint`) are ignored.
|
|
433
442
|
|
|
434
443
|
### §7.8 Declared task versions
|
|
435
444
|
|
|
436
445
|
A checkpointed node's value is REPLAYED on a later run instead of being
|
|
437
446
|
recomputed. That is sound only while the handler that produced it is the
|
|
438
|
-
same handler.
|
|
439
|
-
declare `version`: a
|
|
440
|
-
implementation
|
|
447
|
+
same handler. Every `task` in a workflow containing any checkpoint must
|
|
448
|
+
declare `version`: a nonblank string naming its implementation identity.
|
|
449
|
+
This includes recomputed tasks: an upstream implementation can affect a
|
|
450
|
+
downstream checkpoint even when its own result is never saved.
|
|
441
451
|
|
|
442
452
|
```jsonc
|
|
443
453
|
{ "kind": "task", "run": "summarise", "version": "2026-09-05", "checkpoint": true }
|
|
@@ -463,9 +473,13 @@ declared identity it depends on, keyed by node id and SORTED, so two
|
|
|
463
473
|
compiles of the same document produce the same map — byte for byte —
|
|
464
474
|
whatever order the declarations were written in. A handler that is itself
|
|
465
475
|
a compiled workflow may expose its own `taskVersions`; those compose under
|
|
466
|
-
the node's path (`outer`, `outer/inner`),
|
|
476
|
+
the node's path (`outer`, `outer/inner`), with each node-id segment escaped
|
|
477
|
+
as JSON Pointer (`~` becomes `~0`, `/` becomes `~1`), so a composed run has one
|
|
467
478
|
identity rather than two. Every task id is retained as an own member,
|
|
468
|
-
including names inherited by ordinary JavaScript objects.
|
|
479
|
+
including names inherited by ordinary JavaScript objects. Registry nested
|
|
480
|
+
maps must contain nonblank string versions and valid escaped paths.
|
|
481
|
+
A literal `outer/inner` node is `outer~1inner`, distinct from a nested
|
|
482
|
+
`inner` task under `outer`.
|
|
469
483
|
|
|
470
484
|
```js
|
|
471
485
|
compileDag(doc, { tasks: { summarise: { version: '2026-09-05', run: handler } } })
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/flow",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.72.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"prepack": "npm run build:types"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@jarenjs/core": "^0.
|
|
53
|
-
"@jarenjs/json": "^0.
|
|
52
|
+
"@jarenjs/core": "^0.72.2",
|
|
53
|
+
"@jarenjs/json": "^0.72.2"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/jaren-dag/0.1/authoring",
|
|
4
|
+
"title": "Jaren dataflow graph 0.1 (authoring profile)",
|
|
5
|
+
"description": "Authoring profile of the canonical grammar: the document SHAPE only, with queryDocument, stylesheetDocument left open. Constrains decoding on small models, where the full grammar does not decode at all. It is deliberately WEAKER than the canonical schema - a document valid here may be nonsense - so the engine compiler is the gate that makes it safe, and generated documents must be compiled before use.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"$dag",
|
|
9
|
+
"nodes",
|
|
10
|
+
"edges"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"$dag": {
|
|
14
|
+
"description": "The dag format version; required.",
|
|
15
|
+
"const": "0.1"
|
|
16
|
+
},
|
|
17
|
+
"nodes": {
|
|
18
|
+
"description": "Node declarations, id to declaration. The id is what edges reference.",
|
|
19
|
+
"type": "object",
|
|
20
|
+
"additionalProperties": {
|
|
21
|
+
"$ref": "#/$defs/node"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"edges": {
|
|
25
|
+
"description": "The wiring: data flows from a node's result to a consumer's input scope, in document order for port-object assembly.",
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {
|
|
28
|
+
"$ref": "#/$defs/edge"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"$defs": {
|
|
33
|
+
"node": {
|
|
34
|
+
"anyOf": [
|
|
35
|
+
{
|
|
36
|
+
"description": "Yields the run's input value (null when absent). Accepts no inbound edge.",
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"kind": {
|
|
40
|
+
"const": "input"
|
|
41
|
+
},
|
|
42
|
+
"checkpoint": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"description": "Opt-in durable checkpointing (FLOW-FORMAT §7.6): asserts this node's output is JSON-serializable; recorded values are seeded on resume instead of re-evaluated."
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": [
|
|
48
|
+
"kind"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"description": "The run's result is this node's input scope value. Exactly one per document; no outbound edge.",
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"kind": {
|
|
56
|
+
"const": "output"
|
|
57
|
+
},
|
|
58
|
+
"checkpoint": {
|
|
59
|
+
"type": "boolean",
|
|
60
|
+
"description": "Opt-in durable checkpointing (FLOW-FORMAT §7.6): asserts this node's output is JSON-serializable; recorded values are seeded on resume instead of re-evaluated."
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"required": [
|
|
64
|
+
"kind"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"description": "Yields its literal value. Accepts no inbound edge.",
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"kind": {
|
|
72
|
+
"const": "const"
|
|
73
|
+
},
|
|
74
|
+
"value": {
|
|
75
|
+
"description": "Any JSON value, delivered by reference."
|
|
76
|
+
},
|
|
77
|
+
"checkpoint": {
|
|
78
|
+
"type": "boolean",
|
|
79
|
+
"description": "Opt-in durable checkpointing (FLOW-FORMAT §7.6): asserts this node's output is JSON-serializable; recorded values are seeded on resume instead of re-evaluated."
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"required": [
|
|
83
|
+
"kind",
|
|
84
|
+
"value"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"description": "A Jaren JSON Query evaluated with $ bound to the node's input scope; an empty result yields null.",
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"kind": {
|
|
92
|
+
"const": "query"
|
|
93
|
+
},
|
|
94
|
+
"query": {
|
|
95
|
+
"description": "The query document.",
|
|
96
|
+
"allOf": [
|
|
97
|
+
{
|
|
98
|
+
"$ref": "#/$defs/queryDocument"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"checkpoint": {
|
|
103
|
+
"type": "boolean",
|
|
104
|
+
"description": "Opt-in durable checkpointing (FLOW-FORMAT §7.6): asserts this node's output is JSON-serializable; recorded values are seeded on resume instead of re-evaluated."
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"required": [
|
|
108
|
+
"kind",
|
|
109
|
+
"query"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"description": "A JSLT stylesheet transforming the node's input scope; an empty result yields null.",
|
|
114
|
+
"type": "object",
|
|
115
|
+
"properties": {
|
|
116
|
+
"kind": {
|
|
117
|
+
"const": "jslt"
|
|
118
|
+
},
|
|
119
|
+
"stylesheet": {
|
|
120
|
+
"description": "The stylesheet document (bare rule array or envelope form).",
|
|
121
|
+
"allOf": [
|
|
122
|
+
{
|
|
123
|
+
"$ref": "#/$defs/stylesheetDocument"
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"checkpoint": {
|
|
128
|
+
"type": "boolean",
|
|
129
|
+
"description": "Opt-in durable checkpointing (FLOW-FORMAT §7.6): asserts this node's output is JSON-serializable; recorded values are seeded on resume instead of re-evaluated."
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"required": [
|
|
133
|
+
"kind",
|
|
134
|
+
"stylesheet"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"description": "A registered async handler, called as handler({ with, input }, signal); the handler must honor the run's shared AbortSignal.",
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"kind": {
|
|
142
|
+
"const": "task"
|
|
143
|
+
},
|
|
144
|
+
"run": {
|
|
145
|
+
"description": "The compile-time registry handler name.",
|
|
146
|
+
"type": "string",
|
|
147
|
+
"minLength": 1
|
|
148
|
+
},
|
|
149
|
+
"version": {
|
|
150
|
+
"description": "Declared nonblank handler identity (FLOW-FORMAT section 7.8). The compiler requires it on every task in a workflow containing checkpoints, including recomputed tasks that can affect saved downstream results. The registry must supply the same token; identity is never derived from source.",
|
|
151
|
+
"type": "string",
|
|
152
|
+
"minLength": 1,
|
|
153
|
+
"pattern": "\\S"
|
|
154
|
+
},
|
|
155
|
+
"with": {
|
|
156
|
+
"description": "Handler props: a query resolved against the node's input scope (null when absent or empty).",
|
|
157
|
+
"allOf": [
|
|
158
|
+
{
|
|
159
|
+
"$ref": "#/$defs/queryDocument"
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
"checkpoint": {
|
|
164
|
+
"type": "boolean",
|
|
165
|
+
"description": "Opt-in durable checkpointing (FLOW-FORMAT §7.6): asserts this node's output is JSON-serializable; recorded values are seeded on resume instead of re-evaluated."
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"required": [
|
|
169
|
+
"kind",
|
|
170
|
+
"run"
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
"edge": {
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {
|
|
178
|
+
"from": {
|
|
179
|
+
"description": "The producing node id.",
|
|
180
|
+
"type": "string"
|
|
181
|
+
},
|
|
182
|
+
"to": {
|
|
183
|
+
"description": "The consuming node id.",
|
|
184
|
+
"type": "string"
|
|
185
|
+
},
|
|
186
|
+
"port": {
|
|
187
|
+
"description": "Names this delivery in the consumer's input-scope object; when any inbound edge of a node is ported, all must be, uniquely.",
|
|
188
|
+
"type": "string",
|
|
189
|
+
"minLength": 1
|
|
190
|
+
},
|
|
191
|
+
"select": {
|
|
192
|
+
"description": "A query applied to the source value before delivery; an empty result delivers null.",
|
|
193
|
+
"allOf": [
|
|
194
|
+
{
|
|
195
|
+
"$ref": "#/$defs/queryDocument"
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": [
|
|
201
|
+
"from",
|
|
202
|
+
"to"
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
"queryDocument": {
|
|
206
|
+
"description": "A Jaren query expression; validate against the full grammar and compile locally."
|
|
207
|
+
},
|
|
208
|
+
"stylesheetDocument": {
|
|
209
|
+
"description": "A JSLT stylesheet; validate and compile locally."
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"description": "The query document.",
|
|
96
96
|
"allOf": [
|
|
97
97
|
{
|
|
98
|
-
"$ref": "
|
|
98
|
+
"$ref": "#/definitions/queryDocument"
|
|
99
99
|
}
|
|
100
100
|
]
|
|
101
101
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"description": "The stylesheet document (bare rule array or envelope form).",
|
|
121
121
|
"allOf": [
|
|
122
122
|
{
|
|
123
|
-
"$ref": "
|
|
123
|
+
"$ref": "#/definitions/stylesheetDocument"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -147,15 +147,16 @@
|
|
|
147
147
|
"minLength": 1
|
|
148
148
|
},
|
|
149
149
|
"version": {
|
|
150
|
-
"description": "
|
|
150
|
+
"description": "Declared nonblank handler identity (FLOW-FORMAT section 7.8). The compiler requires it on every task in a workflow containing checkpoints, including recomputed tasks that can affect saved downstream results. The registry must supply the same token; identity is never derived from source.",
|
|
151
151
|
"type": "string",
|
|
152
|
-
"minLength": 1
|
|
152
|
+
"minLength": 1,
|
|
153
|
+
"pattern": "\\S"
|
|
153
154
|
},
|
|
154
155
|
"with": {
|
|
155
156
|
"description": "Handler props: a query resolved against the node's input scope (null when absent or empty).",
|
|
156
157
|
"allOf": [
|
|
157
158
|
{
|
|
158
|
-
"$ref": "
|
|
159
|
+
"$ref": "#/definitions/queryDocument"
|
|
159
160
|
}
|
|
160
161
|
]
|
|
161
162
|
},
|
|
@@ -191,7 +192,7 @@
|
|
|
191
192
|
"description": "A query applied to the source value before delivery; an empty result delivers null.",
|
|
192
193
|
"allOf": [
|
|
193
194
|
{
|
|
194
|
-
"$ref": "
|
|
195
|
+
"$ref": "#/definitions/queryDocument"
|
|
195
196
|
}
|
|
196
197
|
]
|
|
197
198
|
}
|
|
@@ -200,6 +201,12 @@
|
|
|
200
201
|
"from",
|
|
201
202
|
"to"
|
|
202
203
|
]
|
|
204
|
+
},
|
|
205
|
+
"queryDocument": {
|
|
206
|
+
"$ref": "https://jarenjs.dev/schemas/jaren-query/0.1/draft-07"
|
|
207
|
+
},
|
|
208
|
+
"stylesheetDocument": {
|
|
209
|
+
"$ref": "https://jarenjs.dev/schemas/jaren-jslt/0.1/draft-07"
|
|
203
210
|
}
|
|
204
211
|
}
|
|
205
212
|
}
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"description": "The query document.",
|
|
96
96
|
"allOf": [
|
|
97
97
|
{
|
|
98
|
-
"$ref": "
|
|
98
|
+
"$ref": "#/$defs/queryDocument"
|
|
99
99
|
}
|
|
100
100
|
]
|
|
101
101
|
},
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
"description": "The stylesheet document (bare rule array or envelope form).",
|
|
121
121
|
"allOf": [
|
|
122
122
|
{
|
|
123
|
-
"$ref": "
|
|
123
|
+
"$ref": "#/$defs/stylesheetDocument"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -147,15 +147,16 @@
|
|
|
147
147
|
"minLength": 1
|
|
148
148
|
},
|
|
149
149
|
"version": {
|
|
150
|
-
"description": "
|
|
150
|
+
"description": "Declared nonblank handler identity (FLOW-FORMAT section 7.8). The compiler requires it on every task in a workflow containing checkpoints, including recomputed tasks that can affect saved downstream results. The registry must supply the same token; identity is never derived from source.",
|
|
151
151
|
"type": "string",
|
|
152
|
-
"minLength": 1
|
|
152
|
+
"minLength": 1,
|
|
153
|
+
"pattern": "\\S"
|
|
153
154
|
},
|
|
154
155
|
"with": {
|
|
155
156
|
"description": "Handler props: a query resolved against the node's input scope (null when absent or empty).",
|
|
156
157
|
"allOf": [
|
|
157
158
|
{
|
|
158
|
-
"$ref": "
|
|
159
|
+
"$ref": "#/$defs/queryDocument"
|
|
159
160
|
}
|
|
160
161
|
]
|
|
161
162
|
},
|
|
@@ -191,7 +192,7 @@
|
|
|
191
192
|
"description": "A query applied to the source value before delivery; an empty result delivers null.",
|
|
192
193
|
"allOf": [
|
|
193
194
|
{
|
|
194
|
-
"$ref": "
|
|
195
|
+
"$ref": "#/$defs/queryDocument"
|
|
195
196
|
}
|
|
196
197
|
]
|
|
197
198
|
}
|
|
@@ -200,6 +201,12 @@
|
|
|
200
201
|
"from",
|
|
201
202
|
"to"
|
|
202
203
|
]
|
|
204
|
+
},
|
|
205
|
+
"queryDocument": {
|
|
206
|
+
"$ref": "https://jarenjs.dev/schemas/jaren-query/0.1"
|
|
207
|
+
},
|
|
208
|
+
"stylesheetDocument": {
|
|
209
|
+
"$ref": "https://jarenjs.dev/schemas/jaren-jslt/0.1"
|
|
203
210
|
}
|
|
204
211
|
}
|
|
205
212
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://jarenjs.dev/schemas/jaren-fsm/0.1/authoring",
|
|
4
|
+
"title": "Jaren finite state machine 0.1 (authoring profile)",
|
|
5
|
+
"description": "Authoring profile of the canonical grammar: the document SHAPE only, with queryDocument left open. Constrains decoding on small models, where the full grammar does not decode at all. It is deliberately WEAKER than the canonical schema - a document valid here may be nonsense - so the engine compiler is the gate that makes it safe, and generated documents must be compiled before use.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"$fsm": {
|
|
9
|
+
"description": "The fsm format version; absent implies 0.1.",
|
|
10
|
+
"const": "0.1"
|
|
11
|
+
},
|
|
12
|
+
"initial": {
|
|
13
|
+
"description": "The id of the machine's initial state, or null when the document does not choose one (a session must then be started with an explicit state).",
|
|
14
|
+
"type": [
|
|
15
|
+
"string",
|
|
16
|
+
"null"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"states": {
|
|
20
|
+
"description": "Declared states, in document order. A string is shorthand for { id }.",
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": {
|
|
23
|
+
"anyOf": [
|
|
24
|
+
{
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"id": {
|
|
31
|
+
"description": "The state id transitions refer to.",
|
|
32
|
+
"type": "string"
|
|
33
|
+
},
|
|
34
|
+
"entry": {
|
|
35
|
+
"$ref": "#/$defs/effects"
|
|
36
|
+
},
|
|
37
|
+
"exit": {
|
|
38
|
+
"$ref": "#/$defs/effects"
|
|
39
|
+
},
|
|
40
|
+
"final": {
|
|
41
|
+
"description": "Marks a terminal state; a session reports done here.",
|
|
42
|
+
"type": "boolean"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": [
|
|
46
|
+
"id"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"transitions": {
|
|
53
|
+
"description": "The transition table. Document order IS the selection order: the first entry whose from-state, event (a null or absent event matches any name) and guard all match fires.",
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"from": {
|
|
59
|
+
"description": "The state this transition leaves.",
|
|
60
|
+
"type": "string"
|
|
61
|
+
},
|
|
62
|
+
"event": {
|
|
63
|
+
"description": "The event name this transition answers; null or absent matches any event (a wildcard). A transition never fires spontaneously.",
|
|
64
|
+
"type": [
|
|
65
|
+
"string",
|
|
66
|
+
"null"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"guard": {
|
|
70
|
+
"description": "A Jaren JSON Query document asserted by effective boolean value against the evaluation scope { state, event, payload, context }; null or absent means no guard. A plain string not starting with '$' is a literal and therefore always true — projection-produced display guards are vacuous by design.",
|
|
71
|
+
"anyOf": [
|
|
72
|
+
{
|
|
73
|
+
"type": "null"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"$ref": "#/$defs/queryDocument"
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"to": {
|
|
81
|
+
"description": "The state this transition enters.",
|
|
82
|
+
"type": "string"
|
|
83
|
+
},
|
|
84
|
+
"effects": {
|
|
85
|
+
"$ref": "#/$defs/effects"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"required": [
|
|
89
|
+
"from",
|
|
90
|
+
"to"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"required": [
|
|
96
|
+
"initial",
|
|
97
|
+
"states",
|
|
98
|
+
"transitions"
|
|
99
|
+
],
|
|
100
|
+
"$defs": {
|
|
101
|
+
"effects": {
|
|
102
|
+
"description": "Effect descriptors, returned as resolved data when their state or transition fires — the engine never executes them.",
|
|
103
|
+
"type": "array",
|
|
104
|
+
"items": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"run": {
|
|
108
|
+
"description": "The host-registered handler name.",
|
|
109
|
+
"type": "string",
|
|
110
|
+
"minLength": 1
|
|
111
|
+
},
|
|
112
|
+
"with": {
|
|
113
|
+
"description": "Handler props: a Jaren JSON Query document evaluated against the scope at fire time; an empty result omits the member.",
|
|
114
|
+
"allOf": [
|
|
115
|
+
{
|
|
116
|
+
"$ref": "#/$defs/queryDocument"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"required": [
|
|
122
|
+
"run"
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"queryDocument": {
|
|
127
|
+
"description": "A Jaren query expression; validate against the full grammar and compile locally."
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"type": "null"
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
|
-
"$ref": "
|
|
76
|
+
"$ref": "#/definitions/queryDocument"
|
|
77
77
|
}
|
|
78
78
|
]
|
|
79
79
|
},
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"description": "Handler props: a Jaren JSON Query document evaluated against the scope at fire time; an empty result omits the member.",
|
|
114
114
|
"allOf": [
|
|
115
115
|
{
|
|
116
|
-
"$ref": "
|
|
116
|
+
"$ref": "#/definitions/queryDocument"
|
|
117
117
|
}
|
|
118
118
|
]
|
|
119
119
|
}
|
|
@@ -122,6 +122,9 @@
|
|
|
122
122
|
"run"
|
|
123
123
|
]
|
|
124
124
|
}
|
|
125
|
+
},
|
|
126
|
+
"queryDocument": {
|
|
127
|
+
"$ref": "https://jarenjs.dev/schemas/jaren-query/0.1/draft-07"
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
130
|
}
|
|
@@ -11,14 +11,19 @@
|
|
|
11
11
|
},
|
|
12
12
|
"initial": {
|
|
13
13
|
"description": "The id of the machine's initial state, or null when the document does not choose one (a session must then be started with an explicit state).",
|
|
14
|
-
"type": [
|
|
14
|
+
"type": [
|
|
15
|
+
"string",
|
|
16
|
+
"null"
|
|
17
|
+
]
|
|
15
18
|
},
|
|
16
19
|
"states": {
|
|
17
20
|
"description": "Declared states, in document order. A string is shorthand for { id }.",
|
|
18
21
|
"type": "array",
|
|
19
22
|
"items": {
|
|
20
23
|
"anyOf": [
|
|
21
|
-
{
|
|
24
|
+
{
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
22
27
|
{
|
|
23
28
|
"type": "object",
|
|
24
29
|
"properties": {
|
|
@@ -26,14 +31,20 @@
|
|
|
26
31
|
"description": "The state id transitions refer to.",
|
|
27
32
|
"type": "string"
|
|
28
33
|
},
|
|
29
|
-
"entry": {
|
|
30
|
-
|
|
34
|
+
"entry": {
|
|
35
|
+
"$ref": "#/$defs/effects"
|
|
36
|
+
},
|
|
37
|
+
"exit": {
|
|
38
|
+
"$ref": "#/$defs/effects"
|
|
39
|
+
},
|
|
31
40
|
"final": {
|
|
32
41
|
"description": "Marks a terminal state; a session reports done here.",
|
|
33
42
|
"type": "boolean"
|
|
34
43
|
}
|
|
35
44
|
},
|
|
36
|
-
"required": [
|
|
45
|
+
"required": [
|
|
46
|
+
"id"
|
|
47
|
+
]
|
|
37
48
|
}
|
|
38
49
|
]
|
|
39
50
|
}
|
|
@@ -50,26 +61,42 @@
|
|
|
50
61
|
},
|
|
51
62
|
"event": {
|
|
52
63
|
"description": "The event name this transition answers; null or absent matches any event (a wildcard). A transition never fires spontaneously.",
|
|
53
|
-
"type": [
|
|
64
|
+
"type": [
|
|
65
|
+
"string",
|
|
66
|
+
"null"
|
|
67
|
+
]
|
|
54
68
|
},
|
|
55
69
|
"guard": {
|
|
56
70
|
"description": "A Jaren JSON Query document asserted by effective boolean value against the evaluation scope { state, event, payload, context }; null or absent means no guard. A plain string not starting with '$' is a literal and therefore always true — projection-produced display guards are vacuous by design.",
|
|
57
71
|
"anyOf": [
|
|
58
|
-
{
|
|
59
|
-
|
|
72
|
+
{
|
|
73
|
+
"type": "null"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"$ref": "#/$defs/queryDocument"
|
|
77
|
+
}
|
|
60
78
|
]
|
|
61
79
|
},
|
|
62
80
|
"to": {
|
|
63
81
|
"description": "The state this transition enters.",
|
|
64
82
|
"type": "string"
|
|
65
83
|
},
|
|
66
|
-
"effects": {
|
|
84
|
+
"effects": {
|
|
85
|
+
"$ref": "#/$defs/effects"
|
|
86
|
+
}
|
|
67
87
|
},
|
|
68
|
-
"required": [
|
|
88
|
+
"required": [
|
|
89
|
+
"from",
|
|
90
|
+
"to"
|
|
91
|
+
]
|
|
69
92
|
}
|
|
70
93
|
}
|
|
71
94
|
},
|
|
72
|
-
"required": [
|
|
95
|
+
"required": [
|
|
96
|
+
"initial",
|
|
97
|
+
"states",
|
|
98
|
+
"transitions"
|
|
99
|
+
],
|
|
73
100
|
"$defs": {
|
|
74
101
|
"effects": {
|
|
75
102
|
"description": "Effect descriptors, returned as resolved data when their state or transition fires — the engine never executes them.",
|
|
@@ -84,11 +111,20 @@
|
|
|
84
111
|
},
|
|
85
112
|
"with": {
|
|
86
113
|
"description": "Handler props: a Jaren JSON Query document evaluated against the scope at fire time; an empty result omits the member.",
|
|
87
|
-
"allOf": [
|
|
114
|
+
"allOf": [
|
|
115
|
+
{
|
|
116
|
+
"$ref": "#/$defs/queryDocument"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
88
119
|
}
|
|
89
120
|
},
|
|
90
|
-
"required": [
|
|
121
|
+
"required": [
|
|
122
|
+
"run"
|
|
123
|
+
]
|
|
91
124
|
}
|
|
125
|
+
},
|
|
126
|
+
"queryDocument": {
|
|
127
|
+
"$ref": "https://jarenjs.dev/schemas/jaren-query/0.1"
|
|
92
128
|
}
|
|
93
129
|
}
|
|
94
130
|
}
|
package/src/dag.js
CHANGED
|
@@ -95,7 +95,7 @@ function normalizeTaskEntry(entry, name) {
|
|
|
95
95
|
`compileDag: the registered handler '${name}' is not a function, nor { run, version }`);
|
|
96
96
|
}
|
|
97
97
|
if (entry.version !== undefined
|
|
98
|
-
&& (typeof entry.version !== 'string' || entry.version === '')) {
|
|
98
|
+
&& (typeof entry.version !== 'string' || entry.version.trim() === '')) {
|
|
99
99
|
throw new TypeError(
|
|
100
100
|
`compileDag: the registered handler '${name}' has a version that is not a non-empty string`);
|
|
101
101
|
}
|
|
@@ -103,6 +103,10 @@ function normalizeTaskEntry(entry, name) {
|
|
|
103
103
|
throw new TypeError(
|
|
104
104
|
`compileDag: the registered handler '${name}' has a taskVersions that is not an object`);
|
|
105
105
|
}
|
|
106
|
+
if (entry.taskVersions !== undefined && Object.entries(entry.taskVersions).some(([path, version]) =>
|
|
107
|
+
/~(?:[^01]|$)/.test(path) || typeof version !== 'string' || version.trim() === '')) {
|
|
108
|
+
throw new TypeError(`compileDag: the registered handler '${name}' has invalid taskVersions paths or versions`);
|
|
109
|
+
}
|
|
106
110
|
return {
|
|
107
111
|
run: entry.run,
|
|
108
112
|
version: entry.version ?? null,
|
|
@@ -156,6 +160,7 @@ export function compileDag(doc, options) {
|
|
|
156
160
|
/** @type {Map<string, any>} */
|
|
157
161
|
const nodes = new Map();
|
|
158
162
|
const order = Object.keys(doc.nodes);
|
|
163
|
+
const durable = order.some((id) => doc.nodes[id]?.checkpoint === true);
|
|
159
164
|
for (const id of order) {
|
|
160
165
|
const decl = doc.nodes[id];
|
|
161
166
|
const base = `/nodes/${encodeJSONPointerSegment(id)}`;
|
|
@@ -202,7 +207,7 @@ export function compileDag(doc, options) {
|
|
|
202
207
|
`task node '${id}' must carry a non-empty string "run"`, `${base}/run`);
|
|
203
208
|
}
|
|
204
209
|
if (decl.version !== undefined
|
|
205
|
-
&& (typeof decl.version !== 'string' || decl.version === '')) {
|
|
210
|
+
&& (typeof decl.version !== 'string' || decl.version.trim() === '')) {
|
|
206
211
|
throw new FlowCompileError('JF0011',
|
|
207
212
|
`task node '${id}' has a "version" member that is not a non-empty string`,
|
|
208
213
|
`${base}/version`);
|
|
@@ -212,9 +217,9 @@ export function compileDag(doc, options) {
|
|
|
212
217
|
// same implementation. That identity is declared, never derived:
|
|
213
218
|
// hashing a closure's source would call a reformat a new task and
|
|
214
219
|
// a changed dependency the same one
|
|
215
|
-
if (
|
|
220
|
+
if (durable && decl.version === undefined) {
|
|
216
221
|
throw new FlowCompileError('JF0011',
|
|
217
|
-
`task node '${id}'
|
|
222
|
+
`task node '${id}' belongs to a workflow with checkpoints, so it must also declare a "version" — `
|
|
218
223
|
+ 'a checkpointed result is replayed only while the handler that produced it is '
|
|
219
224
|
+ 'the same one, and that identity has to be stated', `${base}/version`);
|
|
220
225
|
}
|
|
@@ -461,6 +466,8 @@ export function compileDag(doc, options) {
|
|
|
461
466
|
loaded = await checkpoint.load(runId);
|
|
462
467
|
}
|
|
463
468
|
catch (err) {
|
|
469
|
+
if (signal !== undefined && onAbort !== null)
|
|
470
|
+
signal.removeEventListener('abort', onAbort);
|
|
464
471
|
const cause = asError(err);
|
|
465
472
|
throw new FlowRuntimeError('JF2009',
|
|
466
473
|
`the checkpoint store failed to load run '${runId}': ${cause.message}`,
|
|
@@ -631,10 +638,11 @@ export function compileDag(doc, options) {
|
|
|
631
638
|
const versions = {};
|
|
632
639
|
for (const node of nodes.values()) {
|
|
633
640
|
if (node.kind !== 'task' || node.version === null) continue;
|
|
634
|
-
|
|
641
|
+
const pathPrefix = encodeJSONPointerSegment(node.id);
|
|
642
|
+
setObjectMember(versions, pathPrefix, node.version);
|
|
635
643
|
if (node.nestedVersions === null) continue;
|
|
636
644
|
for (const [path, version] of Object.entries(node.nestedVersions)) {
|
|
637
|
-
setObjectMember(versions, `${
|
|
645
|
+
setObjectMember(versions, `${pathPrefix}/${path}`, version);
|
|
638
646
|
}
|
|
639
647
|
}
|
|
640
648
|
const taskVersions = Object.freeze(Object.fromEntries(
|