@jarenjs/flow 0.67.0 → 0.72.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/README.md CHANGED
@@ -270,8 +270,10 @@ Every subpath a consumer can import, derived from the manifest by
270
270
  | Import | Kind | Declarations |
271
271
  |---|---|---|
272
272
  | `@jarenjs/flow` | JavaScript | declared |
273
+ | `@jarenjs/flow/schemas/jaren-dag.authoring.schema.json` | schema | — |
273
274
  | `@jarenjs/flow/schemas/jaren-dag.draft-07.schema.json` | schema | — |
274
275
  | `@jarenjs/flow/schemas/jaren-dag.schema.json` | schema | — |
276
+ | `@jarenjs/flow/schemas/jaren-fsm.authoring.schema.json` | schema | — |
275
277
  | `@jarenjs/flow/schemas/jaren-fsm.draft-07.schema.json` | schema | — |
276
278
  | `@jarenjs/flow/schemas/jaren-fsm.schema.json` | schema | — |
277
279
  | `@jarenjs/flow/package.json` | metadata | — |
@@ -425,19 +425,21 @@ await dag.run(input, { runId: 'run-42' });
425
425
  runs twice after a crash is the caller's bug, bluntly. The
426
426
  mitigation is an idempotency key threaded through the node's
427
427
  `with` props and honoured by the effectful system itself.
428
- - Resuming under a DIFFERENT document than the one that saved is
429
- undefined behaviour keep the document stable with the run (the
430
- `@jarenjs/db` queue stores it on the job row for exactly this
431
- reason). Values recorded for node ids the current document does not
428
+ - A standalone checkpoint store owns run identity: it must bind the run
429
+ id to the workflow, input and `taskVersions` before returning saved
430
+ values. `compileDag` validates the registry but cannot infer the
431
+ provenance of an arbitrary host store. Use the `@jarenjs/db` DAG job
432
+ runner for persisted identity comparison before node-value loading. Values recorded for node ids the current document does not
432
433
  declare (or no longer declares `checkpoint`) are ignored.
433
434
 
434
435
  ### §7.8 Declared task versions
435
436
 
436
437
  A checkpointed node's value is REPLAYED on a later run instead of being
437
438
  recomputed. That is sound only while the handler that produced it is the
438
- same handler. So a `task` node that declares `checkpoint: true` must also
439
- declare `version`: a non-empty string naming the identity of the
440
- implementation it depends on.
439
+ same handler. Every `task` in a workflow containing any checkpoint must
440
+ declare `version`: a nonblank string naming its implementation identity.
441
+ This includes recomputed tasks: an upstream implementation can affect a
442
+ downstream checkpoint even when its own result is never saved.
441
443
 
442
444
  ```jsonc
443
445
  { "kind": "task", "run": "summarise", "version": "2026-09-05", "checkpoint": true }
@@ -463,9 +465,13 @@ declared identity it depends on, keyed by node id and SORTED, so two
463
465
  compiles of the same document produce the same map — byte for byte —
464
466
  whatever order the declarations were written in. A handler that is itself
465
467
  a compiled workflow may expose its own `taskVersions`; those compose under
466
- the node's path (`outer`, `outer/inner`), so a composed run has one
468
+ the node's path (`outer`, `outer/inner`), with each node-id segment escaped
469
+ as JSON Pointer (`~` becomes `~0`, `/` becomes `~1`), so a composed run has one
467
470
  identity rather than two. Every task id is retained as an own member,
468
- including names inherited by ordinary JavaScript objects.
471
+ including names inherited by ordinary JavaScript objects. Registry nested
472
+ maps must contain nonblank string versions and valid escaped paths.
473
+ A literal `outer/inner` node is `outer~1inner`, distinct from a nested
474
+ `inner` task under `outer`.
469
475
 
470
476
  ```js
471
477
  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.67.0",
4
+ "version": "0.72.0",
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.67.0",
53
- "@jarenjs/json": "^0.67.0"
52
+ "@jarenjs/core": "^0.72.0",
53
+ "@jarenjs/json": "^0.72.0"
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": "https://jarenjs.dev/schemas/jaren-query/0.1/draft-07"
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": "https://jarenjs.dev/schemas/jaren-jslt/0.1/draft-07"
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": "The declared identity of the handler implementation this node depends on (FLOW-FORMAT section 7.8). REQUIRED when checkpoint is true: a recorded value is replayed only while the handler that produced it is the same one, and that identity is declared, never derived from source. The registry must supply the same token.",
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": "https://jarenjs.dev/schemas/jaren-query/0.1/draft-07"
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": "https://jarenjs.dev/schemas/jaren-query/0.1/draft-07"
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": "https://jarenjs.dev/schemas/jaren-query/0.1"
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": "https://jarenjs.dev/schemas/jaren-jslt/0.1"
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": "The declared identity of the handler implementation this node depends on (FLOW-FORMAT section 7.8). REQUIRED when checkpoint is true: a recorded value is replayed only while the handler that produced it is the same one, and that identity is declared, never derived from source. The registry must supply the same token.",
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": "https://jarenjs.dev/schemas/jaren-query/0.1"
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": "https://jarenjs.dev/schemas/jaren-query/0.1"
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": "https://jarenjs.dev/schemas/jaren-query/0.1/draft-07"
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": "https://jarenjs.dev/schemas/jaren-query/0.1/draft-07"
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": ["string", "null"]
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
- { "type": "string" },
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": { "$ref": "#/$defs/effects" },
30
- "exit": { "$ref": "#/$defs/effects" },
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": ["id"]
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": ["string", "null"]
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
- { "type": "null" },
59
- { "$ref": "https://jarenjs.dev/schemas/jaren-query/0.1" }
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": { "$ref": "#/$defs/effects" }
84
+ "effects": {
85
+ "$ref": "#/$defs/effects"
86
+ }
67
87
  },
68
- "required": ["from", "to"]
88
+ "required": [
89
+ "from",
90
+ "to"
91
+ ]
69
92
  }
70
93
  }
71
94
  },
72
- "required": ["initial", "states", "transitions"],
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": [{ "$ref": "https://jarenjs.dev/schemas/jaren-query/0.1" }]
114
+ "allOf": [
115
+ {
116
+ "$ref": "#/$defs/queryDocument"
117
+ }
118
+ ]
88
119
  }
89
120
  },
90
- "required": ["run"]
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 (node.checkpoint && decl.version === undefined) {
220
+ if (durable && decl.version === undefined) {
216
221
  throw new FlowCompileError('JF0011',
217
- `task node '${id}' declares checkpoint, so it must also declare a "version" — `
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
  }
@@ -631,10 +636,11 @@ export function compileDag(doc, options) {
631
636
  const versions = {};
632
637
  for (const node of nodes.values()) {
633
638
  if (node.kind !== 'task' || node.version === null) continue;
634
- setObjectMember(versions, node.id, node.version);
639
+ const pathPrefix = encodeJSONPointerSegment(node.id);
640
+ setObjectMember(versions, pathPrefix, node.version);
635
641
  if (node.nestedVersions === null) continue;
636
642
  for (const [path, version] of Object.entries(node.nestedVersions)) {
637
- setObjectMember(versions, `${node.id}/${path}`, version);
643
+ setObjectMember(versions, `${pathPrefix}/${path}`, version);
638
644
  }
639
645
  }
640
646
  const taskVersions = Object.freeze(Object.fromEntries(