@jarenjs/linq 0.49.2 → 0.66.1

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.
Files changed (79) hide show
  1. package/ARCHITECTURE.md +227 -0
  2. package/README.md +650 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1221 -0
  5. package/docs/DB-CLIENT.md +882 -0
  6. package/docs/FLOW-PEN.md +1033 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +778 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1092 -0
  12. package/docs/QUERY-PEN.md +1724 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +251 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +255 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +377 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +48 -11
  24. package/src/contract/define.js +282 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +338 -0
  28. package/src/db/handle.js +89 -0
  29. package/src/db/include.js +351 -0
  30. package/src/db/index.js +24 -0
  31. package/src/db/ledger.js +195 -0
  32. package/src/db/live.js +43 -0
  33. package/src/db/membership.js +37 -0
  34. package/src/db/open.js +130 -0
  35. package/src/document.js +143 -13
  36. package/src/effect.js +65 -0
  37. package/src/errors.js +78 -6
  38. package/src/expression.js +463 -36
  39. package/src/federate.js +531 -0
  40. package/src/flow/capture.js +33 -0
  41. package/src/flow/dag.js +316 -0
  42. package/src/flow/fsm.js +323 -0
  43. package/src/flow/index.js +22 -0
  44. package/src/forms/index.js +43 -0
  45. package/src/forms/rules.js +170 -0
  46. package/src/forms/submit.js +177 -0
  47. package/src/index.js +5 -2
  48. package/src/jslt/body.js +226 -0
  49. package/src/jslt/index.js +18 -0
  50. package/src/jslt/rules.js +202 -0
  51. package/src/json-boundary.js +90 -0
  52. package/src/migration/define.js +318 -0
  53. package/src/migration/index.js +15 -0
  54. package/src/migration/steps.js +244 -0
  55. package/src/model/collection.js +273 -0
  56. package/src/model/define.js +125 -0
  57. package/src/model/entity.js +307 -0
  58. package/src/model/index.js +47 -0
  59. package/src/model/relation.js +85 -0
  60. package/src/provider.js +137 -20
  61. package/src/schema/brand.js +31 -0
  62. package/src/schema/builders.js +526 -0
  63. package/src/schema/check.js +29 -0
  64. package/src/schema/emit.js +394 -0
  65. package/src/schema/factories.js +239 -0
  66. package/src/schema/index.js +37 -0
  67. package/src/schema-of.js +24 -0
  68. package/src/sequence.js +233 -103
  69. package/src/sources.js +10 -3
  70. package/types/app.d.ts +293 -0
  71. package/types/contract.d.ts +468 -0
  72. package/types/db.d.ts +359 -0
  73. package/types/flow.d.ts +285 -0
  74. package/types/forms.d.ts +253 -0
  75. package/types/index.d.ts +296 -26
  76. package/types/jslt.d.ts +193 -0
  77. package/types/migration.d.ts +201 -0
  78. package/types/model.d.ts +526 -0
  79. package/types/schema.d.ts +494 -0
@@ -0,0 +1,1033 @@
1
+ # The Jaren flow pen
2
+
3
+ > `./flow` — `jaren-fsm` 0.1 machines and `jaren-dag` 0.1 dataflows,
4
+ > every query-valued member captured. **Read it when** you are declaring
5
+ > a state machine or a dependency graph of tasks
6
+
7
+ Version 0.1. The key words MUST, MUST NOT, SHOULD and MAY are to be
8
+ interpreted as described in RFC 2119. This document is a **guide** — read
9
+ it in order and you can write the format — whose one normative section is
10
+ [§2 The mapping table](#2-the-mapping-table); the rules every pen keeps, the shared refusal table, the
11
+ index of the other pens and every pen's mapping table collected in one
12
+ place are the normative reference,
13
+ [LINQ-FORMAT.md](LINQ-FORMAT.md).
14
+
15
+ ## 1. What it writes
16
+
17
+ You have a process to describe — a document that moves between states as
18
+ events arrive, or a pipeline that takes one value and hands it through
19
+ named steps — and you want it as data, so a diagram, a runner and a test
20
+ can all read the same thing. Writing that data by hand means typing state
21
+ ids into a transition table and JSONPath strings into guards, with
22
+ nothing checking that either exists. This pen makes both a function call:
23
+ the ids are literal types, and every query-valued member is a callback it
24
+ records.
25
+
26
+ **This subpath writes two different documents, compiled by two different
27
+ engines.** `defineFsm()` writes a `jaren-fsm` 0.1 machine — control
28
+ states and a transition table — and `defineDag()` writes a `jaren-dag`
29
+ 0.1 dataflow — named nodes wired by edges. They share a package, a
30
+ subpath and a capture, and nothing else: a reader who thinks the two
31
+ functions write the same document will misread everything after this
32
+ paragraph.
33
+
34
+ ```js
35
+ import { defineFsm, state, on, effect } from '@jarenjs/linq/flow';
36
+ import { defineDag, input, constant, query, jslt, task, output, edge, typedTasks } from '@jarenjs/linq/flow';
37
+ ```
38
+
39
+ | | the machine | the dataflow |
40
+ |---|---|---|
41
+ | document | `{ $fsm: '0.1', initial, states, transitions }` | `{ $dag: '0.1', nodes, edges }` |
42
+ | grammar | `jaren-fsm` (`packages/flow/schemas/jaren-fsm.schema.json`) | `jaren-dag` (`packages/flow/schemas/jaren-dag.schema.json`) |
43
+ | format | [FLOW-FORMAT](../../flow/docs/FLOW-FORMAT.md) §2 | FLOW-FORMAT §6 |
44
+ | engine | `compileFsm` — and `fsmToApp`, which projects a machine into app documents | `compileDag` |
45
+ | shape of the work | synchronous, pure: `(state, event, options)` → a result and effect DESCRIPTORS the host runs | asynchronous: one input in, one value out, tasks resolved from a registry |
46
+ | the pen's vocabulary | `defineFsm`, `state`, `on` (`.when`, `.to`, `.effects`), `effect` | `defineDag`, `input`, `output`, `constant`, `query`, `jslt`, `task` (`.checkpoint`), `edge`, `typedTasks` |
47
+
48
+ Every query-valued member of either document is a CALLBACK captured over
49
+ the scope the engine evaluates it in, never a path typed as a string: a
50
+ guard and an effect's `with` over FLOW-FORMAT §3's step scope, a `query`
51
+ node's document, a task's `with` and an edge's `select` over §6.1's input
52
+ scope. That is also why the one trap the format names itself is refused
53
+ here — a guard given as a plain STRING is `JL0102`, because §3 makes a
54
+ non-`$` literal vacuously TRUE so that a diagram's display annotation can
55
+ never change execution (§4.2).
56
+
57
+ State ids, event names and node ids are literal types, so a transition
58
+ into an undeclared state and an edge on an undeclared node are compile
59
+ errors; at runtime they are `JL0102` naming the id, before the compiler's
60
+ `JF0004`/`JF0006`/`JF0013`. A machine written with string states and no
61
+ guards is also a valid `jaren-workflow` document — the projection
62
+ contract FLOW-FORMAT §1 calls the format's subset — so the pen's machines
63
+ and the mermaid projection's meet where the format says they do (§6.4).
64
+
65
+ The pen imports nothing of `@jarenjs/flow`: the compilers stay the only
66
+ judge of what the documents mean, and §7's tree-shaking probe holds it.
67
+
68
+ **The running example.** §3 is one editorial workflow, told twice because
69
+ the subpath writes two documents. The machine half (§3.1–§3.3) is an
70
+ article moving from draft to published: the transition table, then the
71
+ same table with effects, then the guard that reads a score against a
72
+ threshold. The dataflow half (§3.4–§3.7) is the pipeline around it: draft
73
+ and critique, filter the submissions that are long enough, render the
74
+ accepted ones as a view, and answer a question about them through a task
75
+ registry. §5 reads the types back off the same two documents.
76
+
77
+ ## 2. The mapping table
78
+
79
+ Thirteen exported names and sixteen rows: the four builder methods
80
+ (`.when`, `.to`, `.effects`, `.checkpoint`) earn rows of their own, and
81
+ `input()` and `output()` share one because they differ in nothing but the
82
+ word they write. The completeness gate in `test/linq/pen-docs.test.js`
83
+ asserts that every exported callable name appears somewhere in this
84
+ section.
85
+
86
+ | Method | Emits | Type reading | Status |
87
+ |---|---|---|---|
88
+ | `defineFsm({ initial, states, transitions, context? })` | `{ $fsm: '0.1', initial, states, transitions }` | `Fsm<States, Events, Context>`; `StatesOf<>`, `EventsOf<>`, `ContextOf<>` read it | native; a member the pen does not know, a missing `initial` (pass `null`), a non-array `states`/`transitions`, an entry that is not a declaration, a transition that never named its target, a `context` that is not a builder `JL0101`; an undeclared state id `JL0102` |
89
+ | `state(id, { entry?, exit?, final? })` | `{ id, entry?, exit?, final? }`; a bare string in `states` stays §2's shorthand | `StateDeclaration<Id>` — `Id` is a literal | native; an empty id, another member, a non-boolean `final`, a non-array or non-`effect()` entry/exit `JL0101` |
90
+ | `on(from, event?, { payload? })` | one entry of `transitions`, `{ from, event?, guard?, to, effects? }` in §2's order; a wildcard writes no `event` | `Transition<From, To, Event, Payload>` — a wildcard names no event, so it adds nothing to `EventsOf<>` | native; an empty `from`, a non-string event, another option, a `payload` that is not a builder `JL0101`; an undeclared `from`/`to` `JL0102` |
91
+ | `.when(fn)` / `.when(document)` | the transition's `guard` (§3) | the scope is `Scope<unknown, Payload>`; annotate for `context` | native; a plain STRING `JL0102` (§3's vacuous-guard rule); a non-JSON document `JL0101`; any external `JL0104` |
92
+ | `.to(state)` | the transition's `to` | `Transition<From, To, …>` — `To` is a literal | native; an empty id `JL0101`; an undeclared id `JL0102` at `defineFsm()` |
93
+ | `.effects([...])`, `state(…, { entry, exit })` | the effects lists §4 fires in exit → transition → entry order | `EffectDeclaration[]` | native; a non-array, or an entry that is not `effect()`, `JL0101` |
94
+ | `effect(run, with?)` | `{ run, with? }` | `EffectDeclaration<Run>`; the scope is the honest top until annotated | native; an empty `run`, a non-JSON `with` `JL0101`; any external `JL0104` |
95
+ | `defineDag({ nodes, edges })` | `{ $dag: '0.1', nodes, edges }` | `Dag<Ids, Tasks>`; `NodesOf<>`, `TasksOf<>` read it | native; a member the pen does not know, no node, a `nodes` map whose prototype a `__proto__:` literal replaced, a value that is not a node or edge declaration `JL0101`; an edge on an undeclared id `JL0102` |
96
+ | `input()` / `output()` | `{ kind: 'input' }` / `{ kind: 'output' }` | `NodeDeclaration<'input'>` / `<'output'>` | native |
97
+ | `constant(value)` | `{ kind: 'const', value }` | `NodeDeclaration<'const'>` | native; `undefined`, or a value that is not JSON, `JL0101` |
98
+ | `query(fn \| document)` | `{ kind: 'query', query }` | `NodeDeclaration<'query'>` | native; nothing passed, or a document that is not JSON, `JL0101`; any external `JL0104` |
99
+ | `jslt(stylesheet)` | `{ kind: 'jslt', stylesheet }` — the JSLT pen's document ([JSLT-PEN.md](JSLT-PEN.md)), or one by hand | `NodeDeclaration<'jslt'>` | native; nothing passed, or a value that is not JSON, `JL0101` |
100
+ | `task(run, with?)` | `{ kind: 'task', run, with? }` | `NodeDeclaration<'task', Run>` — `Run` is a literal | native; an empty `run` `JL0101`; any external in `with` `JL0104` |
101
+ | `.checkpoint()` | `checkpoint: true`, written last (§7.6) | a new declaration; the one it came from is unchanged | native |
102
+ | `edge(from, to, { port?, select? })` | `{ from, to, port?, select? }` | `EdgeDeclaration<From, To>` | native; an empty end, an empty `port`, another member `JL0101`; any external in `select` `JL0104` |
103
+ | `typedTasks(graph, tasks)` | — (identity) | the registry `compileDag` resolves must carry one handler per declared task name | native; a missing or misspelled name does not compile |
104
+
105
+ Three rules the table implies, spelled out:
106
+
107
+ - **The node kinds are a closed vocabulary and the pen mirrors it
108
+ exactly.** FLOW-FORMAT §6 fixes six kinds, and this pen has one
109
+ function per kind — so a kind the format does not have cannot be
110
+ spelled, and a kind it adds later needs a pen function before it can
111
+ be. `defineDag` refuses anything in `nodes` that is not one of the six
112
+ (`JL0101` naming all six), which is why a hand-written
113
+ `{ kind: 'input' }` is refused where the equivalent `input()` is taken.
114
+ - **`payload` and `context` are TYPES; nothing is emitted for them.** The
115
+ format carries no schema for either — a machine's data lives in the
116
+ host (§3) — so `on(from, event, { payload })` and
117
+ `defineFsm({ context })` type the guards and the host's `step()` call
118
+ and write no member. The refusal for a non-builder says so in as many
119
+ words.
120
+ - **What the pen does NOT judge is the compiler's.** Duplicate state ids
121
+ (`JF0003`), a guard's or stylesheet's own operators (`JF0007`,
122
+ `JF0014`), the dag wiring rules (`JF0015`), acyclicity (`JF0016`), the
123
+ exactly-one-output rule (`JF0017`) and task-registry resolution
124
+ (`JF0018`). The pen EMITS those documents and the compilers refuse
125
+ them; `test/linq/flow-pen.test.js` builds each one through the pen and
126
+ asserts the compiler's code, so the non-judgement is itself gated.
127
+
128
+ ## 3. Worked examples
129
+
130
+ Every `js` fence below exports exactly one document, and the `json` fence
131
+ that follows it is what the pen emits — executed by
132
+ `test/linq/pen-docs.test.js`. FLOW-FORMAT §2's machine and §6's dataflow
133
+ are additionally rebuilt through the pen and held BYTE-equal to the
134
+ format doc's own fences by `test/linq/flow-pen.test.js`, and every
135
+ document below validates under its published grammar.
136
+
137
+ ### 3.1 A machine with a guard
138
+
139
+ Three states, three transitions, one guard, and a wildcard listed last —
140
+ document order is the whole priority scheme (§4), so a fallback goes at
141
+ the bottom and nothing else is needed to express precedence.
142
+
143
+ ```js
144
+ import * as s from '@jarenjs/linq/schema';
145
+ import { defineFsm, on, state } from '@jarenjs/linq/flow';
146
+
147
+ export const review = defineFsm({
148
+ initial: 'draft',
149
+ states: ['draft', 'review', state('published', { final: true })],
150
+ transitions: [
151
+ on('draft', 'submit').to('review'),
152
+ on('review', 'approve', { payload: s.object({ fresh: s.boolean(), by: s.string() }) })
153
+ .when((sc) => sc.payload.fresh)
154
+ .to('published'),
155
+ on('review').to('draft'),
156
+ ],
157
+ });
158
+ ```
159
+
160
+ ```json
161
+ {
162
+ "$fsm": "0.1",
163
+ "initial": "draft",
164
+ "states": ["draft", "review", { "id": "published", "final": true }],
165
+ "transitions": [
166
+ { "from": "draft", "event": "submit", "to": "review" },
167
+ { "from": "review", "event": "approve", "guard": "$.payload.fresh", "to": "published" },
168
+ { "from": "review", "to": "draft" }
169
+ ]
170
+ }
171
+ ```
172
+
173
+ Three things to read off it. A bare string in `states` stays a bare
174
+ string — the format's own shorthand for `{ id }`, and the shape the
175
+ `jaren-workflow` projection carries — while `state(id, options)` writes
176
+ the object form. The wildcard writes NO `event` member, which is what
177
+ makes it match anything. And the `payload` builder emitted nothing: it
178
+ typed `sc.payload` inside the guard and left the document alone.
179
+
180
+ The guard `"$.payload.fresh"` is a path, and a path is what a captured
181
+ member read records. Send `approve` with `{ fresh: false }` and this
182
+ machine does not stay put — the guard fails, the wildcard below it
183
+ matches, and the step lands on `draft`. That is §4's selection rule
184
+ working exactly as written, and it is the reason a fallback's POSITION is
185
+ part of the design.
186
+
187
+ ### 3.2 The same machine with effects, and an effect's `with`
188
+
189
+ An effect is a `{ run, with? }` descriptor. The engine never invokes it:
190
+ a fired transition RETURNS descriptors as data and the host's registry
191
+ runs them, which is the boundary that keeps the machine pure.
192
+
193
+ ```js
194
+ import * as s from '@jarenjs/linq/schema';
195
+ import { defineFsm, effect, on, state } from '@jarenjs/linq/flow';
196
+
197
+ export const reviewed = defineFsm({
198
+ initial: 'draft',
199
+ states: [
200
+ 'draft',
201
+ 'review',
202
+ state('published', { entry: [effect('announce', (sc) => ({ by: sc.payload.by, from: sc.state }))], final: true }),
203
+ ],
204
+ transitions: [
205
+ on('draft', 'submit').to('review'),
206
+ on('review', 'approve', { payload: s.object({ fresh: s.boolean(), by: s.string() }) })
207
+ .when((sc) => sc.payload.fresh)
208
+ .to('published'),
209
+ on('review').to('draft').effects([effect('toast', () => ({ text: 'sent back' }))]),
210
+ ],
211
+ });
212
+ ```
213
+
214
+ ```json
215
+ {
216
+ "$fsm": "0.1",
217
+ "initial": "draft",
218
+ "states": [
219
+ "draft",
220
+ "review",
221
+ { "id": "published",
222
+ "entry": [{ "run": "announce", "with": { "by": "$.payload.by", "from": "$.state" } }],
223
+ "final": true }
224
+ ],
225
+ "transitions": [
226
+ { "from": "draft", "event": "submit", "to": "review" },
227
+ { "from": "review", "event": "approve", "guard": "$.payload.fresh", "to": "published" },
228
+ { "from": "review", "to": "draft",
229
+ "effects": [{ "run": "toast", "with": { "text": "sent back" } }] }
230
+ ]
231
+ }
232
+ ```
233
+
234
+ The two `with` members are the same capture in two spellings. `announce`
235
+ reads the step scope, so its members record paths (`$.payload.by`,
236
+ `$.state`); `toast` returns a literal, so its `with` is an object
237
+ CONSTRUCTOR — `{ "text": "sent back" }`, not `{ "$const": … }` — for the
238
+ reason [JSLT-PEN.md](JSLT-PEN.md) §1.1 point 4 gives, and because it is
239
+ the spelling FLOW-FORMAT §2's own example carries.
240
+
241
+ Stepping `approve` with `{ fresh: true, by: 'ada' }` from `review`
242
+ answers
243
+ `{ changed: true, state: 'published', effects: [{ run: 'announce', with: { by: 'ada', from: 'review' } }], final: true, errors: [] }`.
244
+ The `with` resolved against the scope; the handler named `announce` is
245
+ the host's to provide. The engine has NO code for an unregistered effect
246
+ handler and could not have one — FLOW-FORMAT §1.1 makes effect execution
247
+ a non-goal, so `compileFsm` never looks a name up and never calls
248
+ anything. A `run` nobody registered is a descriptor the host quietly
249
+ drops, which is worth knowing because it is one of the few mistakes in
250
+ this pen's surface that nothing on either side reports.
251
+
252
+ ### 3.3 A guard over the whole step scope
253
+
254
+ The case that motivates the capture. A guard comparing two members of the
255
+ scope, and reading the event name besides, is an operator document — not
256
+ a path — and there is no string a writer could type that means it.
257
+
258
+ ```js
259
+ import * as s from '@jarenjs/linq/schema';
260
+ import { defineFsm, on } from '@jarenjs/linq/flow';
261
+
262
+ export const scored = defineFsm({
263
+ initial: 'review',
264
+ states: ['review', 'published'],
265
+ transitions: [
266
+ on('review', 'approve', { payload: s.object({ score: s.number() }) })
267
+ .when((sc) => sc.context.threshold.le(sc.payload.score).and(sc.event.eq('approve')))
268
+ .to('published'),
269
+ ],
270
+ context: s.object({ threshold: s.number() }),
271
+ });
272
+ ```
273
+
274
+ ```json
275
+ {
276
+ "$fsm": "0.1",
277
+ "initial": "review",
278
+ "states": ["review", "published"],
279
+ "transitions": [
280
+ { "from": "review", "event": "approve",
281
+ "guard": { "$and": [ { "$le": ["$.context.threshold", "$.payload.score"] },
282
+ { "$eq": ["$.event", "approve"] } ] },
283
+ "to": "published" }
284
+ ]
285
+ }
286
+ ```
287
+
288
+ `compileFsm(scored).step('review', 'approve', { payload: { score: 8 }, context: { threshold: 5 } })`
289
+ fires; the same call with `{ score: 3 }` reports
290
+ `{ changed: false, state: 'review', … }` — an unhandled step, not an
291
+ error (§4).
292
+
293
+ This is what §4.2's refusal is protecting. A writer who wants this
294
+ condition and reaches for a string gets a guard that is EBV-true for
295
+ every step, forever, silently. The pen refuses the string; the callback
296
+ is the route.
297
+
298
+ **A TypeScript caller writes one more thing here.** `sc.payload` is typed
299
+ by the event's own declaration on the same call, but `sc.context` is not:
300
+ `on()` is evaluated before `defineFsm()` ever sees the `context` builder,
301
+ so the scope's context is the honest top and `sc.context.threshold` does
302
+ not compile. The annotation is where the type comes from —
303
+
304
+ ```ts
305
+ on('review', 'approve', { payload: Score })
306
+ .when((sc: Scope<{ threshold: number }, { score: number }>) =>
307
+ sc.context.threshold.le(sc.payload.score))
308
+ .to('published')
309
+ ```
310
+
311
+ — and §5.1 says why it cannot be inferred. The emitted document is
312
+ identical either way; the annotation buys the compiler, not the
313
+ document.
314
+
315
+ ### 3.4 A dataflow: input, two tasks, output
316
+
317
+ Two `task` nodes chained, the second checkpointed, and a `select` on the
318
+ delivering edge. `run` is a NAME the pen writes and never resolves — the
319
+ registry a host hands `compileDag` owns the handler.
320
+
321
+ ```js
322
+ import { defineDag, edge, input, output, task } from '@jarenjs/linq/flow';
323
+
324
+ export const writing = defineDag({
325
+ nodes: {
326
+ brief: input(),
327
+ draft: task('llm', (v) => ({ prompt: v.topic })),
328
+ review: task('critic', (v) => ({ text: v }), { version: '1' }).checkpoint(),
329
+ out: output(),
330
+ },
331
+ edges: [
332
+ edge('brief', 'draft'),
333
+ edge('draft', 'review'),
334
+ edge('review', 'out', { select: (v) => v.get('verdict') }),
335
+ ],
336
+ });
337
+ ```
338
+
339
+ ```json
340
+ {
341
+ "$dag": "0.1",
342
+ "nodes": {
343
+ "brief": { "kind": "input" },
344
+ "draft": { "kind": "task", "run": "llm", "with": { "prompt": "$.topic" } },
345
+ "review": { "kind": "task", "run": "critic", "version": "1", "with": { "text": "$" }, "checkpoint": true },
346
+ "out": { "kind": "output" }
347
+ },
348
+ "edges": [
349
+ { "from": "brief", "to": "draft" },
350
+ { "from": "draft", "to": "review" },
351
+ { "from": "review", "to": "out", "select": "$['verdict']" }
352
+ ]
353
+ }
354
+ ```
355
+
356
+ `review` carries a `version` because it is checkpointed: a recorded value
357
+ is replayed on a later run only while the handler that produced it is the
358
+ same one, so that identity is DECLARED — the registry must hand
359
+ `compileDag` the same token as `{ run, version }`, and a disagreement is
360
+ `JF0019` before any node runs. `.checkpoint()` refuses a task that has
361
+ not declared one.
362
+
363
+ `review`'s `with` is `{ "text": "$" }`: the callback returned the scope
364
+ value itself, and the scope value of a node with one unported inbound
365
+ edge IS the delivered value, verbatim (§6.1). `checkpoint: true` is
366
+ written last whatever order `.checkpoint()` was called in, and it is
367
+ opt-in per node — with a checkpoint store configured, only `review`'s
368
+ value is recorded, and a resumed run seeds it instead of calling the
369
+ handler again (§7.6).
370
+
371
+ `v.get('verdict')` in the `select` records `"$['verdict']"` rather than
372
+ `"$.verdict"`. Both are the same path; `get()` always writes the bracket
373
+ form because it takes an arbitrary string. Reach for it when a member
374
+ name is not an identifier, or when it collides with a chain method — a
375
+ member called `count` read as `v.count` answers the method, not the
376
+ member.
377
+
378
+ ### 3.5 A `query` node, and the rule that changes its shape
379
+
380
+ A `query` node runs a Jaren JSON Query over its input scope. This example
381
+ takes a query DOCUMENT rather than a callback, because FLOW-FORMAT §6's
382
+ own example does and because a FLWOR phrase naming its binding `r` is not
383
+ something the chain emits (it packs and names its binding `it`).
384
+
385
+ ```js
386
+ import { defineDag, edge, input, output, query } from '@jarenjs/linq/flow';
387
+
388
+ export const longEnough = defineDag({
389
+ nodes: {
390
+ subs: input(),
391
+ long: query({ $for: { r: '$[*]' }, $where: { $ge: ['$r.words', 500] }, $return: '$r' }),
392
+ out: output(),
393
+ },
394
+ edges: [edge('subs', 'long'), edge('long', 'out')],
395
+ });
396
+ ```
397
+
398
+ ```json
399
+ {
400
+ "$dag": "0.1",
401
+ "nodes": {
402
+ "subs": { "kind": "input" },
403
+ "long": { "kind": "query",
404
+ "query": { "$for": { "r": "$[*]" }, "$where": { "$ge": ["$r.words", 500] }, "$return": "$r" } },
405
+ "out": { "kind": "output" }
406
+ },
407
+ "edges": [
408
+ { "from": "subs", "to": "long" },
409
+ { "from": "long", "to": "out" }
410
+ ]
411
+ }
412
+ ```
413
+
414
+ **A node's result carries the query engine's singleton rule, and the
415
+ result shape changes with the data.** `compileDag` calls a compiled query
416
+ through its default entry point, and QUERY-FORMAT rule 5 identifies a
417
+ one-item sequence with the item. So this graph, run three times:
418
+
419
+ ```json
420
+ [
421
+ { "input": [{"name":"ada","age":36},{"name":"kit","age":9},{"name":"lin","age":20}],
422
+ "result": [{"name":"ada","age":36},{"name":"lin","age":20}] },
423
+ { "input": [{"name":"ada","age":36},{"name":"kit","age":9}],
424
+ "result": {"name":"ada","age":36} },
425
+ { "input": [{"name":"kit","age":9}],
426
+ "result": null }
427
+ ]
428
+ ```
429
+
430
+ Two survivors give an array, ONE survivor gives the row itself, none
431
+ gives `null`. FLOW-FORMAT §6 states only the empty case, and the
432
+ difference is invisible until a downstream `$[*]` iterates an object's
433
+ values instead of an array's items. The pen cannot fix it — this is the
434
+ engine's reading of a document the pen wrote faithfully — so it is
435
+ recorded in [docs/ROADMAP.md](../../../docs/ROADMAP.md) under
436
+ `@jarenjs/flow`, where the resolution is a format decision (a §6 sentence
437
+ and a worked example, or a node-level "always a sequence" option). Until
438
+ then: a `query` node feeding anything that expects a list wants a `$for`
439
+ whose `$return` is explicitly an array constructor, or a downstream node
440
+ that tolerates both.
441
+
442
+ ### 3.6 A `jslt` node
443
+
444
+ A `jslt` node's stylesheet is the JSLT pen's document
445
+ ([JSLT-PEN.md](JSLT-PEN.md)), embedded whole. This is the composition the
446
+ two pens exist for: a dataflow that ends in a rendered view, written
447
+ entirely by code.
448
+
449
+ ```js
450
+ import { defineDag, edge, input, jslt, output } from '@jarenjs/linq/flow';
451
+ import { apply, rule, stylesheet } from '@jarenjs/linq/jslt';
452
+
453
+ export const listing = defineDag({
454
+ nodes: {
455
+ articles: input(),
456
+ list: jslt(stylesheet([
457
+ rule('$', (v) => ['ul', {}, [apply(v.all())]]),
458
+ rule('$[*]', (v) => ['li', {}, v.title]),
459
+ ])),
460
+ out: output(),
461
+ },
462
+ edges: [edge('articles', 'list'), edge('list', 'out')],
463
+ });
464
+ ```
465
+
466
+ ```json
467
+ {
468
+ "$dag": "0.1",
469
+ "nodes": {
470
+ "articles": { "kind": "input" },
471
+ "list": { "kind": "jslt",
472
+ "stylesheet": { "$jslt": "0.1", "rules": [
473
+ { "match": "$", "body": ["ul", {}, [{ "$apply": "$[*]" }]] },
474
+ { "match": "$[*]", "body": ["li", {}, "$.title"] } ] } },
475
+ "out": { "kind": "output" }
476
+ },
477
+ "edges": [
478
+ { "from": "articles", "to": "list" },
479
+ { "from": "list", "to": "out" }
480
+ ]
481
+ }
482
+ ```
483
+
484
+ Over `[{ title: 'ada' }, { title: 'lin' }]` this runs to
485
+ `['ul', {}, [['li', {}, 'ada'], ['li', {}, 'lin']]]` — a `jaren-vnode`
486
+ tree, which is what [JSLT-PEN.md](JSLT-PEN.md) §3.7 is about. The
487
+ envelope form and the bare rules array are both accepted here; the
488
+ envelope is what `stylesheet()` writes, and a rules array is what a hand
489
+ written node usually carries. Nothing about the stylesheet is judged by
490
+ this pen — `jslt()` checks only that the value is JSON, and `JF0014` is
491
+ the compiler's if the stylesheet itself is wrong.
492
+
493
+ ### 3.7 Ports, a constant, and `typedTasks` over the registry
494
+
495
+ A node with more than one inbound edge needs ports: when any inbound edge
496
+ names one, every inbound edge MUST, and the node's `$` becomes the object
497
+ of port-named values in EDGE document order (§6.1). `typedTasks(graph,
498
+ registry)` binds the handler table to the graph — identity at runtime,
499
+ a compile-time check that the table has one handler per declared task
500
+ name.
501
+
502
+ ```js
503
+ import { constant, defineDag, edge, input, output, task, typedTasks } from '@jarenjs/linq/flow';
504
+
505
+ const answering = defineDag({
506
+ nodes: {
507
+ question: input(),
508
+ facts: constant({ product: 'jarenjs', version: '0.52.7' }),
509
+ answer: task('llm', (v) => ({ prompt: v.get('q'), facts: v.get('f') })),
510
+ out: output(),
511
+ },
512
+ edges: [
513
+ edge('question', 'answer', { port: 'q' }),
514
+ edge('facts', 'answer', { port: 'f' }),
515
+ edge('answer', 'out', { select: (v) => v.get('text') }),
516
+ ],
517
+ });
518
+
519
+ // the registry the host will hand compileDag, checked against the graph's task names
520
+ void typedTasks(answering, {
521
+ llm: async ({ with: w }) => ({ text: `${w.prompt} — ${w.facts.product} ${w.facts.version}` }),
522
+ });
523
+
524
+ export const graph = answering;
525
+ ```
526
+
527
+ ```json
528
+ {
529
+ "$dag": "0.1",
530
+ "nodes": {
531
+ "question": { "kind": "input" },
532
+ "facts": { "kind": "const", "value": { "product": "jarenjs", "version": "0.52.7" } },
533
+ "answer": { "kind": "task", "run": "llm",
534
+ "with": { "prompt": "$['q']", "facts": "$['f']" } },
535
+ "out": { "kind": "output" }
536
+ },
537
+ "edges": [
538
+ { "from": "question", "to": "answer", "port": "q" },
539
+ { "from": "facts", "to": "answer", "port": "f" },
540
+ { "from": "answer", "to": "out", "select": "$['text']" }
541
+ ]
542
+ }
543
+ ```
544
+
545
+ `typedTasks` emits nothing — it is the identity function, and the fence
546
+ exports the graph. Rename `llm` to `model` in the registry and the fence
547
+ stops COMPILING, which is the whole point: the check is
548
+ `{ [K in TasksOf<D>]: TaskHandler }`, and the runtime check stays
549
+ `JF0018` for a host that assembled its registry dynamically. §5.3 states
550
+ the other direction — annotating the node map with `NodesFor<Registry>`,
551
+ which catches a misspelled `task('nope')` at the node rather than at the
552
+ binding.
553
+
554
+ ## 4. Refusals
555
+
556
+ The flow pen raises these three `LinqBuildError` codes and no others —
557
+ `test/linq/pen-docs.test.js` holds this list equal, in both directions,
558
+ to the codes `packages/linq/src/flow/` throws. The full condition each
559
+ code states across every pen is the binder's,
560
+ [LINQ-FORMAT.md](LINQ-FORMAT.md) §1.3.
561
+
562
+ | Code | What this pen raises it for |
563
+ |---|---|
564
+ | `JL0101` | a value this pen cannot spell, a member it does not know, or a name → value map it cannot read |
565
+ | `JL0102` | a guard given as a plain STRING, or a state or node id no declaration carries |
566
+ | `JL0104` | any external at all: both engines evaluate with one `$` and nothing else |
567
+
568
+ `packages/linq/src/flow/` carries **33 throw sites** — 30 `JL0101` and 3
569
+ `JL0102`. Two families of condition reach a caller through this pen
570
+ without being thrown in its directory: the effect descriptor and its list
571
+ (`packages/linq/src/effect.js`, shared with the app pen) and the shared
572
+ capture's external check (`packages/linq/src/capture-root.js`), which is
573
+ where every one of this pen's `JL0104`s comes from — the code appears in
574
+ `src/flow/` only in the comments that explain it, and the refusal gate
575
+ counts those, correctly, as the pen owing the reader a row.
576
+
577
+ Every message below is the one the pen raised when the spelling beside it
578
+ was run, with the code prefix (`JL0101: `) removed. `docPath`, where the
579
+ refusal carries one, is the JSON pointer of the node being assembled and
580
+ is appended to the message text as well (`… at /transitions/0/to`).
581
+
582
+ ### 4.1 `JL0101` — the value, the member and the map
583
+
584
+ **The machine.**
585
+
586
+ | The spelling that trips it | The message | The spelling that works |
587
+ |---|---|---|
588
+ | `defineFsm('$')` | `defineFsm() takes { initial, states, transitions, context? }, got a string` | the four members |
589
+ | `defineFsm({ states, transitions })` | `defineFsm() needs an initial state — the format requires the member; pass null for a machine that chooses none (a session then starts with an explicit state)` — `docPath` `/initial` | `initial: 'draft'`, or `null` |
590
+ | `defineFsm({ …, nope: 1 })` | `defineFsm() does not take 'nope' — it takes initial, states, transitions, context` — `docPath` `/nope` | the four members |
591
+ | `defineFsm({ …, context: {} })` | `defineFsm() context is a schema-pen builder that types the host data a guard reads (FLOW-FORMAT §3) — the format carries no context schema, so nothing is emitted for it; got a Object instance` — `docPath` `/context` | `s.object({ … })` |
592
+ | `defineFsm({ …, states: '$' })` | `defineFsm() states is an array of ids and state() declarations, got a string` — `docPath` `/states` | an array |
593
+ | `defineFsm({ …, states: [{ id: 'a' }] })` | `defineFsm() states[0] is an id or state(id, options?), got a Object instance` — `docPath` `/states/0` | `'a'` or `state('a')` |
594
+ | `defineFsm({ initial: 42, … })` | `defineFsm() initial takes a state id — a non-empty string, got 42` | a declared id |
595
+ | `defineFsm({ …, transitions: '$' })` | `defineFsm() transitions is an array of on(…) declarations, got a string` — `docPath` `/transitions` | an array |
596
+ | `defineFsm({ …, transitions: [{ from: 'a', to: 'a' }] })` | `defineFsm() transitions[0] is on(from, event?).to(state), got a Object instance` — `docPath` `/transitions/0` | `on('a', 'go').to('a')` |
597
+ | `defineFsm({ …, transitions: [on('a', 'go')] })` | `defineFsm() transitions[0] never named its target — on('a', 'go') needs .to(state)` — `docPath` `/transitions/0` | finish it with `.to(state)` |
598
+
599
+ **A state, a transition, an effect.**
600
+
601
+ | The spelling that trips it | The message | The spelling that works |
602
+ |---|---|---|
603
+ | `state('')` | `state() takes a state id — a non-empty string, got a string` | a non-empty id |
604
+ | `state('a', 'x')` | `state() options are { entry?, exit?, final? }, got a string` | the options object |
605
+ | `state('a', { onEntry: [] })` | `state() does not take 'onEntry' — it takes entry, exit, final` — `docPath` `/onEntry` | `entry` |
606
+ | `state('a', { final: 'yes' })` | `state() final is a boolean, got a string` — `docPath` `/final` | `true` |
607
+ | `state('a', { entry: 'x' })` | `state() entry is an array of effect() descriptors, got a string` | an array |
608
+ | `state('a', { entry: [{ run: 'x' }] })` | `state() entry[0] is effect(run, with?), got a Object instance` | `effect('x')` |
609
+ | `on(1)` | `on() takes a state id — a non-empty string, got 1` | a declared id |
610
+ | `on('a', 7)` | `on() takes an event name as a non-empty string, or null for the wildcard that matches any event (FLOW-FORMAT §2), got 7` — `docPath` `/event` | `'go'`, or `null` |
611
+ | `on('a', 'go', 'x')` | `on() options are { payload? }, got a string` | `{ payload: … }` |
612
+ | `on('a', 'go', { data: s.string() })` | `on() does not take 'data' — it takes payload` — `docPath` `/data` | `payload` |
613
+ | `on('a', 'go', { payload: { type: 'object' } })` | `on() payload is a schema-pen builder that types the event's payload — the format carries no payload schema, so nothing is emitted for it; got a Object instance` — `docPath` `/payload` | `s.object({ … })` |
614
+ | `on('a', 'go').to(42)` | `to() takes a state id — a non-empty string, got 42` | a declared id |
615
+ | `on('a', 'go').effects('x')` | `effects() is an array of effect() descriptors, got a string` | an array |
616
+ | `on('a', 'go').when(new Date(0))` | `when() received a Date instance, which is not JSON — a document carries null, booleans, finite numbers (never -0), strings, arrays and plain objects, and nothing else` | a callback, or a query document |
617
+ | `effect('')` | `effect() takes the handler name as a non-empty string, got a string` — `docPath` `/run` | a registered name |
618
+ | `effect('x', { with: Symbol('s') })` | `effect() with received a Object instance, which is not JSON — a document carries null, booleans, finite numbers (never -0), strings, arrays and plain objects, and nothing else` | a callback, or a query document |
619
+
620
+ **The dataflow.**
621
+
622
+ | The spelling that trips it | The message | The spelling that works |
623
+ |---|---|---|
624
+ | `defineDag([])` | `defineDag() takes { nodes, edges }, got a Array instance` | the two members |
625
+ | `defineDag({ …, tasks: {} })` | `defineDag() does not take 'tasks' — it takes nodes, edges` — `docPath` `/tasks` | the registry goes to `compileDag` |
626
+ | `defineDag({ nodes: '$', edges: [] })` | `defineDag() nodes is a plain object of id → node declaration, got a string` — `docPath` `/nodes` | an object |
627
+ | `defineDag({ nodes: { __proto__: input() }, edges: [] })` | `defineDag() nodes received a map whose prototype was replaced: a '__proto__:' key in an object literal sets the prototype instead of adding a member, so that member is not there to emit — spell it { ['__proto__']: … }, which is an own key` — `docPath` `/nodes` | `{ ['__proto__']: input() }` |
628
+ | `defineDag({ nodes: {}, edges: [] })` | `defineDag() needs at least one node` — `docPath` `/nodes` | at least one |
629
+ | `defineDag({ nodes: { a: { kind: 'input' } }, … })` | `defineDag() node 'a' is input(), constant(), query(), jslt(), task() or output(), got a Object instance` — `docPath` `/nodes/a` | `input()` |
630
+ | `defineDag({ …, edges: {} })` | `defineDag() edges is an array of edge(from, to) declarations, got a Object instance` — `docPath` `/edges` | an array |
631
+ | `defineDag({ …, edges: [{ from: 'a', to: 'a' }] })` | `defineDag() edges[0] is edge(from, to, options?), got a Object instance` — `docPath` `/edges/0` | `edge('a', 'a')` |
632
+ | `constant(undefined)` | `constant() takes the value the node yields — every JSON value, null included; undefined is not one` — `docPath` `/value` | any JSON value |
633
+ | `constant(new Date(0))` | `constant() received a Date instance, which is not JSON — …` | its ISO string, or its epoch number |
634
+ | `query(undefined)` | `query() takes a callback (v) => … captured over the node input, or a query document` — `docPath` `/query` | one of the two |
635
+ | `jslt(undefined)` | `jslt() takes a stylesheet document — the JSLT pen's stylesheet(…) or rule array, or one written by hand` — `docPath` `/stylesheet` | `stylesheet([…])` |
636
+ | `task('')` | `task() takes the handler name as a non-empty string, got a string` — `docPath` `/run` | a registry name |
637
+ | `edge('', 'b')`, `edge('a', '')` | `edge() takes the producing node id as a non-empty string, got a string` — `docPath` `/from` | a declared id |
638
+ | `edge('a', 'b', 'x')` | `edge() options are { port?, select? }, got a string` | the options object |
639
+ | `edge('a', 'b', { port: '' })` | `edge() port is a non-empty string, got a string` — `docPath` `/port` | a port name |
640
+ | `edge('a', 'b', { ports: 'x' })` | `edge() does not take 'ports' — it takes port, select` — `docPath` `/ports` | `port` |
641
+
642
+ A refusal a captured member can raise that is the CHAIN's rather than
643
+ this pen's: `effect('x', () => new Date(0))` is `JL0005` ("a captured
644
+ expression cannot embed a Date instance — it carries no own enumerable
645
+ members, so it would embed as `{}`"). It is documented in
646
+ [QUERY-PEN.md](QUERY-PEN.md) §9.
647
+
648
+ ### 4.2 `JL0102` — a guard given as a plain string
649
+
650
+ This deserves its own prose because a reader will hit it, and because the
651
+ message alone does not explain why a perfectly reasonable-looking string
652
+ is refused.
653
+
654
+ FLOW-FORMAT §3 asserts a guard by **effective boolean value**. A string
655
+ that does not start with `$` is a literal string, and a non-empty literal
656
+ is EBV-true. So a guard spelled `'count > 3'` is not a condition the
657
+ engine fails to parse — it is a condition that passes, on every step,
658
+ forever, silently. That is deliberate in the FORMAT: the mermaid state
659
+ diagram projection carries opaque display guards lifted from diagram
660
+ labels (`[count > 3]`), and a picture's annotation MUST NOT change
661
+ execution.
662
+
663
+ The pen refuses to emit one, because a pen writing that document is not
664
+ projecting a diagram — it is writing code that meant something.
665
+
666
+ | The spelling that trips it | The message | The spelling that works |
667
+ |---|---|---|
668
+ | `on('a', 'go').when('count > 3')` | `a guard given as a plain string is asserted by effective boolean value, and a non-empty literal is therefore VACUOUSLY TRUE (FLOW-FORMAT §3: a projected display guard must not change execution) — got "count > 3"; pass a body instead: .when((s) => s.payload.fresh)` — `docPath` `/guard` | `.when((sc) => sc.context.count.gt(3))` |
669
+ | `on('a', 'go').when('$.payload.fresh')` | the same message, with `"$.payload.fresh"` | `.when((sc) => sc.payload.fresh)` |
670
+
671
+ The second row is the one that catches people. `'$.payload.fresh'` IS a
672
+ valid guard document — the engine would read it as a path and evaluate it
673
+ correctly. The pen refuses it anyway, and the reason is that it cannot
674
+ tell that string apart from the first row's at build time without
675
+ becoming a JSONPath parser, which LINQ-FORMAT §1.1 rule 1 forbids: a pen
676
+ mirrors the engine's rules, it does not re-implement its compiler.
677
+ Refusing every plain string is the rule that has no wrong answers, and
678
+ the callback is both shorter and typed:
679
+
680
+ ```js
681
+ // refused, though the document it would write is correct
682
+ on('review', 'approve').when('$.payload.fresh').to('published')
683
+
684
+ // what the writer meant, captured — and sc.payload is typed by the event's declaration
685
+ on('review', 'approve', { payload: Approval }).when((sc) => sc.payload.fresh).to('published')
686
+ ```
687
+
688
+ A guard written as a query DOCUMENT rides verbatim and is not refused —
689
+ `.when({ $gt: ['$.payload.n', 3] })` emits exactly that. The refusal is
690
+ on the string form alone, because the string form is the ambiguous one.
691
+
692
+ ### 4.3 `JL0102` — an id no declaration carries
693
+
694
+ The other `JL0102` condition, and the pen's clearest case of catching the
695
+ engine's own rule one step earlier. Both machines and dataflows have it.
696
+
697
+ | The spelling that trips it | The message | The spelling that works |
698
+ |---|---|---|
699
+ | `defineFsm({ initial: 'nope', states: ['draft'], transitions: [] })` | `defineFsm() initial names the state 'nope', which "states" does not declare — the declared states are 'draft'` — `docPath` `/initial` | a declared id — the message lists them |
700
+ | `on('draft', 'go').to('nope')` in a machine declaring `draft`, `review` | `transition 0 names the state 'nope', which "states" does not declare — the declared states are 'draft', 'review'` — `docPath` `/transitions/0/to` | as above |
701
+ | `on('nope', 'go').to('draft')` | the same message, `docPath` `/transitions/0/from` | as above |
702
+ | `edge('nope', 'out')` in a graph declaring `rows`, `out` | `edge 0 names the node 'nope', which "nodes" does not declare — the declared nodes are 'rows', 'out'` — `docPath` `/edges/0/from` | a declared id — the message lists them |
703
+ | `edge('rows', 'nope')` | the same message, `docPath` `/edges/0/to` | as above |
704
+
705
+ Every message names the id that was not found AND the ids that were,
706
+ which is what makes it useful for a typo: the reader does not have to
707
+ scroll back to the declaration to see what they meant to write.
708
+
709
+ Three checks in a row are worth naming, because they fire in a fixed
710
+ order and only the last one is the engine's:
711
+
712
+ 1. **The type checker.** State ids and node ids are literal types read
713
+ off the declarations, so `to('nope')` and `edge('nope', 'out')` do not
714
+ compile in a typed consumer. That is the cheapest place to be told
715
+ (§5.2).
716
+ 2. **The pen**, for a JavaScript consumer or a dynamically built id:
717
+ `JL0102` at `defineFsm()`/`defineDag()`, naming the id.
718
+ 3. **The compiler**, for a hand-written document that never went through
719
+ the pen: `JF0004` (`initial` is neither `null` nor a declared state),
720
+ `JF0006` (a transition's `from` or `to` names no declared state) and
721
+ `JF0013` (an edge's `from` or `to` names no declared node).
722
+
723
+ ### 4.4 `JL0104` — the closed world is EMPTY
724
+
725
+ A machine's guard, an effect's `with`, a `query` node, a task's `with`
726
+ and an edge's `select` bind NOTHING but `$`. Not `root`, not `path`, not
727
+ a parameter — both flow engines evaluate these with a single `$` and no
728
+ externals at all.
729
+
730
+ | The spelling that trips it | The message | The spelling that works |
731
+ |---|---|---|
732
+ | `on('a', 'go').when((sc, x) => x.root)` | `a when() callback cannot bind 'root' — its query evaluates with no externals at all; anything else has nothing to bind to — when() evaluates over the step scope { state, event, payload, context } (FLOW-FORMAT §3), which its argument IS` | read the scope: `(sc) => sc.context.url` |
733
+ | `effect('x', (sc, y) => ({ n: y.rate }))` | the same, `an effect() with callback cannot bind 'rate'`, with the same step-scope advice | read the scope, or put the value in `context` |
734
+ | `query((v, x) => x.root)` | the same, `a query() callback cannot bind 'root'`, advised `query() evaluates over the node's input scope (FLOW-FORMAT §6.1), which its argument IS` | read the input: `(v) => v.all()` |
735
+ | `task('llm', (v, x) => x.root)` | the same, `a task() with callback cannot bind 'root'` | as above |
736
+ | `edge('a', 'b', { select: (v, x) => x.root })` | the same, `an edge() select callback cannot bind 'root'`, advised `edge() select evaluates over the source value (FLOW-FORMAT §6)` | as above |
737
+
738
+ Refusing at build time is the whole value of this check, and the reason
739
+ is FLOW-FORMAT §5.2: a guard whose evaluation fails does NOT fail the
740
+ step. It reads **false** and the failure is recorded. So a guard naming
741
+ an unbound external would compile, run, and quietly never fire — the
742
+ worst failure mode a state machine has. `JQ2006` at step time is a
743
+ recorded error nobody reads; `JL0104` at build time is a stack trace on
744
+ the line that wrote it.
745
+
746
+ **Why the message names no scope of its own.** The first half comes from
747
+ the shared capture (`packages/linq/src/capture-root.js`), which knows how
748
+ many externals the evaluator binds and nothing else — WHERE a query is
749
+ evaluated is the pen's fact, not the capture's, and this pen's five
750
+ members have three different answers. Each one supplies its own, which is
751
+ what the clause after the dash is. The word "callback" is this pen's too:
752
+ it has no rules.
753
+
754
+ ## 5. The types
755
+
756
+ The declarations are `packages/linq/types/flow.d.ts` (285 lines), and
757
+ every claim below is pinned at compile level in
758
+ `test/consumer/linq-flow.ts` with a runtime twin in
759
+ `test/linq/flow-pen.test.js`. This subpath exports **no builder class, no
760
+ constant and no type guard** — the three kinds the mapping table excludes
761
+ are empty here, so §2 names the whole runtime surface.
762
+
763
+ ```ts
764
+ import { compileDag, compileFsm, fsmToApp } from '@jarenjs/flow';
765
+ import { defineDag, defineFsm, edge, input, on, output, state, task, typedTasks } from '@jarenjs/linq/flow';
766
+ import type { ContextOf, Dag, EventsOf, Fsm, NodesFor, NodesOf, Scope, StatesOf, TasksOf } from '@jarenjs/linq/flow';
767
+
768
+ // over §3.1's machine — no `context` was declared, so ContextOf<> is the honest top
769
+ type State = StatesOf<typeof review>; // 'draft' | 'review' | 'published'
770
+ type Event = EventsOf<typeof review>; // 'submit' | 'approve' (a wildcard names none)
771
+ type NoCtx = ContextOf<typeof review>; // unknown
772
+
773
+ // over §3.3's machine, which declared one
774
+ type Ctx = ContextOf<typeof scored>; // { threshold: number }
775
+
776
+ const machine = compileFsm(review);
777
+ machine.step('review', 'approve', { payload: { fresh: true, by: 'ada' } });
778
+ fsmToApp(review); // a machine of string states projects to app documents
779
+
780
+ // over §3.7's graph
781
+ type Ids = NodesOf<typeof graph>; // 'question' | 'facts' | 'answer' | 'out'
782
+ type Names = TasksOf<typeof graph>; // 'llm'
783
+ compileDag(graph, { tasks: typedTasks(graph, { llm: askTheModel }) });
784
+ ```
785
+
786
+ `Fsm<States, Events, Ctx>` and `Dag<Ids, Tasks>` carry their phantoms —
787
+ `__states`, `__events`, `__context`; `__nodes`, `__tasks` — declared and
788
+ never present at runtime, and the `…Of<>` helpers read them back. A
789
+ transition builder carries `__from`, `__to` and `__event`, and `on()`
790
+ answers a `PendingTransition` that has NO `__to` until `.to()` gives it
791
+ one: a transition that never named its target does not compile, before it
792
+ is the `JL0101` §4.1 shows.
793
+
794
+ ### 5.1 How a captured member is typed, and where the annotation goes
795
+
796
+ The capture itself is the JSLT pen's, and it is explained once, in
797
+ [JSLT-PEN.md](JSLT-PEN.md) §1.1 — a callback run once at build time
798
+ against a recording proxy. What differs here is the closed world: this
799
+ pen calls the shared capture with an EMPTY external list, so a flow
800
+ callback has one argument and a second one binds nothing (§4.4). The
801
+ `fold: false` setting is the same, which is why an effect's literal
802
+ `with` is a constructor and not a `$const`.
803
+
804
+ A captured value's TYPE is the honest top until it is annotated or
805
+ declared, and the pen has three different answers for the three places
806
+ that matters:
807
+
808
+ | Where | How it is typed | Why not by inference |
809
+ |---|---|---|
810
+ | a guard's `sc.payload` | the event's own declaration: `on(from, event, { payload })` | it is the same call — nothing to defer |
811
+ | a guard's `sc.context` | ANNOTATION: `.when((sc: Scope<Thresholds>) => …)` | `on()` is evaluated before `defineFsm()` sees the `context` builder |
812
+ | an `effect()`'s scope | ANNOTATION: `effect('x', (sc: Scope<Thresholds, Approval>) => …)` | an `effect()` is written before the transition that carries it exists |
813
+ | a `query`/`task`/`select` value | ANNOTATION: `query((v: Expr<Row[]>) => …)` | a node's input scope is decided by its inbound EDGES, which are declared after it |
814
+
815
+ What `defineFsm({ context })` types is the MACHINE — `ContextOf<>` — and
816
+ that is the reading a host needs, because the host is what calls
817
+ `step(state, event, { context })`. The annotation inside a guard and the
818
+ builder on the machine are two statements of the same fact, and nothing
819
+ in TypeScript can derive the first from the second here.
820
+
821
+ ### 5.2 Literal ids, and what does not compile
822
+
823
+ ```ts
824
+ // @ts-expect-error — 'nope' is not a declared state (before JL0102, long before JF0006)
825
+ void defineFsm({ initial: 'draft', states: ['draft'], transitions: [on('draft', 'go').to('nope')] });
826
+ // @ts-expect-error — a transition that never named its target is not a transition
827
+ void defineFsm({ initial: 'draft', states: ['draft'], transitions: [on('draft', 'go')] });
828
+ // @ts-expect-error — the initial state must be declared too
829
+ void defineFsm({ initial: 'nope', states: ['draft'], transitions: [] });
830
+ // @ts-expect-error — 'nope' is not a declared node (before JL0102, long before JF0013)
831
+ void defineDag({ nodes: { i: input(), o: output() }, edges: [edge('nope', 'o')] });
832
+ ```
833
+
834
+ The mechanism is `const` type parameters on every id-taking function plus
835
+ `StateIdOf<St[number]>` over the `states` array, so the ids come from the
836
+ declarations themselves rather than from a union the caller maintains. A
837
+ wildcard contributes `never` to the event union, which is why
838
+ `EventsOf<>` of a machine whose only transition is `on('a').to('b')` is
839
+ `never` and not `string`.
840
+
841
+ ### 5.3 `defineDag<Tasks>` cannot exist — and what a caller writes instead
842
+
843
+ State it plainly, because a reader who reaches for the type parameter
844
+ will get a compiler error that does not explain itself: **there is no
845
+ `defineDag<Tasks>(…)` and there cannot be one.**
846
+
847
+ The reason is TypeScript's, not this pen's. A function's type arguments
848
+ are all-or-none: supply one explicitly and every other type parameter
849
+ stops being inferred and falls back to its constraint or its default. The
850
+ node map's literal ids (`'rows' | 'out'`) and its task names are INFERRED
851
+ from the argument, so a signature that also took `Tasks` explicitly would
852
+ have to give up the inference that makes `NodesOf<>` and every
853
+ `edge(from, to)` check work. One or the other, never both.
854
+
855
+ So the pen ships both directions as separate spellings, and a caller who
856
+ wants a registry checked uses the first:
857
+
858
+ ```ts
859
+ interface Registry { llm: (props: { with: unknown; input: unknown }, signal: AbortSignal) => unknown }
860
+
861
+ // direction 1 — annotate the node map: a task naming a handler the registry lacks
862
+ // fails AT THE NODE, which is where the typo is
863
+ const nodes = {
864
+ rows: input(),
865
+ ask: task('llm'),
866
+ out: output(),
867
+ } satisfies NodesFor<Registry>;
868
+
869
+ const graph = defineDag({ nodes, edges: [edge('rows', 'ask'), edge('ask', 'out')] });
870
+ // ^ still Dag<'rows' | 'ask' | 'out', 'llm'> — satisfies preserves the literal types
871
+
872
+ // direction 2 — an identity wrapper at the binding: one handler per declared task name
873
+ compileDag(graph, { tasks: typedTasks(graph, { llm: askTheModel }) });
874
+ // @ts-expect-error — the graph declares 'llm', not 'other'
875
+ void typedTasks(graph, { other: async () => null });
876
+ ```
877
+
878
+ `satisfies` rather than a type annotation is what makes direction 1 work:
879
+ an annotation (`const nodes: NodesFor<Registry> = { … }`) would WIDEN the
880
+ node map to the annotated type and destroy the literal ids, so
881
+ `edge('rows', 'ask')` would stop being checked. `satisfies` checks the
882
+ value against the type and keeps the value's own inferred type, which is
883
+ exactly the trade this needs.
884
+
885
+ The same wall stands in front of `defineFsm<Ctx>` for the same reason,
886
+ and §5.1's annotation table is the answer there. The runtime check for a
887
+ missing handler remains the engine's `JF0018`, at `compileDag` — early,
888
+ not mid-run.
889
+
890
+ ### 5.4 What the pins hold
891
+
892
+ `test/consumer/linq-flow.ts` (119 lines) is the compile-level record. It
893
+ pins, with `Equals<A, B>` — identity in both directions, never
894
+ assignability — `StatesOf<>`, `EventsOf<>` and `ContextOf<>` of a machine
895
+ with a mixed `states` array, a `payload`-declared guard, an annotated
896
+ effect and a wildcard; the `never` event union of a machine whose only
897
+ transition is a wildcard; and `NodesOf<>` and `TasksOf<>` of a six-node
898
+ graph. It pins the two type-level directions of the registry check and
899
+ nine `@ts-expect-error` negatives, each of which FAILS the build if it
900
+ ever starts compiling: an undeclared `to`, an unfinished transition, an
901
+ undeclared `initial`, an unknown `defineFsm` member, an undeclared edge
902
+ end in each direction, an unknown `defineDag` member, a registry naming a
903
+ handler the graph does not declare, and a `task()` naming a handler the
904
+ registry does not provide.
905
+
906
+ ## 6. What it cannot spell
907
+
908
+ ### 6.1 A display guard that decides execution
909
+
910
+ §4.2's refusal restated as a limit, because it is one. The format has
911
+ exactly one guard vocabulary and it is executable: a `$`-rooted path or
912
+ an operator document. There is no way to carry a human-readable condition
913
+ alongside — `[count > 3]` from a diagram label — and have it BE the
914
+ condition, and the format's decision (a non-`$` literal is vacuously
915
+ true) means the closest thing to it is a guard that always passes.
916
+
917
+ The alternative is to carry both: the executable guard in `guard`, and
918
+ the display text wherever the projection wants it. That is what the
919
+ mermaid projection does in the other direction, and it is why the two
920
+ documents can round-trip at all.
921
+
922
+ ### 6.2 There is no workflow pen, and there will not be one
923
+
924
+ `jaren-workflow` is what `@jarenjs/mermaid`'s state-diagram parser
925
+ produces from a diagram: no `$fsm` key, string states, null events and
926
+ null guards. It is a PROJECTION of a machine, and the binder's "no pen,
927
+ by decision" table ([LINQ-FORMAT.md](LINQ-FORMAT.md) §1.0) says why it
928
+ gets no pen of its own — the fsm pen's documents are its executable
929
+ superset.
930
+
931
+ The practical consequence, and it is a good one: a machine written with
932
+ string states and no guards through `defineFsm()` validates under
933
+ `jaren-workflow` as well as under `jaren-fsm`, so a pen-written machine
934
+ is projectable to a diagram without a conversion step, and a
935
+ diagram-parsed workflow compiles with `compileFsm` unchanged.
936
+ `test/linq/flow-pen.test.js` validates a pen document under both
937
+ grammars.
938
+
939
+ ### 6.3 The dataflow's non-goals, which are the format's
940
+
941
+ Named here so a reader does not read absence as an oversight, each with
942
+ the section that owns it. A run is one value in and one value out —
943
+ streaming a graph chunk by chunk changes the node contract and is a
944
+ format revision (§7.5). A machine has no hierarchy, no history states, no
945
+ parallel regions and no delayed or timed transitions (§1.1). A machine's
946
+ current state is a string and storing it is the host's business (§1.1);
947
+ `createFsmSession` is a convenience over that, not persistence. And an
948
+ effect is never invoked by the engine: the descriptors come back as data
949
+ and the host's registry runs them. Both engines' remaining non-goals are
950
+ on [docs/ROADMAP.md](../../../docs/ROADMAP.md) under `@jarenjs/flow`, and
951
+ none of them is a pen limitation.
952
+
953
+ ### 6.4 The shape a `query` node's result takes
954
+
955
+ §3.5's singleton rule is a limit a writer has to plan around today, and
956
+ it belongs in this list even though it is neither a refusal nor a pen
957
+ decision: a `query` node's result is a value, an array or `null`
958
+ depending on how many items the query yielded, and the document cannot
959
+ say which it wants. Until the format answers, the spelling that has one
960
+ meaning is a `$return` that constructs an array explicitly.
961
+
962
+ ### 6.5 When not to reach for this pen
963
+
964
+ - **The machine or the graph is data.** A `jaren-fsm` or `jaren-dag`
965
+ document read from a file, drawn in the studio, or projected from
966
+ mermaid is a value; `compileFsm`/`compileDag` take it directly.
967
+ - **The control flow is a function.** A pipeline that runs in one
968
+ process, never crosses a boundary and is never drawn is three
969
+ `await`s. A dataflow document buys checkpointing, a diagram, a task
970
+ registry a host substitutes and a document a test can assert — pay for
971
+ it when you want one of those.
972
+ - **The states are not a closed set.** `defineFsm` types state ids as
973
+ literals, which is most of what it buys you; a machine whose states are
974
+ computed, loaded, or numerous enough that nobody would type them is
975
+ better as data.
976
+ - **The work is long-running and needs to survive a restart.** §6.2 says
977
+ it plainly: there is no workflow pen and there will not be one.
978
+ Durability, retries, timers and compensation belong to a workflow
979
+ engine, and a dataflow document that grew them would be one wearing the
980
+ wrong name.
981
+ - **A guard needs to ask something the query language cannot.** Guards
982
+ evaluate over the step scope with one `$` and no externals (§4.4), so a
983
+ clock, a lookup, or a call into a service has no spelling. Decide it in
984
+ the host and send a different EVENT — which is what an event is for.
985
+
986
+ ## 7. Cost
987
+
988
+ `@jarenjs/linq/flow` builds to **<!--fact:bundle.flow-->19,910<!--/fact--> bytes** as a minified,
989
+ tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
990
+ measures and `npm run test:tree-shaking` reports, published rounded
991
+ (<!--fact:bundle.flow.kb-->20<!--/fact--> kB) beside the other nine subpath prices in
992
+ [docs/CONSUMING.md](../../../docs/CONSUMING.md).
993
+
994
+ The probe is a gate, not a report. Building a machine with a guard and
995
+ two effects as a consumer would — `defineFsm`, `state`, `on` and
996
+ `effect` — it asserts that the bundle carries:
997
+
998
+ - **no `@jarenjs/flow` byte** — neither compiler, no `fsmToApp`, no
999
+ session. A consumer who only WRITES documents — a build script, a CLI
1000
+ that emits a graph, a test fixture — ships none of the engine;
1001
+ - **no other engine** — nothing of `@jarenjs/json`, `@jarenjs/validate`,
1002
+ `@jarenjs/emit`, `@jarenjs/db`, `@jarenjs/formats`, `@jarenjs/refs` or
1003
+ `@jarenjs/contract`;
1004
+ - **no chain module** — none of `sequence.js`, `document.js`, `async.js`,
1005
+ `concurrency.js`, `provider.js`, `sources.js` or `schema-of.js`;
1006
+ - **of the schema pen, only `brand.js`** — the builder brand, which
1007
+ `on()` and `defineFsm()` need to tell a `payload`/`context` builder
1008
+ from a hand-written object;
1009
+ - **no other pen** — not one byte of `model`, `jslt`, `migration`,
1010
+ `contract`, `app`, `forms` or `db`, which is what §2's `jslt()` row
1011
+ means when it says the stylesheet arrives as a document: the node takes
1012
+ JSON and never imports the pen that wrote it;
1013
+ - **a ceiling** of 20,000 bytes; and the other direction, that neither the
1014
+ chain's bundle nor the schema pen's carries a byte of
1015
+ `packages/linq/src/flow/`.
1016
+
1017
+ Two documents, two grammars, thirteen exported names — and 57 bytes more
1018
+ than `./jslt`'s <!--fact:bundle.jslt-->19,856<!--/fact-->, which writes one. The reason is that most of
1019
+ both prices is the same shared machinery: the recording proxy
1020
+ (`expression.js`), the root capture (`capture-root.js`) and the JSON
1021
+ boundary (`json-boundary.js`). What this pen adds on top of them is 685
1022
+ lines of member checks and the messages §4 quotes — and, as the model
1023
+ pen's own §7 notes, the message text is most of what a mirrored rule
1024
+ costs.
1025
+
1026
+ What a consumer actually pays for §3.6's graph is both subpaths, since
1027
+ the stylesheet has to be written by something — but not the sum: the
1028
+ recording proxy, the root capture and the JSON boundary are shared, so
1029
+ the second subpath adds only its own files. Every figure on this page is
1030
+ measured rather than typed: the probe compares `docs/CONSUMING.md`'s ten
1031
+ rounded prices AND every pen document's exact §7 byte count to the bundle
1032
+ it just built, so a stale number is a red gate rather than a wrong
1033
+ sentence.