@jarenjs/linq 0.49.2 → 0.56.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.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -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 +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
@@ -0,0 +1,1026 @@
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 })).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", "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`'s `with` is `{ "text": "$" }`: the callback returned the scope
357
+ value itself, and the scope value of a node with one unported inbound
358
+ edge IS the delivered value, verbatim (§6.1). `checkpoint: true` is
359
+ written last whatever order `.checkpoint()` was called in, and it is
360
+ opt-in per node — with a checkpoint store configured, only `review`'s
361
+ value is recorded, and a resumed run seeds it instead of calling the
362
+ handler again (§7.6).
363
+
364
+ `v.get('verdict')` in the `select` records `"$['verdict']"` rather than
365
+ `"$.verdict"`. Both are the same path; `get()` always writes the bracket
366
+ form because it takes an arbitrary string. Reach for it when a member
367
+ name is not an identifier, or when it collides with a chain method — a
368
+ member called `count` read as `v.count` answers the method, not the
369
+ member.
370
+
371
+ ### 3.5 A `query` node, and the rule that changes its shape
372
+
373
+ A `query` node runs a Jaren JSON Query over its input scope. This example
374
+ takes a query DOCUMENT rather than a callback, because FLOW-FORMAT §6's
375
+ own example does and because a FLWOR phrase naming its binding `r` is not
376
+ something the chain emits (it packs and names its binding `it`).
377
+
378
+ ```js
379
+ import { defineDag, edge, input, output, query } from '@jarenjs/linq/flow';
380
+
381
+ export const longEnough = defineDag({
382
+ nodes: {
383
+ subs: input(),
384
+ long: query({ $for: { r: '$[*]' }, $where: { $ge: ['$r.words', 500] }, $return: '$r' }),
385
+ out: output(),
386
+ },
387
+ edges: [edge('subs', 'long'), edge('long', 'out')],
388
+ });
389
+ ```
390
+
391
+ ```json
392
+ {
393
+ "$dag": "0.1",
394
+ "nodes": {
395
+ "subs": { "kind": "input" },
396
+ "long": { "kind": "query",
397
+ "query": { "$for": { "r": "$[*]" }, "$where": { "$ge": ["$r.words", 500] }, "$return": "$r" } },
398
+ "out": { "kind": "output" }
399
+ },
400
+ "edges": [
401
+ { "from": "subs", "to": "long" },
402
+ { "from": "long", "to": "out" }
403
+ ]
404
+ }
405
+ ```
406
+
407
+ **A node's result carries the query engine's singleton rule, and the
408
+ result shape changes with the data.** `compileDag` calls a compiled query
409
+ through its default entry point, and QUERY-FORMAT rule 5 identifies a
410
+ one-item sequence with the item. So this graph, run three times:
411
+
412
+ ```json
413
+ [
414
+ { "input": [{"name":"ada","age":36},{"name":"kit","age":9},{"name":"lin","age":20}],
415
+ "result": [{"name":"ada","age":36},{"name":"lin","age":20}] },
416
+ { "input": [{"name":"ada","age":36},{"name":"kit","age":9}],
417
+ "result": {"name":"ada","age":36} },
418
+ { "input": [{"name":"kit","age":9}],
419
+ "result": null }
420
+ ]
421
+ ```
422
+
423
+ Two survivors give an array, ONE survivor gives the row itself, none
424
+ gives `null`. FLOW-FORMAT §6 states only the empty case, and the
425
+ difference is invisible until a downstream `$[*]` iterates an object's
426
+ values instead of an array's items. The pen cannot fix it — this is the
427
+ engine's reading of a document the pen wrote faithfully — so it is
428
+ recorded in [docs/ROADMAP.md](../../../docs/ROADMAP.md) under
429
+ `@jarenjs/flow`, where the resolution is a format decision (a §6 sentence
430
+ and a worked example, or a node-level "always a sequence" option). Until
431
+ then: a `query` node feeding anything that expects a list wants a `$for`
432
+ whose `$return` is explicitly an array constructor, or a downstream node
433
+ that tolerates both.
434
+
435
+ ### 3.6 A `jslt` node
436
+
437
+ A `jslt` node's stylesheet is the JSLT pen's document
438
+ ([JSLT-PEN.md](JSLT-PEN.md)), embedded whole. This is the composition the
439
+ two pens exist for: a dataflow that ends in a rendered view, written
440
+ entirely by code.
441
+
442
+ ```js
443
+ import { defineDag, edge, input, jslt, output } from '@jarenjs/linq/flow';
444
+ import { apply, rule, stylesheet } from '@jarenjs/linq/jslt';
445
+
446
+ export const listing = defineDag({
447
+ nodes: {
448
+ articles: input(),
449
+ list: jslt(stylesheet([
450
+ rule('$', (v) => ['ul', {}, [apply(v.all())]]),
451
+ rule('$[*]', (v) => ['li', {}, v.title]),
452
+ ])),
453
+ out: output(),
454
+ },
455
+ edges: [edge('articles', 'list'), edge('list', 'out')],
456
+ });
457
+ ```
458
+
459
+ ```json
460
+ {
461
+ "$dag": "0.1",
462
+ "nodes": {
463
+ "articles": { "kind": "input" },
464
+ "list": { "kind": "jslt",
465
+ "stylesheet": { "$jslt": "0.1", "rules": [
466
+ { "match": "$", "body": ["ul", {}, [{ "$apply": "$[*]" }]] },
467
+ { "match": "$[*]", "body": ["li", {}, "$.title"] } ] } },
468
+ "out": { "kind": "output" }
469
+ },
470
+ "edges": [
471
+ { "from": "articles", "to": "list" },
472
+ { "from": "list", "to": "out" }
473
+ ]
474
+ }
475
+ ```
476
+
477
+ Over `[{ title: 'ada' }, { title: 'lin' }]` this runs to
478
+ `['ul', {}, [['li', {}, 'ada'], ['li', {}, 'lin']]]` — a `jaren-vnode`
479
+ tree, which is what [JSLT-PEN.md](JSLT-PEN.md) §3.7 is about. The
480
+ envelope form and the bare rules array are both accepted here; the
481
+ envelope is what `stylesheet()` writes, and a rules array is what a hand
482
+ written node usually carries. Nothing about the stylesheet is judged by
483
+ this pen — `jslt()` checks only that the value is JSON, and `JF0014` is
484
+ the compiler's if the stylesheet itself is wrong.
485
+
486
+ ### 3.7 Ports, a constant, and `typedTasks` over the registry
487
+
488
+ A node with more than one inbound edge needs ports: when any inbound edge
489
+ names one, every inbound edge MUST, and the node's `$` becomes the object
490
+ of port-named values in EDGE document order (§6.1). `typedTasks(graph,
491
+ registry)` binds the handler table to the graph — identity at runtime,
492
+ a compile-time check that the table has one handler per declared task
493
+ name.
494
+
495
+ ```js
496
+ import { constant, defineDag, edge, input, output, task, typedTasks } from '@jarenjs/linq/flow';
497
+
498
+ const answering = defineDag({
499
+ nodes: {
500
+ question: input(),
501
+ facts: constant({ product: 'jarenjs', version: '0.52.7' }),
502
+ answer: task('llm', (v) => ({ prompt: v.get('q'), facts: v.get('f') })),
503
+ out: output(),
504
+ },
505
+ edges: [
506
+ edge('question', 'answer', { port: 'q' }),
507
+ edge('facts', 'answer', { port: 'f' }),
508
+ edge('answer', 'out', { select: (v) => v.get('text') }),
509
+ ],
510
+ });
511
+
512
+ // the registry the host will hand compileDag, checked against the graph's task names
513
+ void typedTasks(answering, {
514
+ llm: async ({ with: w }) => ({ text: `${w.prompt} — ${w.facts.product} ${w.facts.version}` }),
515
+ });
516
+
517
+ export const graph = answering;
518
+ ```
519
+
520
+ ```json
521
+ {
522
+ "$dag": "0.1",
523
+ "nodes": {
524
+ "question": { "kind": "input" },
525
+ "facts": { "kind": "const", "value": { "product": "jarenjs", "version": "0.52.7" } },
526
+ "answer": { "kind": "task", "run": "llm",
527
+ "with": { "prompt": "$['q']", "facts": "$['f']" } },
528
+ "out": { "kind": "output" }
529
+ },
530
+ "edges": [
531
+ { "from": "question", "to": "answer", "port": "q" },
532
+ { "from": "facts", "to": "answer", "port": "f" },
533
+ { "from": "answer", "to": "out", "select": "$['text']" }
534
+ ]
535
+ }
536
+ ```
537
+
538
+ `typedTasks` emits nothing — it is the identity function, and the fence
539
+ exports the graph. Rename `llm` to `model` in the registry and the fence
540
+ stops COMPILING, which is the whole point: the check is
541
+ `{ [K in TasksOf<D>]: TaskHandler }`, and the runtime check stays
542
+ `JF0018` for a host that assembled its registry dynamically. §5.3 states
543
+ the other direction — annotating the node map with `NodesFor<Registry>`,
544
+ which catches a misspelled `task('nope')` at the node rather than at the
545
+ binding.
546
+
547
+ ## 4. Refusals
548
+
549
+ The flow pen raises these three `LinqBuildError` codes and no others —
550
+ `test/linq/pen-docs.test.js` holds this list equal, in both directions,
551
+ to the codes `packages/linq/src/flow/` throws. The full condition each
552
+ code states across every pen is the binder's,
553
+ [LINQ-FORMAT.md](LINQ-FORMAT.md) §1.3.
554
+
555
+ | Code | What this pen raises it for |
556
+ |---|---|
557
+ | `JL0101` | a value this pen cannot spell, a member it does not know, or a name → value map it cannot read |
558
+ | `JL0102` | a guard given as a plain STRING, or a state or node id no declaration carries |
559
+ | `JL0104` | any external at all: both engines evaluate with one `$` and nothing else |
560
+
561
+ `packages/linq/src/flow/` carries **33 throw sites** — 30 `JL0101` and 3
562
+ `JL0102`. Two families of condition reach a caller through this pen
563
+ without being thrown in its directory: the effect descriptor and its list
564
+ (`packages/linq/src/effect.js`, shared with the app pen) and the shared
565
+ capture's external check (`packages/linq/src/capture-root.js`), which is
566
+ where every one of this pen's `JL0104`s comes from — the code appears in
567
+ `src/flow/` only in the comments that explain it, and the refusal gate
568
+ counts those, correctly, as the pen owing the reader a row.
569
+
570
+ Every message below is the one the pen raised when the spelling beside it
571
+ was run, with the code prefix (`JL0101: `) removed. `docPath`, where the
572
+ refusal carries one, is the JSON pointer of the node being assembled and
573
+ is appended to the message text as well (`… at /transitions/0/to`).
574
+
575
+ ### 4.1 `JL0101` — the value, the member and the map
576
+
577
+ **The machine.**
578
+
579
+ | The spelling that trips it | The message | The spelling that works |
580
+ |---|---|---|
581
+ | `defineFsm('$')` | `defineFsm() takes { initial, states, transitions, context? }, got a string` | the four members |
582
+ | `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` |
583
+ | `defineFsm({ …, nope: 1 })` | `defineFsm() does not take 'nope' — it takes initial, states, transitions, context` — `docPath` `/nope` | the four members |
584
+ | `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({ … })` |
585
+ | `defineFsm({ …, states: '$' })` | `defineFsm() states is an array of ids and state() declarations, got a string` — `docPath` `/states` | an array |
586
+ | `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')` |
587
+ | `defineFsm({ initial: 42, … })` | `defineFsm() initial takes a state id — a non-empty string, got 42` | a declared id |
588
+ | `defineFsm({ …, transitions: '$' })` | `defineFsm() transitions is an array of on(…) declarations, got a string` — `docPath` `/transitions` | an array |
589
+ | `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')` |
590
+ | `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)` |
591
+
592
+ **A state, a transition, an effect.**
593
+
594
+ | The spelling that trips it | The message | The spelling that works |
595
+ |---|---|---|
596
+ | `state('')` | `state() takes a state id — a non-empty string, got a string` | a non-empty id |
597
+ | `state('a', 'x')` | `state() options are { entry?, exit?, final? }, got a string` | the options object |
598
+ | `state('a', { onEntry: [] })` | `state() does not take 'onEntry' — it takes entry, exit, final` — `docPath` `/onEntry` | `entry` |
599
+ | `state('a', { final: 'yes' })` | `state() final is a boolean, got a string` — `docPath` `/final` | `true` |
600
+ | `state('a', { entry: 'x' })` | `state() entry is an array of effect() descriptors, got a string` | an array |
601
+ | `state('a', { entry: [{ run: 'x' }] })` | `state() entry[0] is effect(run, with?), got a Object instance` | `effect('x')` |
602
+ | `on(1)` | `on() takes a state id — a non-empty string, got 1` | a declared id |
603
+ | `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` |
604
+ | `on('a', 'go', 'x')` | `on() options are { payload? }, got a string` | `{ payload: … }` |
605
+ | `on('a', 'go', { data: s.string() })` | `on() does not take 'data' — it takes payload` — `docPath` `/data` | `payload` |
606
+ | `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({ … })` |
607
+ | `on('a', 'go').to(42)` | `to() takes a state id — a non-empty string, got 42` | a declared id |
608
+ | `on('a', 'go').effects('x')` | `effects() is an array of effect() descriptors, got a string` | an array |
609
+ | `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 |
610
+ | `effect('')` | `effect() takes the handler name as a non-empty string, got a string` — `docPath` `/run` | a registered name |
611
+ | `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 |
612
+
613
+ **The dataflow.**
614
+
615
+ | The spelling that trips it | The message | The spelling that works |
616
+ |---|---|---|
617
+ | `defineDag([])` | `defineDag() takes { nodes, edges }, got a Array instance` | the two members |
618
+ | `defineDag({ …, tasks: {} })` | `defineDag() does not take 'tasks' — it takes nodes, edges` — `docPath` `/tasks` | the registry goes to `compileDag` |
619
+ | `defineDag({ nodes: '$', edges: [] })` | `defineDag() nodes is a plain object of id → node declaration, got a string` — `docPath` `/nodes` | an object |
620
+ | `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() }` |
621
+ | `defineDag({ nodes: {}, edges: [] })` | `defineDag() needs at least one node` — `docPath` `/nodes` | at least one |
622
+ | `defineDag({ nodes: { a: { kind: 'input' } }, … })` | `defineDag() node 'a' is input(), constant(), query(), jslt(), task() or output(), got a Object instance` — `docPath` `/nodes/a` | `input()` |
623
+ | `defineDag({ …, edges: {} })` | `defineDag() edges is an array of edge(from, to) declarations, got a Object instance` — `docPath` `/edges` | an array |
624
+ | `defineDag({ …, edges: [{ from: 'a', to: 'a' }] })` | `defineDag() edges[0] is edge(from, to, options?), got a Object instance` — `docPath` `/edges/0` | `edge('a', 'a')` |
625
+ | `constant(undefined)` | `constant() takes the value the node yields — every JSON value, null included; undefined is not one` — `docPath` `/value` | any JSON value |
626
+ | `constant(new Date(0))` | `constant() received a Date instance, which is not JSON — …` | its ISO string, or its epoch number |
627
+ | `query(undefined)` | `query() takes a callback (v) => … captured over the node input, or a query document` — `docPath` `/query` | one of the two |
628
+ | `jslt(undefined)` | `jslt() takes a stylesheet document — the JSLT pen's stylesheet(…) or rule array, or one written by hand` — `docPath` `/stylesheet` | `stylesheet([…])` |
629
+ | `task('')` | `task() takes the handler name as a non-empty string, got a string` — `docPath` `/run` | a registry name |
630
+ | `edge('', 'b')`, `edge('a', '')` | `edge() takes the producing node id as a non-empty string, got a string` — `docPath` `/from` | a declared id |
631
+ | `edge('a', 'b', 'x')` | `edge() options are { port?, select? }, got a string` | the options object |
632
+ | `edge('a', 'b', { port: '' })` | `edge() port is a non-empty string, got a string` — `docPath` `/port` | a port name |
633
+ | `edge('a', 'b', { ports: 'x' })` | `edge() does not take 'ports' — it takes port, select` — `docPath` `/ports` | `port` |
634
+
635
+ A refusal a captured member can raise that is the CHAIN's rather than
636
+ this pen's: `effect('x', () => new Date(0))` is `JL0005` ("a captured
637
+ expression cannot embed a Date instance — it carries no own enumerable
638
+ members, so it would embed as `{}`"). It is documented in
639
+ [QUERY-PEN.md](QUERY-PEN.md) §9.
640
+
641
+ ### 4.2 `JL0102` — a guard given as a plain string
642
+
643
+ This deserves its own prose because a reader will hit it, and because the
644
+ message alone does not explain why a perfectly reasonable-looking string
645
+ is refused.
646
+
647
+ FLOW-FORMAT §3 asserts a guard by **effective boolean value**. A string
648
+ that does not start with `$` is a literal string, and a non-empty literal
649
+ is EBV-true. So a guard spelled `'count > 3'` is not a condition the
650
+ engine fails to parse — it is a condition that passes, on every step,
651
+ forever, silently. That is deliberate in the FORMAT: the mermaid state
652
+ diagram projection carries opaque display guards lifted from diagram
653
+ labels (`[count > 3]`), and a picture's annotation MUST NOT change
654
+ execution.
655
+
656
+ The pen refuses to emit one, because a pen writing that document is not
657
+ projecting a diagram — it is writing code that meant something.
658
+
659
+ | The spelling that trips it | The message | The spelling that works |
660
+ |---|---|---|
661
+ | `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))` |
662
+ | `on('a', 'go').when('$.payload.fresh')` | the same message, with `"$.payload.fresh"` | `.when((sc) => sc.payload.fresh)` |
663
+
664
+ The second row is the one that catches people. `'$.payload.fresh'` IS a
665
+ valid guard document — the engine would read it as a path and evaluate it
666
+ correctly. The pen refuses it anyway, and the reason is that it cannot
667
+ tell that string apart from the first row's at build time without
668
+ becoming a JSONPath parser, which LINQ-FORMAT §1.1 rule 1 forbids: a pen
669
+ mirrors the engine's rules, it does not re-implement its compiler.
670
+ Refusing every plain string is the rule that has no wrong answers, and
671
+ the callback is both shorter and typed:
672
+
673
+ ```js
674
+ // refused, though the document it would write is correct
675
+ on('review', 'approve').when('$.payload.fresh').to('published')
676
+
677
+ // what the writer meant, captured — and sc.payload is typed by the event's declaration
678
+ on('review', 'approve', { payload: Approval }).when((sc) => sc.payload.fresh).to('published')
679
+ ```
680
+
681
+ A guard written as a query DOCUMENT rides verbatim and is not refused —
682
+ `.when({ $gt: ['$.payload.n', 3] })` emits exactly that. The refusal is
683
+ on the string form alone, because the string form is the ambiguous one.
684
+
685
+ ### 4.3 `JL0102` — an id no declaration carries
686
+
687
+ The other `JL0102` condition, and the pen's clearest case of catching the
688
+ engine's own rule one step earlier. Both machines and dataflows have it.
689
+
690
+ | The spelling that trips it | The message | The spelling that works |
691
+ |---|---|---|
692
+ | `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 |
693
+ | `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 |
694
+ | `on('nope', 'go').to('draft')` | the same message, `docPath` `/transitions/0/from` | as above |
695
+ | `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 |
696
+ | `edge('rows', 'nope')` | the same message, `docPath` `/edges/0/to` | as above |
697
+
698
+ Every message names the id that was not found AND the ids that were,
699
+ which is what makes it useful for a typo: the reader does not have to
700
+ scroll back to the declaration to see what they meant to write.
701
+
702
+ Three checks in a row are worth naming, because they fire in a fixed
703
+ order and only the last one is the engine's:
704
+
705
+ 1. **The type checker.** State ids and node ids are literal types read
706
+ off the declarations, so `to('nope')` and `edge('nope', 'out')` do not
707
+ compile in a typed consumer. That is the cheapest place to be told
708
+ (§5.2).
709
+ 2. **The pen**, for a JavaScript consumer or a dynamically built id:
710
+ `JL0102` at `defineFsm()`/`defineDag()`, naming the id.
711
+ 3. **The compiler**, for a hand-written document that never went through
712
+ the pen: `JF0004` (`initial` is neither `null` nor a declared state),
713
+ `JF0006` (a transition's `from` or `to` names no declared state) and
714
+ `JF0013` (an edge's `from` or `to` names no declared node).
715
+
716
+ ### 4.4 `JL0104` — the closed world is EMPTY
717
+
718
+ A machine's guard, an effect's `with`, a `query` node, a task's `with`
719
+ and an edge's `select` bind NOTHING but `$`. Not `root`, not `path`, not
720
+ a parameter — both flow engines evaluate these with a single `$` and no
721
+ externals at all.
722
+
723
+ | The spelling that trips it | The message | The spelling that works |
724
+ |---|---|---|
725
+ | `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` |
726
+ | `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` |
727
+ | `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()` |
728
+ | `task('llm', (v, x) => x.root)` | the same, `a task() with callback cannot bind 'root'` | as above |
729
+ | `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 |
730
+
731
+ Refusing at build time is the whole value of this check, and the reason
732
+ is FLOW-FORMAT §5.2: a guard whose evaluation fails does NOT fail the
733
+ step. It reads **false** and the failure is recorded. So a guard naming
734
+ an unbound external would compile, run, and quietly never fire — the
735
+ worst failure mode a state machine has. `JQ2006` at step time is a
736
+ recorded error nobody reads; `JL0104` at build time is a stack trace on
737
+ the line that wrote it.
738
+
739
+ **Why the message names no scope of its own.** The first half comes from
740
+ the shared capture (`packages/linq/src/capture-root.js`), which knows how
741
+ many externals the evaluator binds and nothing else — WHERE a query is
742
+ evaluated is the pen's fact, not the capture's, and this pen's five
743
+ members have three different answers. Each one supplies its own, which is
744
+ what the clause after the dash is. The word "callback" is this pen's too:
745
+ it has no rules.
746
+
747
+ ## 5. The types
748
+
749
+ The declarations are `packages/linq/types/flow.d.ts` (285 lines), and
750
+ every claim below is pinned at compile level in
751
+ `test/consumer/linq-flow.ts` with a runtime twin in
752
+ `test/linq/flow-pen.test.js`. This subpath exports **no builder class, no
753
+ constant and no type guard** — the three kinds the mapping table excludes
754
+ are empty here, so §2 names the whole runtime surface.
755
+
756
+ ```ts
757
+ import { compileDag, compileFsm, fsmToApp } from '@jarenjs/flow';
758
+ import { defineDag, defineFsm, edge, input, on, output, state, task, typedTasks } from '@jarenjs/linq/flow';
759
+ import type { ContextOf, Dag, EventsOf, Fsm, NodesFor, NodesOf, Scope, StatesOf, TasksOf } from '@jarenjs/linq/flow';
760
+
761
+ // over §3.1's machine — no `context` was declared, so ContextOf<> is the honest top
762
+ type State = StatesOf<typeof review>; // 'draft' | 'review' | 'published'
763
+ type Event = EventsOf<typeof review>; // 'submit' | 'approve' (a wildcard names none)
764
+ type NoCtx = ContextOf<typeof review>; // unknown
765
+
766
+ // over §3.3's machine, which declared one
767
+ type Ctx = ContextOf<typeof scored>; // { threshold: number }
768
+
769
+ const machine = compileFsm(review);
770
+ machine.step('review', 'approve', { payload: { fresh: true, by: 'ada' } });
771
+ fsmToApp(review); // a machine of string states projects to app documents
772
+
773
+ // over §3.7's graph
774
+ type Ids = NodesOf<typeof graph>; // 'question' | 'facts' | 'answer' | 'out'
775
+ type Names = TasksOf<typeof graph>; // 'llm'
776
+ compileDag(graph, { tasks: typedTasks(graph, { llm: askTheModel }) });
777
+ ```
778
+
779
+ `Fsm<States, Events, Ctx>` and `Dag<Ids, Tasks>` carry their phantoms —
780
+ `__states`, `__events`, `__context`; `__nodes`, `__tasks` — declared and
781
+ never present at runtime, and the `…Of<>` helpers read them back. A
782
+ transition builder carries `__from`, `__to` and `__event`, and `on()`
783
+ answers a `PendingTransition` that has NO `__to` until `.to()` gives it
784
+ one: a transition that never named its target does not compile, before it
785
+ is the `JL0101` §4.1 shows.
786
+
787
+ ### 5.1 How a captured member is typed, and where the annotation goes
788
+
789
+ The capture itself is the JSLT pen's, and it is explained once, in
790
+ [JSLT-PEN.md](JSLT-PEN.md) §1.1 — a callback run once at build time
791
+ against a recording proxy. What differs here is the closed world: this
792
+ pen calls the shared capture with an EMPTY external list, so a flow
793
+ callback has one argument and a second one binds nothing (§4.4). The
794
+ `fold: false` setting is the same, which is why an effect's literal
795
+ `with` is a constructor and not a `$const`.
796
+
797
+ A captured value's TYPE is the honest top until it is annotated or
798
+ declared, and the pen has three different answers for the three places
799
+ that matters:
800
+
801
+ | Where | How it is typed | Why not by inference |
802
+ |---|---|---|
803
+ | a guard's `sc.payload` | the event's own declaration: `on(from, event, { payload })` | it is the same call — nothing to defer |
804
+ | a guard's `sc.context` | ANNOTATION: `.when((sc: Scope<Thresholds>) => …)` | `on()` is evaluated before `defineFsm()` sees the `context` builder |
805
+ | an `effect()`'s scope | ANNOTATION: `effect('x', (sc: Scope<Thresholds, Approval>) => …)` | an `effect()` is written before the transition that carries it exists |
806
+ | 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 |
807
+
808
+ What `defineFsm({ context })` types is the MACHINE — `ContextOf<>` — and
809
+ that is the reading a host needs, because the host is what calls
810
+ `step(state, event, { context })`. The annotation inside a guard and the
811
+ builder on the machine are two statements of the same fact, and nothing
812
+ in TypeScript can derive the first from the second here.
813
+
814
+ ### 5.2 Literal ids, and what does not compile
815
+
816
+ ```ts
817
+ // @ts-expect-error — 'nope' is not a declared state (before JL0102, long before JF0006)
818
+ void defineFsm({ initial: 'draft', states: ['draft'], transitions: [on('draft', 'go').to('nope')] });
819
+ // @ts-expect-error — a transition that never named its target is not a transition
820
+ void defineFsm({ initial: 'draft', states: ['draft'], transitions: [on('draft', 'go')] });
821
+ // @ts-expect-error — the initial state must be declared too
822
+ void defineFsm({ initial: 'nope', states: ['draft'], transitions: [] });
823
+ // @ts-expect-error — 'nope' is not a declared node (before JL0102, long before JF0013)
824
+ void defineDag({ nodes: { i: input(), o: output() }, edges: [edge('nope', 'o')] });
825
+ ```
826
+
827
+ The mechanism is `const` type parameters on every id-taking function plus
828
+ `StateIdOf<St[number]>` over the `states` array, so the ids come from the
829
+ declarations themselves rather than from a union the caller maintains. A
830
+ wildcard contributes `never` to the event union, which is why
831
+ `EventsOf<>` of a machine whose only transition is `on('a').to('b')` is
832
+ `never` and not `string`.
833
+
834
+ ### 5.3 `defineDag<Tasks>` cannot exist — and what a caller writes instead
835
+
836
+ State it plainly, because a reader who reaches for the type parameter
837
+ will get a compiler error that does not explain itself: **there is no
838
+ `defineDag<Tasks>(…)` and there cannot be one.**
839
+
840
+ The reason is TypeScript's, not this pen's. A function's type arguments
841
+ are all-or-none: supply one explicitly and every other type parameter
842
+ stops being inferred and falls back to its constraint or its default. The
843
+ node map's literal ids (`'rows' | 'out'`) and its task names are INFERRED
844
+ from the argument, so a signature that also took `Tasks` explicitly would
845
+ have to give up the inference that makes `NodesOf<>` and every
846
+ `edge(from, to)` check work. One or the other, never both.
847
+
848
+ So the pen ships both directions as separate spellings, and a caller who
849
+ wants a registry checked uses the first:
850
+
851
+ ```ts
852
+ interface Registry { llm: (props: { with: unknown; input: unknown }, signal: AbortSignal) => unknown }
853
+
854
+ // direction 1 — annotate the node map: a task naming a handler the registry lacks
855
+ // fails AT THE NODE, which is where the typo is
856
+ const nodes = {
857
+ rows: input(),
858
+ ask: task('llm'),
859
+ out: output(),
860
+ } satisfies NodesFor<Registry>;
861
+
862
+ const graph = defineDag({ nodes, edges: [edge('rows', 'ask'), edge('ask', 'out')] });
863
+ // ^ still Dag<'rows' | 'ask' | 'out', 'llm'> — satisfies preserves the literal types
864
+
865
+ // direction 2 — an identity wrapper at the binding: one handler per declared task name
866
+ compileDag(graph, { tasks: typedTasks(graph, { llm: askTheModel }) });
867
+ // @ts-expect-error — the graph declares 'llm', not 'other'
868
+ void typedTasks(graph, { other: async () => null });
869
+ ```
870
+
871
+ `satisfies` rather than a type annotation is what makes direction 1 work:
872
+ an annotation (`const nodes: NodesFor<Registry> = { … }`) would WIDEN the
873
+ node map to the annotated type and destroy the literal ids, so
874
+ `edge('rows', 'ask')` would stop being checked. `satisfies` checks the
875
+ value against the type and keeps the value's own inferred type, which is
876
+ exactly the trade this needs.
877
+
878
+ The same wall stands in front of `defineFsm<Ctx>` for the same reason,
879
+ and §5.1's annotation table is the answer there. The runtime check for a
880
+ missing handler remains the engine's `JF0018`, at `compileDag` — early,
881
+ not mid-run.
882
+
883
+ ### 5.4 What the pins hold
884
+
885
+ `test/consumer/linq-flow.ts` (119 lines) is the compile-level record. It
886
+ pins, with `Equals<A, B>` — identity in both directions, never
887
+ assignability — `StatesOf<>`, `EventsOf<>` and `ContextOf<>` of a machine
888
+ with a mixed `states` array, a `payload`-declared guard, an annotated
889
+ effect and a wildcard; the `never` event union of a machine whose only
890
+ transition is a wildcard; and `NodesOf<>` and `TasksOf<>` of a six-node
891
+ graph. It pins the two type-level directions of the registry check and
892
+ nine `@ts-expect-error` negatives, each of which FAILS the build if it
893
+ ever starts compiling: an undeclared `to`, an unfinished transition, an
894
+ undeclared `initial`, an unknown `defineFsm` member, an undeclared edge
895
+ end in each direction, an unknown `defineDag` member, a registry naming a
896
+ handler the graph does not declare, and a `task()` naming a handler the
897
+ registry does not provide.
898
+
899
+ ## 6. What it cannot spell
900
+
901
+ ### 6.1 A display guard that decides execution
902
+
903
+ §4.2's refusal restated as a limit, because it is one. The format has
904
+ exactly one guard vocabulary and it is executable: a `$`-rooted path or
905
+ an operator document. There is no way to carry a human-readable condition
906
+ alongside — `[count > 3]` from a diagram label — and have it BE the
907
+ condition, and the format's decision (a non-`$` literal is vacuously
908
+ true) means the closest thing to it is a guard that always passes.
909
+
910
+ The alternative is to carry both: the executable guard in `guard`, and
911
+ the display text wherever the projection wants it. That is what the
912
+ mermaid projection does in the other direction, and it is why the two
913
+ documents can round-trip at all.
914
+
915
+ ### 6.2 There is no workflow pen, and there will not be one
916
+
917
+ `jaren-workflow` is what `@jarenjs/mermaid`'s state-diagram parser
918
+ produces from a diagram: no `$fsm` key, string states, null events and
919
+ null guards. It is a PROJECTION of a machine, and the binder's "no pen,
920
+ by decision" table ([LINQ-FORMAT.md](LINQ-FORMAT.md) §1.0) says why it
921
+ gets no pen of its own — the fsm pen's documents are its executable
922
+ superset.
923
+
924
+ The practical consequence, and it is a good one: a machine written with
925
+ string states and no guards through `defineFsm()` validates under
926
+ `jaren-workflow` as well as under `jaren-fsm`, so a pen-written machine
927
+ is projectable to a diagram without a conversion step, and a
928
+ diagram-parsed workflow compiles with `compileFsm` unchanged.
929
+ `test/linq/flow-pen.test.js` validates a pen document under both
930
+ grammars.
931
+
932
+ ### 6.3 The dataflow's non-goals, which are the format's
933
+
934
+ Named here so a reader does not read absence as an oversight, each with
935
+ the section that owns it. A run is one value in and one value out —
936
+ streaming a graph chunk by chunk changes the node contract and is a
937
+ format revision (§7.5). A machine has no hierarchy, no history states, no
938
+ parallel regions and no delayed or timed transitions (§1.1). A machine's
939
+ current state is a string and storing it is the host's business (§1.1);
940
+ `createFsmSession` is a convenience over that, not persistence. And an
941
+ effect is never invoked by the engine: the descriptors come back as data
942
+ and the host's registry runs them. Both engines' remaining non-goals are
943
+ on [docs/ROADMAP.md](../../../docs/ROADMAP.md) under `@jarenjs/flow`, and
944
+ none of them is a pen limitation.
945
+
946
+ ### 6.4 The shape a `query` node's result takes
947
+
948
+ §3.5's singleton rule is a limit a writer has to plan around today, and
949
+ it belongs in this list even though it is neither a refusal nor a pen
950
+ decision: a `query` node's result is a value, an array or `null`
951
+ depending on how many items the query yielded, and the document cannot
952
+ say which it wants. Until the format answers, the spelling that has one
953
+ meaning is a `$return` that constructs an array explicitly.
954
+
955
+ ### 6.5 When not to reach for this pen
956
+
957
+ - **The machine or the graph is data.** A `jaren-fsm` or `jaren-dag`
958
+ document read from a file, drawn in the studio, or projected from
959
+ mermaid is a value; `compileFsm`/`compileDag` take it directly.
960
+ - **The control flow is a function.** A pipeline that runs in one
961
+ process, never crosses a boundary and is never drawn is three
962
+ `await`s. A dataflow document buys checkpointing, a diagram, a task
963
+ registry a host substitutes and a document a test can assert — pay for
964
+ it when you want one of those.
965
+ - **The states are not a closed set.** `defineFsm` types state ids as
966
+ literals, which is most of what it buys you; a machine whose states are
967
+ computed, loaded, or numerous enough that nobody would type them is
968
+ better as data.
969
+ - **The work is long-running and needs to survive a restart.** §6.2 says
970
+ it plainly: there is no workflow pen and there will not be one.
971
+ Durability, retries, timers and compensation belong to a workflow
972
+ engine, and a dataflow document that grew them would be one wearing the
973
+ wrong name.
974
+ - **A guard needs to ask something the query language cannot.** Guards
975
+ evaluate over the step scope with one `$` and no externals (§4.4), so a
976
+ clock, a lookup, or a call into a service has no spelling. Decide it in
977
+ the host and send a different EVENT — which is what an event is for.
978
+
979
+ ## 7. Cost
980
+
981
+ `@jarenjs/linq/flow` builds to **<!--fact:bundle.flow-->19,181<!--/fact--> bytes** as a minified,
982
+ tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
983
+ measures and `npm run test:tree-shaking` reports, published rounded
984
+ (<!--fact:bundle.flow.kb-->19<!--/fact--> kB) beside the other nine subpath prices in
985
+ [docs/CONSUMING.md](../../../docs/CONSUMING.md).
986
+
987
+ The probe is a gate, not a report. Building a machine with a guard and
988
+ two effects as a consumer would — `defineFsm`, `state`, `on` and
989
+ `effect` — it asserts that the bundle carries:
990
+
991
+ - **no `@jarenjs/flow` byte** — neither compiler, no `fsmToApp`, no
992
+ session. A consumer who only WRITES documents — a build script, a CLI
993
+ that emits a graph, a test fixture — ships none of the engine;
994
+ - **no other engine** — nothing of `@jarenjs/json`, `@jarenjs/validate`,
995
+ `@jarenjs/emit`, `@jarenjs/db`, `@jarenjs/formats`, `@jarenjs/refs` or
996
+ `@jarenjs/contract`;
997
+ - **no chain module** — none of `sequence.js`, `document.js`, `async.js`,
998
+ `concurrency.js`, `provider.js`, `sources.js` or `schema-of.js`;
999
+ - **of the schema pen, only `brand.js`** — the builder brand, which
1000
+ `on()` and `defineFsm()` need to tell a `payload`/`context` builder
1001
+ from a hand-written object;
1002
+ - **no other pen** — not one byte of `model`, `jslt`, `migration`,
1003
+ `contract`, `app`, `forms` or `db`, which is what §2's `jslt()` row
1004
+ means when it says the stylesheet arrives as a document: the node takes
1005
+ JSON and never imports the pen that wrote it;
1006
+ - **a ceiling** of 20,000 bytes; and the other direction, that neither the
1007
+ chain's bundle nor the schema pen's carries a byte of
1008
+ `packages/linq/src/flow/`.
1009
+
1010
+ Two documents, two grammars, thirteen exported names — and 57 bytes more
1011
+ than `./jslt`'s <!--fact:bundle.jslt-->19,124<!--/fact-->, which writes one. The reason is that most of
1012
+ both prices is the same shared machinery: the recording proxy
1013
+ (`expression.js`), the root capture (`capture-root.js`) and the JSON
1014
+ boundary (`json-boundary.js`). What this pen adds on top of them is 685
1015
+ lines of member checks and the messages §4 quotes — and, as the model
1016
+ pen's own §7 notes, the message text is most of what a mirrored rule
1017
+ costs.
1018
+
1019
+ What a consumer actually pays for §3.6's graph is both subpaths, since
1020
+ the stylesheet has to be written by something — but not the sum: the
1021
+ recording proxy, the root capture and the JSON boundary are shared, so
1022
+ the second subpath adds only its own files. Every figure on this page is
1023
+ measured rather than typed: the probe compares `docs/CONSUMING.md`'s ten
1024
+ rounded prices AND every pen document's exact §7 byte count to the bundle
1025
+ it just built, so a stale number is a red gate rather than a wrong
1026
+ sentence.