@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,1143 @@
1
+ # The Jaren app pen
2
+
3
+ > `./app` — the `jaren-app` 0.1 document `createApp` runs, and the JSON
4
+ > Schema of its state beside it. **Read it when** you are declaring a
5
+ > whole application: state, view, actions, effects
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
+ An app document is a whole interactive application as one JSON value —
18
+ its state, the view that renders it, the actions that change it, the
19
+ subscriptions that feed it. Written by hand it is JSON Pointers in
20
+ strings, action names in strings, and query expressions in object
21
+ literals, none of which anything checks until the app runs. This pen
22
+ makes each of them a function call over the state you declared, so a
23
+ pointer is derived from the shape, an action name a view binds is checked
24
+ against the actions map, and a patch you can spell wrong does not
25
+ compile.
26
+
27
+ ```js
28
+ import { defineApp, action, transition, effect, bind, sub } from '@jarenjs/linq/app';
29
+ import { add, append, replace, remove, move, copy, test } from '@jarenjs/linq/app';
30
+ ```
31
+
32
+ writes the `jaren-app` 0.1 document
33
+ ([APP-FORMAT](../../app/docs/APP-FORMAT.md)) `createApp` runs — a whole
34
+ interactive application as one JSON value — and, beside it, the JSON
35
+ Schema of its state. The two are two members of ONE result,
36
+ `{ document, stateSchema }`, and are never merged: the format has no
37
+ slot for a state schema, and the hook that wants one is
38
+ `options.validateState` (APP-FORMAT §6), which is a `createApp` option.
39
+
40
+ Thirteen names, and they fall into four groups a reader can hold at
41
+ once: `defineApp()` writes the document; `action()`, `transition()`,
42
+ `effect()` write a transition; the seven patch operations write the
43
+ `patch` inside it; and `bind()` and `sub()` write the two places the
44
+ outside world reaches in — an event, and a subscription.
45
+
46
+ **The running example.** §3 is one project board, taken a feature at a
47
+ time: the tag list whose patch semantics catch everybody out first, the
48
+ estimate counter that is the smallest complete app, the card promotion
49
+ that exercises all seven patch operations, the filter box that reads the
50
+ event, the card list with an effect beside its patch, the live columns
51
+ that fan a subscription out per column, and the grid whose view binds
52
+ actions. Each fence is a whole document — that is what the gate runs —
53
+ and read in order they are the same application growing. §5 reads the
54
+ types back off it.
55
+
56
+ ### 1.1 One document, one capture
57
+
58
+ An action is ONE query document over APP-FORMAT §3.1's three names —
59
+ `$` the whole state, `$event` the serializable event slice, `$payload`
60
+ the dispatch payload — so everything inside it is captured in one pass.
61
+ `transition()`, `effect()` and the patch helpers assemble plain objects
62
+ and leave the spelling to the capture already running; none of them
63
+ starts a capture of its own.
64
+
65
+ That is what makes the format's own guarantee expressible — the
66
+ increment in a patch and the one in an effect's props are the SAME
67
+ expression, because both evaluate against the pre-transition state
68
+ (§3.5 shows the two `{ "$add": ["$.pending", 1] }` nodes) — and it is
69
+ why an effect's props are a VALUE here where the flow pen's are a
70
+ callback ([FLOW-PEN.md](FLOW-PEN.md)): there, each guard is its own
71
+ document; here, the action is.
72
+
73
+ A view is not part of that capture. The `view` member is a JSLT
74
+ stylesheet ([JSLT-PEN.md](JSLT-PEN.md)), captured per rule body against
75
+ its own scope, and a `bind()` inside one is ordinary data the body
76
+ spells — which is what replaces a payload-creator function, because a
77
+ payload built at render time may embed the matched node's own members.
78
+
79
+ ### 1.2 The initial state is derived
80
+
81
+ The initial state comes from the state builder's `default()`s unless
82
+ `initial` names one. A member contributes its `default` (or its
83
+ `const`); an object recurses; an object resolves when it is required or
84
+ when at least one of its own members did, so an optional block of
85
+ defaults appears and an optional empty one does not; and a REQUIRED
86
+ member that resolves to nothing is `JL0102` naming its pointer
87
+ ([§4.3](#43-jl0102--the-state-the-pointer-and-the-binding)) — because a
88
+ state whose required member is absent fails its own `validateState` on
89
+ the boot transaction, which is a fatal `JA0007` and a bad way to learn
90
+ about a missing default.
91
+
92
+ The rule that follows is worth stating on its own, because it is what
93
+ the derivation MEANS: **the state a default describes is the state the
94
+ document carries.** There is no second source. If the document's initial
95
+ state is wrong, a `default()` is wrong or `initial` is wrong, and both
96
+ are visible in the source that wrote it.
97
+
98
+ ### 1.3 The pen carries no engine
99
+
100
+ The pen imports nothing of `@jarenjs/app`, `@jarenjs/view`,
101
+ `@jarenjs/json` or `@jarenjs/validate`: the loop's compiler stays the
102
+ only judge of what an app means. `test/linq/app-pen.test.js` reads every
103
+ file of `packages/linq/src/app/` and asserts the import is absent, and
104
+ the tree-shaking probe (§7) measures the bundle that follows from it.
105
+
106
+ ## 2. The mapping table
107
+
108
+ Every name `@jarenjs/linq/app` exports — all thirteen — and the members
109
+ each writes. The subpath exports no builder class, no constant and no
110
+ guard, so §5 documents the exported TYPES rather than a set of values a
111
+ caller meets.
112
+
113
+ Status: **native** (emits the named member), **emulated** (a composition
114
+ with identical semantics), **refused** (a coded error naming the
115
+ reason).
116
+
117
+ ### 2.1 The document
118
+
119
+ The one call that assembles an application, and the two members it
120
+ answers.
121
+
122
+ | Method | Emits | Type reading | Status |
123
+ |---|---|---|---|
124
+ | `defineApp({ state, initial?, schema?, view, actions?, subs? })` | `{ document: { $app: '0.1', state?, view, actions?, subs? }, stateSchema }` — only the members the author declared, deep-frozen | `AppResult<State, Actions>`; `StateOf<>` and `ActionsOf<>` read it back | native; a member the pen does not know, a missing `view`, a `schema` beside a builder state `JL0101`; an underivable initial state, or a view binding an undeclared action, `JL0102` |
125
+ | `state` | the initial value, from the builder's `default()`s (§1.2); the builder itself becomes `stateSchema` | `Infer<B>` | native |
126
+ | `initial` | the initial value verbatim, in place of the derivation | `Infer<B>` | native; a non-JSON value `JL0101` |
127
+ | `schema` | nothing — it TYPES a `state` given as a plain JSON value, and becomes `stateSchema` | `Infer<B>` | native; beside a builder state, `JL0101` (a builder state IS its schema) |
128
+ | `view` | the JSLT stylesheet verbatim — the pen's `stylesheet([rule(…)])` envelope ([JSLT-PEN.md](JSLT-PEN.md)) or APP-FORMAT §2's bare rule array | `unknown`: a view is a document the grammar judges | native |
129
+ | `actions` | the `actions` map, one captured action document per name | `keyof A & string` — the literal names, which is what `bind<>()` is checked against | native; a value that is not an `action()` `JL0101` |
130
+ | `subs` | the `subs` array, one `sub()` entry per element | `readonly SubDeclaration[]` | native; a value that is not a `sub()` `JL0101` |
131
+
132
+ ### 2.2 The transition
133
+
134
+ What an action returns: the patch to apply, the effects to hand the host,
135
+ and nothing else — a transition is data, so a test can read it.
136
+
137
+ | Method | Emits | Type reading | Status |
138
+ |---|---|---|---|
139
+ | `action(fn, { payload?, event? })` | one action document, captured over `$`, `$event`, `$payload` | `ActionDeclaration<Payload>`; `payload` and `event` are TYPES — the format carries no schema for either, and nothing is emitted for them | native; a non-builder `payload` `JL0101`; an excluded `event` field `JL0102`; a name §3.1 does not bind `JL0104` |
140
+ | `transition({ state?, patch?, effects? })` | the transition object of APP-FORMAT §3.2, in the order the runtime applies it | `Transition` | native; another member `JL0101` |
141
+ | `effect(run, with?)` | `{ run, with? }` (§5.1); `with` is a value in the ACTION's scope, not a callback | `EffectDeclaration<Run>` — `Run` is a literal | native; an empty `run` `JL0101` |
142
+
143
+ Returning nothing from an action is the format's own no-op, and
144
+ `transition({})` is that same empty object: it is allowed, and it is
145
+ what an action that only fires an effect on a later turn writes.
146
+
147
+ ### 2.3 The seven patch operations
148
+
149
+ A `path` is a captured lambda over the state — `(st) => st.todos` is
150
+ `/todos` — so a pointer is DERIVED from the state shape and a typo is a
151
+ capture error rather than a silent `JA2004` at dispatch. A pointer
152
+ string is accepted verbatim for the locations a shape cannot spell.
153
+
154
+ | Method | Emits | Type reading | Status |
155
+ |---|---|---|---|
156
+ | `add(path, value)` | `{ op: 'add', path, value }` — **sets a member, and REPLACES an array when the path names one** | `PatchOp` | native |
157
+ | `append(path, value)` | `{ op: 'add', path: '<path>/-', value }` — RFC 6902's array APPEND, the same op at the array's `-` position | `PatchOp` | native |
158
+ | `replace(path, value)` | `{ op: 'replace', path, value }` — the op a transition writes most, and the one an array ELEMENT needs | `PatchOp` | native |
159
+ | `remove(path)` | `{ op: 'remove', path }` | `PatchOp` | native |
160
+ | `move(from, path)` | `{ op: 'move', from, path }` — both lowered as pointers | `PatchOp` | native |
161
+ | `copy(from, path)` | `{ op: 'copy', from, path }` | `PatchOp` | native |
162
+ | `test(path, value)` | `{ op: 'test', path, value }` — a failing test aborts the WHOLE transition (`JA2004`), which is the format's own way to write a precondition | `PatchOp` | native |
163
+ | a path lambda `(st, x) => …` | the JSON Pointer the state shape describes: `st.todos` → `/todos`, `st.todos.at(2).done` → `/todos/2/done`, `st.get('a/b')` → `/a~1b` (RFC 6901 escaping) | `PatchPath<State, Payload>` — annotate to type it | native |
164
+ | a path lambda with a COMPUTED index | the pointer as a string EXPRESSION, `{ "$concat": ["/todos/", <index>, "/done"] }` — APP-FORMAT §3.2's "op members like `value` and `path` are themselves query expressions" | the same | native; anything that is not a chain of member reads and subscripts `JL0102` |
165
+ | a path as a string | the pointer verbatim, `+ '/-'` under `append()` | `string` | native; a string that does not start with `/` `JL0102` |
166
+
167
+ **`add()` REPLACES an array; `append()` adds to it.** This is the pen's
168
+ single most misread pair and the distinction is RFC 6902's, not the
169
+ pen's: a pointer that NAMES a member replaces that member, so
170
+ `add((st) => st.todos, item)` sets `/todos` to the item and drops the
171
+ list. The append is `add` at the array's `-` position, `/todos/-`, and
172
+ it has its own name here because the difference costs a `validateState`
173
+ rejection at dispatch time to find out otherwise. [§3.1](#31-addpath-value-replaces-an-array-appendpath-value-adds-to-it)
174
+ shows both emitted documents side by side.
175
+
176
+ **The one pointer this pen computes.** Every other path is written as
177
+ pointer TEXT at build time. A non-literal index cannot be — so
178
+ `st.todos.at(x.payload.i).done` emits
179
+ `{ "$concat": ["/todos/", "$payload.i", "/done"] }`, adjacent literal
180
+ segments merged into one operand. (`$string-join` joins a SEQUENCE with
181
+ a separator and casts an array constructor's array to a string —
182
+ `JQ2001`; `$concat` is the operator that concatenates operands, and it
183
+ is what a pointer needs.) The `$concat` node is LIFTED as an operator
184
+ rather than written as data, because a `$`-keyed object in a captured
185
+ tree is otherwise a constructor (the `$map` escape).
186
+
187
+ ### 2.4 The two doors in
188
+
189
+ The two places the outside world reaches an app: an event a view binds,
190
+ and a subscription the host runs.
191
+
192
+ | Method | Emits | Type reading | Status |
193
+ |---|---|---|---|
194
+ | `bind(name, { payload?, event?, preventDefault?, stopPropagation? })` | APP-FORMAT §4's object binding — `{ action, with?, event?, preventDefault?, stopPropagation? }`, in the format's member order | `Binding<Names>` — annotate the call (`bind<Action>('todo/add')`) and an undeclared name stops compiling | native; an empty name, an unknown member or a non-boolean control `JL0101`; a field §3.1 excludes `JL0102` |
195
+ | `sub(run, { with?, when?, withQuery?, key?, for? })` | one APP-FORMAT §5.3 entry, in the format's member order | `SubDeclaration<Run>` | native; an empty `run`, an unknown member or a callback under `with` `JL0101`; a combination §5.3 calls `JA0008` `JL0102`; `$item` outside a `for` `JL0104` |
196
+
197
+ **`with` is data; `withQuery`, `key` and `for` are queries.** That is the
198
+ distinction the format builds its whole restart rule out of, and the pen
199
+ makes it at the door: `with` is verbatim and never evaluated, so a
200
+ static entry never restarts; a callback under it is `JL0101` before the
201
+ runtime would hand a function to a handler as props. A member that is
202
+ sometimes data and sometimes executable is how a document becomes
203
+ accidentally executable.
204
+
205
+ The three combinations §5.3 calls `JA0008` are refused here, naming the
206
+ same reason, because the pen can see all three members at once:
207
+ `with` beside `withQuery`, `with` beside `for`, and `key` without either
208
+ ([§4.3](#43-jl0102--the-state-the-pointer-and-the-binding)).
209
+
210
+ ### 2.5 The `$event` allow-list
211
+
212
+ A binding may request any field name: the built-in allow-list of
213
+ APP-FORMAT §3.1, or a host extractor's own, since an extractor
214
+ registered under a name WINS over the built-in list (§3.1's precedence
215
+ order) and the pen cannot see the host's registry. Requesting a name
216
+ nothing answers is `JA2009` at dispatch — the member is bound `null` and
217
+ the dispatch is NOT dropped — which is the runtime's to report.
218
+
219
+ What the pen refuses is the set §3.1 excludes BY CONSTRUCTION, fourteen
220
+ names: `target`, `currentTarget`, `relatedTarget`, `srcElement`, `view`,
221
+ `files`, `dataTransfer`, `touches`, `targetTouches`, `changedTouches`,
222
+ `path`, `composedPath`, `clipboardData`, `submitter`. The reason is one
223
+ sentence and it is the invariant, not a policy: **`$event` MUST survive
224
+ `JSON.stringify`, the same invariant as state.** A host object never
225
+ enters it. [§4.3](#43-jl0102--the-state-the-pointer-and-the-binding)
226
+ carries the message and the way through.
227
+
228
+ ### 2.6 What the pen does not judge
229
+
230
+ By design, these are the loop's, and the pen emits documents that reach
231
+ them: an unregistered effect or subscription name (`JA2006`, `JA2008`),
232
+ an action or `when` document that throws while evaluating (`JA2002`), a
233
+ patch that fails to apply (`JA2004`), the state invariant (`JA2005`), a
234
+ requested event field nothing answers (`JA2009`), a dynamic
235
+ subscription query that throws (`JA2016`), a fan-out past
236
+ `maxSubInstances` (`JA2017`) and everything else APP-FORMAT §10 lists.
237
+ `test/linq/app-pen.test.js` builds an app through the pen whose only
238
+ effect name is unregistered, boots it under `createApp` and asserts the
239
+ loop reports `JA2006` — the pen's silence there is a tested property,
240
+ not an omission.
241
+
242
+ ## 3. Worked examples
243
+
244
+ Every `js` fence exports exactly one document, and the `json` fence that
245
+ follows is what the pen emits — executed by
246
+ `test/linq/pen-docs.test.js`. APP-FORMAT §2's document and the
247
+ `contract/catalog.load/start` action CONTRACT-FORMAT §11.1 generates are
248
+ rebuilt the same way and held BYTE-equal to those docs' own fences by
249
+ `test/linq/app-pen.test.js`, which also boots every example shape under
250
+ `createApp` with a headless host and dispatches into it. The same suite
251
+ boots an emitted document beside its hand-written twin and compares the
252
+ two applications frame for frame — the pen's document is the same
253
+ application, not merely the same bytes.
254
+
255
+ ### 3.1 `add(path, value)` replaces an array; `append(path, value)` adds to it
256
+
257
+ **The board's tag list**, and the trap everyone meets first.
258
+ Two actions, one array, one document — because the only way to read the
259
+ difference is to see both pointers beside each other:
260
+
261
+ ```js
262
+ import { action, add, append, defineApp, transition } from '@jarenjs/linq/app';
263
+ import { rule } from '@jarenjs/linq/jslt';
264
+ import * as s from '@jarenjs/linq/schema';
265
+
266
+ export const list = defineApp({
267
+ state: s.object({ tags: s.array(s.string()).default([]) }),
268
+ view: [rule('$', (v) => ['ul', {}, v.tags.all()])],
269
+ actions: {
270
+ // RFC 6902 'add' at a path that NAMES the array replaces the array
271
+ 'tags/set': action((st, x) => transition({ patch: [add((c) => c.tags, x.payload)] })),
272
+ // 'add' at the array's '-' position appends one element to it
273
+ 'tags/append': action((st, x) => transition({ patch: [append((c) => c.tags, x.payload)] })),
274
+ },
275
+ }).document;
276
+ ```
277
+ ```json
278
+ {
279
+ "$app": "0.1",
280
+ "state": { "tags": [] },
281
+ "view": [{ "match": "$", "body": ["ul", {}, "$.tags[*]"] }],
282
+ "actions": {
283
+ "tags/set": { "patch": [{ "op": "add", "path": "/tags", "value": "$payload" }] },
284
+ "tags/append": { "patch": [{ "op": "add", "path": "/tags/-", "value": "$payload" }] }
285
+ }
286
+ }
287
+ ```
288
+
289
+ Both operations are `"op": "add"`. The only difference in the emitted
290
+ document is the last two characters of the pointer, `/-`, and that is
291
+ the whole of it:
292
+
293
+ | Dispatched with | `tags/set` leaves | `tags/append` leaves |
294
+ |---|---|---|
295
+ | `'a'` on `[]` | `"a"` — the array is GONE, replaced by a string | `["a"]` |
296
+ | `'c'` on `['a', 'b']` | `"c"` | `["a", "b", "c"]` |
297
+
298
+ `tags/set` is not a bug; it is the operation a reader wants when the
299
+ payload IS the new list (`add((c) => c.tags, x.payload)` where the
300
+ payload is an array replaces the list wholesale). It is a bug when the
301
+ payload is one element, and the state schema is what catches it — the
302
+ next state fails `validateState`, the transition is blocked, and the
303
+ loop reports `JA2005`. Reaching for `append()` is how a reader never
304
+ sees that.
305
+
306
+ `replace()` and `add()` differ once more, at an array ELEMENT:
307
+ `add((c) => c.tags.at(1), x)` INSERTS at index 1 and shifts the rest,
308
+ where `replace((c) => c.tags.at(1), x)` overwrites it. On an object
309
+ member the two are the same operation.
310
+
311
+ ### 3.2 An app with state, one action and a view
312
+
313
+ **The board's estimate counter**, and the smallest app that is a whole
314
+ one. The whole shape in one document: a state whose defaults ARE the initial
315
+ value, a view that reads it and binds an action, and an action that
316
+ patches it.
317
+
318
+ ```js
319
+ import { action, bind, defineApp, replace, transition } from '@jarenjs/linq/app';
320
+ import { rule } from '@jarenjs/linq/jslt';
321
+ import * as s from '@jarenjs/linq/schema';
322
+
323
+ export const counter = defineApp({
324
+ state: s.object({ total: s.integer().default(0), step: s.integer().default(1) }),
325
+ view: [rule('$', (v) => ['main', {},
326
+ ['output', {}, v.total],
327
+ ['button', { on: { click: bind('inc') } }, 'add'],
328
+ ])],
329
+ actions: {
330
+ inc: action((st) => transition({ patch: [replace((c) => c.total, st.total.add(st.step))] })),
331
+ },
332
+ }).document;
333
+ ```
334
+ ```json
335
+ {
336
+ "$app": "0.1",
337
+ "state": { "total": 0, "step": 1 },
338
+ "view": [
339
+ { "match": "$", "body": ["main", {},
340
+ ["output", {}, "$.total"],
341
+ ["button", { "on": { "click": { "action": "inc" } } }, "add"]] }
342
+ ],
343
+ "actions": {
344
+ "inc": { "patch": [{ "op": "replace", "path": "/total", "value": { "$add": ["$.total", "$.step"] } }] }
345
+ }
346
+ }
347
+ ```
348
+
349
+ Three readings in one document. `state` is `{ total: 0, step: 1 }`
350
+ because both members declare a `default()` — nothing else wrote it.
351
+ `v.total` in the view body captured as `"$.total"`, an ordinary JSLT
352
+ body over the state. And `st.total.add(st.step)` captured as
353
+ `{ "$add": ["$.total", "$.step"] }` — the action reads the whole state,
354
+ so a patch value may name any member, not just the one it writes.
355
+
356
+ `bind('inc')` emitted `{ "action": "inc" }` and nothing more:
357
+ `defineApp()` checked that name against `actions` before the document
358
+ existed ([§4.3](#43-jl0102--the-state-the-pointer-and-the-binding)).
359
+
360
+ ### 3.3 A patch chain over a nested path
361
+
362
+ **Promoting a card between the board's columns.**
363
+ Every one of the seven operations, the pointer each writes, and the two
364
+ places a pointer is not plain text:
365
+
366
+ ```js
367
+ import { action, copy, move, remove, replace, test, transition } from '@jarenjs/linq/app';
368
+
369
+ export const promote = action((st, x) => transition({
370
+ patch: [
371
+ test((c) => c.board.columns.at(0).cards.at(2).id, x.payload.id),
372
+ copy((c) => c.board.columns.at(0).cards.at(2), (c) => c.board.columns.at(1).cards.at(0)),
373
+ remove((c) => c.board.columns.at(0).cards.at(2)),
374
+ move((c) => c.board.get('draft/note'), (c) => c.board.get('a~b')),
375
+ replace((c, y) => c.board.columns.at(y.payload.to).title, 'Done'),
376
+ ],
377
+ })).document;
378
+ ```
379
+ ```json
380
+ {
381
+ "patch": [
382
+ { "op": "test", "path": "/board/columns/0/cards/2/id", "value": "$payload.id" },
383
+ { "op": "copy", "from": "/board/columns/0/cards/2", "path": "/board/columns/1/cards/0" },
384
+ { "op": "remove", "path": "/board/columns/0/cards/2" },
385
+ { "op": "move", "from": "/board/draft~1note", "path": "/board/a~0b" },
386
+ { "op": "replace",
387
+ "path": { "$concat": ["/board/columns/", "$payload.to", "/title"] },
388
+ "value": "Done" }
389
+ ]
390
+ }
391
+ ```
392
+
393
+ What to read off it:
394
+
395
+ - **A literal subscript is pointer text.** `at(0)`, `at(2)` became `/0`,
396
+ `/2` at build time; no expression survives into the document.
397
+ - **`get(name)` is how a member whose name is not an identifier is
398
+ reached**, and RFC 6901 escaping is the pen's: `draft/note` →
399
+ `draft~1note`, `a~b` → `a~0b`. It is also the escape for a member
400
+ named like a chain method ([§5.3](#53-a-member-named-like-a-chain-method-captures-as-the-method)).
401
+ - **`move()` and `copy()` lower BOTH pointers.** `from` is written
402
+ before `path`, which is RFC 6902's member order.
403
+ - **A computed subscript is the one expression a path carries**, and it
404
+ is a `$concat` over the literal segments and the index — evaluated at
405
+ dispatch, against the pre-transition state, like every other
406
+ expression in the action.
407
+ - **`test()` aborts the whole transition**, not its own operation. The
408
+ patch above is a precondition followed by four writes: if card 2 is
409
+ not the card the payload names, nothing at all happens and the loop
410
+ reports `JA2004`.
411
+
412
+ ### 3.4 An action reading `$event`
413
+
414
+ **The board's filter box**, where the action reads the keystroke.
415
+ `$event` is the serializable slice of a DOM event: the default four
416
+ members (`type`, `value`, `checked`, `key`) plus one per field the
417
+ BINDING requested. The `action()`'s own `event` option requests nothing
418
+ at run time — it types `x.event` — so the two lists are declared in both
419
+ places when both are wanted.
420
+
421
+ ```js
422
+ import { action, bind, defineApp, replace, transition } from '@jarenjs/linq/app';
423
+ import { rule } from '@jarenjs/linq/jslt';
424
+ import * as s from '@jarenjs/linq/schema';
425
+
426
+ export const search = defineApp({
427
+ state: s.object({
428
+ term: s.string().default(''),
429
+ exact: s.boolean().default(false),
430
+ lastKey: s.string().default(''),
431
+ }),
432
+ view: [rule('$', (v) => ['input', {
433
+ value: v.term,
434
+ on: {
435
+ input: bind('term/typed', { event: ['shiftKey'] }),
436
+ keydown: bind('term/key', { event: ['code'], preventDefault: true }),
437
+ },
438
+ }])],
439
+ actions: {
440
+ 'term/typed': action((st, x) => transition({
441
+ patch: [
442
+ replace((c) => c.term, x.event.value),
443
+ replace((c) => c.exact, x.event.shiftKey),
444
+ ],
445
+ }), { event: ['shiftKey'] }),
446
+ 'term/key': action((st, x) => transition({
447
+ patch: [replace((c) => c.lastKey, x.event.code)],
448
+ }), { event: ['code'] }),
449
+ },
450
+ }).document;
451
+ ```
452
+ ```json
453
+ {
454
+ "$app": "0.1",
455
+ "state": { "term": "", "exact": false, "lastKey": "" },
456
+ "view": [
457
+ { "match": "$", "body": ["input", {
458
+ "value": "$.term",
459
+ "on": {
460
+ "input": { "action": "term/typed", "event": ["shiftKey"] },
461
+ "keydown": { "action": "term/key", "event": ["code"], "preventDefault": true }
462
+ } }] }
463
+ ],
464
+ "actions": {
465
+ "term/typed": { "patch": [
466
+ { "op": "replace", "path": "/term", "value": "$event.value" },
467
+ { "op": "replace", "path": "/exact", "value": "$event.shiftKey" }] },
468
+ "term/key": { "patch": [
469
+ { "op": "replace", "path": "/lastKey", "value": "$event.code" }] }
470
+ }
471
+ }
472
+ ```
473
+
474
+ `x.event.value` is the default slice's member and needed no request;
475
+ `x.event.shiftKey` and `x.event.code` are the built-in allow-list's, and
476
+ each binding asked for the one its action reads. `preventDefault` rides
477
+ on the binding, not on the action, because it is the DISPATCH's
478
+ behaviour and the binding is where the format puts it.
479
+
480
+ What this document cannot ask for is `event.target` — see
481
+ [§2.5](#25-the-event-allow-list) and
482
+ [§4.3](#43-jl0102--the-state-the-pointer-and-the-binding).
483
+
484
+ ### 3.5 An action reading `$payload`, with an effect in the same scope
485
+
486
+ **Adding a card**, where the patch and the effect read the same payload.
487
+
488
+ ```js
489
+ import { action, append, defineApp, effect, replace, transition } from '@jarenjs/linq/app';
490
+ import { rule } from '@jarenjs/linq/jslt';
491
+ import * as s from '@jarenjs/linq/schema';
492
+
493
+ const Card = s.object({ id: s.string(), text: s.string(), done: s.boolean().default(false) });
494
+
495
+ export const cards = defineApp({
496
+ state: s.object({ cards: s.array(Card).default([]), pending: s.integer().default(0) }),
497
+ view: [rule('$', (v) => ['ul', {}, v.cards.all().text])],
498
+ actions: {
499
+ 'card/add': action((st, x) => transition({
500
+ patch: [
501
+ append((c) => c.cards, { id: x.payload.id, text: x.payload.text, done: false }),
502
+ replace((c) => c.pending, st.pending.add(1)),
503
+ ],
504
+ effects: [effect('persist', { id: x.payload.id, at: st.pending.add(1) })],
505
+ }), { payload: s.object({ id: s.string(), text: s.string() }) }),
506
+ },
507
+ }).document;
508
+ ```
509
+ ```json
510
+ {
511
+ "$app": "0.1",
512
+ "state": { "cards": [], "pending": 0 },
513
+ "view": [{ "match": "$", "body": ["ul", {}, "$.cards[*].text"] }],
514
+ "actions": {
515
+ "card/add": {
516
+ "patch": [
517
+ { "op": "add", "path": "/cards/-",
518
+ "value": { "id": "$payload.id", "text": "$payload.text", "done": false } },
519
+ { "op": "replace", "path": "/pending", "value": { "$add": ["$.pending", 1] } }
520
+ ],
521
+ "effects": [
522
+ { "run": "persist",
523
+ "with": { "id": "$payload.id", "at": { "$add": ["$.pending", 1] } } }
524
+ ]
525
+ }
526
+ }
527
+ }
528
+ ```
529
+
530
+ The two `{ "$add": ["$.pending", 1] }` nodes are the point. They are the
531
+ same expression written twice in the source and captured twice into the
532
+ document, and at dispatch they evaluate to the same number — because
533
+ both evaluate against the PRE-transition state, whatever order the
534
+ runtime applies the transition in. An effect that needs the value the
535
+ patch just wrote computes it, rather than reading it back.
536
+
537
+ `{ payload: s.object({ … }) }` emitted nothing. It types `x.payload`
538
+ (§5) and the format carries no schema for a payload, so declaring one
539
+ costs a document nothing.
540
+
541
+ ### 3.6 A subscription with `for` and `$item`
542
+
543
+ **The board's live columns.** A static entry and a dynamic fan-out in one document:
544
+
545
+ ```js
546
+ import { defineApp, sub } from '@jarenjs/linq/app';
547
+ import { rule } from '@jarenjs/linq/jslt';
548
+ import * as s from '@jarenjs/linq/schema';
549
+
550
+ export const chat = defineApp({
551
+ state: s.object({
552
+ online: s.boolean().default(false),
553
+ columns: s.array(s.object({ id: s.string(), unread: s.integer().default(0) })).default([]),
554
+ }),
555
+ view: [rule('$', (v) => ['ul', {}, v.columns.all().id])],
556
+ subs: [
557
+ // static: verbatim props, never restarts
558
+ sub('clock', { with: { ms: 1000, tick: 'clock/tick' } }),
559
+ // dynamic fan-out: one instance per column, keyed by the column's id
560
+ sub('column', {
561
+ when: (st) => st.online,
562
+ for: (st) => st.columns.all(),
563
+ withQuery: (st, x) => ({ id: x.item.id, since: st.columns.all().unread.sum() }),
564
+ key: (st, x) => x.item.id,
565
+ }),
566
+ ],
567
+ }).document;
568
+ ```
569
+ ```json
570
+ {
571
+ "$app": "0.1",
572
+ "state": { "online": false, "columns": [] },
573
+ "view": [{ "match": "$", "body": ["ul", {}, "$.columns[*].id"] }],
574
+ "subs": [
575
+ { "run": "clock", "with": { "ms": 1000, "tick": "clock/tick" } },
576
+ { "run": "column",
577
+ "when": "$.online",
578
+ "withQuery": { "id": "$item.id", "since": { "$sum": "$.columns[*].unread" } },
579
+ "key": "$item.id",
580
+ "for": "$.columns[*]" }
581
+ ]
582
+ }
583
+ ```
584
+
585
+ The members come out in APP-FORMAT §5.3's order — `run`, `with`, `when`,
586
+ `withQuery`, `key`, `for` — whatever order the author wrote them in.
587
+
588
+ `x.item` is bound in `withQuery` and `key`, and ONLY under a `for`: the
589
+ scope the pen captures against is exactly §5.3's closed world, so `when`
590
+ and `for` see the state alone and `$item` there is `JL0104` at build
591
+ time rather than `JA0008` at `createApp` time
592
+ ([§4.4](#44-jl0104--the-closed-worlds)). `withQuery` may read both, as
593
+ `since` does: `$item.id` beside a query over the whole state.
594
+
595
+ The two entries also show the restart rule from the document's side. The
596
+ `clock` entry has `with` and no query, so it never restarts. The `column`
597
+ entry restarts one instance whenever its `key` changes, and
598
+ `stableStringify(item)` would have been the key had none been declared —
599
+ `key` is the opt-out from recomputing a deep key per transaction.
600
+
601
+ ### 3.7 A view binding an action, and the refusal when it is undeclared
602
+
603
+ **The board's grid**, and what a view may bind.
604
+ APP-FORMAT §4 gives a binding two forms and the pen reads both: a string
605
+ under an `on` map, and an object carrying an `action` member ANYWHERE,
606
+ because that member is §4's vocabulary and a widget's `emit` will find
607
+ one wherever it sits.
608
+
609
+ ```js
610
+ import { action, bind, defineApp, remove, transition } from '@jarenjs/linq/app';
611
+ import { rule } from '@jarenjs/linq/jslt';
612
+ import * as s from '@jarenjs/linq/schema';
613
+
614
+ export const table = defineApp({
615
+ state: s.object({ rows: s.array(s.object({ id: s.string() })).default([]), mode: s.string().default('view') }),
616
+ view: [rule('$', (v) => ['table', {},
617
+ // a literal name: the pen checks it against `actions`
618
+ ['button', { on: { click: bind('row/clear', { stopPropagation: true }) } }, 'clear'],
619
+ // an object binding anywhere is a binding — a widget's `emit` finds it here
620
+ ['x-grid', { rowAction: bind('row/drop', { payload: { id: v.rows.all().id } }) }],
621
+ // a `$`-prefixed value names the action at RENDER time; no pen can resolve it
622
+ ['button', { on: { click: v.mode } }, 'go'],
623
+ ])],
624
+ actions: {
625
+ 'row/clear': action(() => transition({ patch: [remove('/rows')] })),
626
+ 'row/drop': action((st, x) => transition({ patch: [remove((c, y) => c.rows.at(y.payload.id))] })),
627
+ },
628
+ }).document;
629
+ ```
630
+ ```json
631
+ {
632
+ "$app": "0.1",
633
+ "state": { "rows": [], "mode": "view" },
634
+ "view": [
635
+ { "match": "$", "body": ["table", {},
636
+ ["button", { "on": { "click": { "action": "row/clear", "stopPropagation": true } } }, "clear"],
637
+ ["x-grid", { "rowAction": { "action": "row/drop", "with": { "id": "$.rows[*].id" } } }],
638
+ ["button", { "on": { "click": "$.mode" } }, "go"]] }
639
+ ],
640
+ "actions": {
641
+ "row/clear": { "patch": [{ "op": "remove", "path": "/rows" }] },
642
+ "row/drop": { "patch": [{ "op": "remove",
643
+ "path": { "$concat": ["/rows/", "$payload.id"] } }] }
644
+ }
645
+ }
646
+ ```
647
+
648
+ Rename `row/drop` to `row/dropped` in `actions`, leave the view alone,
649
+ and the pen refuses the document before it exists — naming what IS
650
+ declared, so a typo is one glance from fixed:
651
+
652
+ ```text
653
+ JL0102: the view binds the action 'row/drop', which "actions" does not
654
+ declare — the runtime drops such a dispatch and reports JA2001
655
+ (APP-FORMAT §4), so the user's click does nothing; the declared actions
656
+ are 'row/clear', 'row/dropped' at /view
657
+ ```
658
+
659
+ The third binding is why the check has a hole in it, honestly: a value
660
+ starting with `$` is a query expression naming the action at render
661
+ time, and no pen can resolve it — the state decides which action a
662
+ render-time binding dispatches. Those the pen skips, and the runtime
663
+ checks them per dispatch as it always did.
664
+
665
+ ## 4. Refusals
666
+
667
+ The app pen raises these `LinqBuildError` codes and no others —
668
+ `test/linq/pen-docs.test.js` holds this list equal, in both directions,
669
+ to the codes `packages/linq/src/app/` throws. The full condition each
670
+ code states across every pen is the binder's,
671
+ [LINQ-FORMAT.md](LINQ-FORMAT.md) §1.3.
672
+
673
+ | Code | What this pen raises it for |
674
+ |---|---|
675
+ | `JL0101` | a value this pen cannot spell, or a name → value map it cannot read |
676
+ | `JL0102` | a construct the format cannot carry |
677
+ | `JL0104` | a pen-owned keyword written through `meta()`, or an external a captured rule did not declare |
678
+
679
+ Every message below is the one the pen raised when the spelling beside
680
+ it was run, with the code prefix removed. `docPath`, where the refusal
681
+ carries one, is the JSON pointer of the node being assembled and is
682
+ appended to the message text as well (`… at /actions/a`).
683
+
684
+ ### 4.1 `JL0101` — the document's own members
685
+
686
+ Raised at the door, before anything is captured.
687
+
688
+ | The spelling that trips it | The message | The spelling that works |
689
+ |---|---|---|
690
+ | `defineApp('$')` | `defineApp() takes { state, initial?, schema?, view, actions?, subs? }, got a string` | a plain object |
691
+ | `defineApp({ state: {}, nope: 1, view: [] })` | `defineApp() does not take 'nope' — it takes state, initial, schema, view, actions, subs` | one of the six |
692
+ | `defineApp({ state: {} })` | `defineApp() needs a view — a JSLT stylesheet, from stylesheet([rule(…)]) or as a bare rule array; the format requires the member and the runtime refuses an app without one (JA0002)` | `view: [rule('$', …)]` |
693
+ | `defineApp({ state: {}, view: 'x' })` | `defineApp() view is a JSLT stylesheet document or a bare rule array, got a string` | an array, or a `stylesheet()` envelope |
694
+ | `defineApp({ state: s.object({}), schema: s.object({}), view: [] })` | `defineApp() takes 'schema' beside a state given as a plain JSON value — a state given as a builder IS its schema` | drop the `schema` |
695
+ | `defineApp({ state: {}, schema: {}, view: [] })` | `defineApp() schema is a schema-pen builder, got a Object instance` | `s.object({ … })` |
696
+ | `defineApp({ …, actions: [] })` | `defineApp() actions is an object of named action() declarations, got a Array instance` | a plain object |
697
+ | `defineApp({ …, actions: { a: { patch: [] } } })` | `defineApp() action 'a' is action((s, x) => transition(…)), got a Object instance` | `action(fn)` |
698
+ | `defineApp({ …, subs: 1 })`, `subs: [{ run: 'x' }]` | `defineApp() subs is an array of sub() declarations, got 1`; `defineApp() subs[0] is sub(run, options?), got a Object instance` | `sub('x')` |
699
+ | `defineApp({ state: () => 1, view: [] })`, the same under `initial` | `defineApp() state received a function, which is not JSON — a document carries null, booleans, finite numbers (never -0), strings, arrays and plain objects, and nothing else` | a JSON value, or a builder |
700
+
701
+ ### 4.2 `JL0101` — the transition, the binding and the subscription
702
+
703
+ | The spelling that trips it | The message | The spelling that works |
704
+ |---|---|---|
705
+ | `action(7)` | `action() takes a callback (s, x) => transition(…), got 7` | a callback |
706
+ | `action(fn, { nope: 1 })` | `action() does not take 'nope' — it takes payload, event` | `payload`, `event` |
707
+ | `action(fn, { payload: {} })` | `action() payload is a schema-pen builder — it types the dispatch's $payload, and the app format carries no schema for it, so nothing is emitted for it; got a Object instance` | `payload: s.object({ … })` |
708
+ | `action(fn, { event: 'x' })`, `{ event: [1] }` | `action() event is an array of field names (APP-FORMAT §4), got a string`; `action() event[0] is a field name, got 1` | `event: ['shiftKey']` |
709
+ | `transition(7)`, `transition({ nope: 1 })` | `transition() takes { state?, patch?, effects? }, got 7`; `transition() does not take 'nope' — it takes state, patch, effects` | one of the three |
710
+ | `transition({ patch: 'x' })`, `{ patch: [1] }` | `transition() patch is an array of add/replace/remove/move/copy/test operations, got a string`; `transition() patch[0] is one of add/replace/remove/move/copy/test, got 1` | an array of operations |
711
+ | `transition({ effects: [{ run: 'x' }] })` | `transition() effects[0] is effect(run, with?), got a Object instance` | `effect('x')` |
712
+ | `effect('')`, `bind('')`, `sub('')` | `effect() takes the handler name as a non-empty string, got a string`; `bind() takes an action name as a non-empty string, …`; `sub() takes the handler name as a non-empty string, …` | a non-empty name |
713
+ | `bind('a', { nope: 1 })` | `bind() does not take 'nope' — it takes payload, event, preventDefault, stopPropagation` | one of the four |
714
+ | `bind('a', { preventDefault: 'yes' })` | `bind() preventDefault is a boolean — it is allowed only on the object binding form and defaults to false (APP-FORMAT §4), got a string` | `preventDefault: true` |
715
+ | `sub('a', { nope: 1 })` | `sub() does not take 'nope' — it takes with, when, withQuery, key, for (APP-FORMAT §5.3)` | one of the five |
716
+ | `sub('a', { with: (st) => st.ms })` | `sub() 'with' is VERBATIM data handed to the handler, never evaluated — for props derived from the state write 'withQuery' instead (APP-FORMAT §5.3)` | `withQuery: (st) => …` |
717
+ | `replace(7, 1)` | `a patch path is a lambda over the state — (st) => st.todos — or a JSON Pointer string, got 7` | a lambda, or a pointer |
718
+
719
+ **The `__proto__` case.** `actions` and the state's `properties` are both
720
+ name → value maps, so the binder's §1.1 rule 5 applies to them:
721
+
722
+ ```js
723
+ defineApp({ state: {}, view: [], actions: { __proto__: action(fn) } })
724
+ // JL0101: defineApp() actions received a map whose prototype was
725
+ // replaced: a '__proto__:' key in an object literal sets the prototype
726
+ // instead of adding a member, so that member is not there to emit —
727
+ // spell it { ['__proto__']: … }, which is an own key at /actions
728
+ ```
729
+
730
+ The computed key IS an own property, and then `__proto__` is ORDINARY
731
+ DATA all the way through this pen — which the app pen is where the
732
+ campaign proves end to end, because it is the one pen whose document
733
+ carries a name map on both sides at once:
734
+
735
+ ```js
736
+ defineApp({
737
+ state: s.object({ ['__proto__']: s.integer().default(7), n: s.integer().default(0) }),
738
+ view: [rule('$', () => ['p', {}, 'x'])],
739
+ actions: { ['__proto__']: action(() => transition({})), ok: action(() => transition({})) },
740
+ }).document
741
+ // { "$app": "0.1", "state": { "__proto__": 7, "n": 0 }, "view": [ … ],
742
+ // "actions": { "__proto__": {}, "ok": {} } }
743
+ ```
744
+
745
+ An action named `__proto__` is a declared action; a state member named
746
+ `__proto__` gets its default into the initial state like any other. Both
747
+ survive because the pen writes them with `setObjectMember` — a plain
748
+ `out[name] = value` would reassign the emitted object's prototype and
749
+ drop the member. `test/linq/pen-docs.test.js` asserts the emitted
750
+ `actions` and `state` still carry `Object.prototype`, still hold
751
+ `__proto__` as an OWN member, and that `state['__proto__']` is `7`.
752
+
753
+ ### 4.3 `JL0102` — the state, the pointer and the binding
754
+
755
+ Four conditions, and a reader meets three of them in their first
756
+ afternoon.
757
+
758
+ | The spelling that trips it | The message | The spelling that works |
759
+ |---|---|---|
760
+ | `defineApp({ state: s.object({ id: s.string() }), view: [] })` | `the initial state cannot be derived: '/id' is required and declares no default — give the member a default(), or pass the whole initial state as defineApp()'s 'initial'` | `s.string().default('')`, or `initial: { id: '' }` |
761
+ | `replace((st) => st.todos.all().count(), 1)` | `a patch path is a JSON Pointer, and this one cannot be written as one: {"$count":"$.todos[*]"} — a path lambda reads members and subscripts off the state (st.todos.at(2).done, st.todos.at(x.payload.i)); anything else, pass the pointer as a string` | `st.todos.at(i)`, or a pointer string |
762
+ | `replace('a/b', 1)` | `a patch path given as a string is an RFC 6901 JSON Pointer — 'a/b' does not start with '/'; write a lambda over the state instead, (st) => st.todos` | `'/a/b'` |
763
+ | `bind('a', { event: ['target'] })`, `action(fn, { event: ['files'] })` | `an event binding cannot request 'target' — APP-FORMAT §3.1 excludes target, files, touch lists and every other host-object-valued field by construction, because $event MUST survive JSON.stringify, the same invariant as state. Register an extractor under a name of its own (§5.4's worked example maps event.target.files to opaque string tokens under 'fileTokens') and request that.` | register an extractor, request its name |
764
+ | a view binding an action `actions` does not declare | `the view binds the action 'nope', which "actions" does not declare — the runtime drops such a dispatch and reports JA2001 (APP-FORMAT §4), so the user's click does nothing; the declared actions are 'real'` | declare it, or fix the spelling |
765
+ | `sub('r', { with: 1, withQuery: fn })` | `sub() carries both 'with' and 'withQuery' — one entry, one props source: 'with' is verbatim data and never restarts, 'withQuery' derives the props from the state and makes the entry dynamic (APP-FORMAT §5.3, the runtime's JA0008)` | one of the two |
766
+ | `sub('r', { with: 1, for: fn })` | `sub() carries 'with' beside 'for' — a fan-out's props come from 'withQuery', or default to the item itself (APP-FORMAT §5.3, the runtime's JA0008)` | drop `with`, or drop `for` |
767
+ | `sub('r', { key: fn })` | `sub() carries 'key' without 'withQuery' or 'for' — a static subscription never restarts, so it has no restart key (APP-FORMAT §5.3, the runtime's JA0008)` | add `withQuery` or `for` |
768
+
769
+ **A patch path that is not a chain of member reads and subscripts.** No
770
+ JSON Pointer can be written for one, and the pen will not guess where an
771
+ arbitrary expression lands in the state. The line between the two is
772
+ exactly this: a chain of `.member` reads, `get(name)` reads and `at(i)`
773
+ subscripts lowers — `at()` with a COMPUTED index included, which is the
774
+ `$concat` of [§2.3](#23-the-seven-patch-operations) and works. Anything
775
+ that calls an OPERATOR does not: `all()`, `count()`, `filter()`,
776
+ arithmetic. The two spellings side by side:
777
+
778
+ ```js
779
+ replace((c, y) => c.todos.at(y.payload.i).done, true) // { "$concat": ["/todos/", "$payload.i", "/done"] }
780
+ replace((c) => c.todos.all().count(), 1) // JL0102 — no pointer names a count
781
+ ```
782
+
783
+ Where a location genuinely cannot be reached from the state shape, pass
784
+ the pointer as a string; that door is always open, and the only check on
785
+ it is that it starts with `/`.
786
+
787
+ **An `$event` field APP-FORMAT §3.1 excludes by construction.** A reader
788
+ will reach for `event.target` on their first afternoon, and the reason
789
+ they cannot have it is not a policy: **`$event` MUST survive
790
+ `JSON.stringify`.** A DOM node in the event slice would break the
791
+ invariant state itself keeps — it could not be serialized, replayed,
792
+ sent to a devtool or compared between transactions — so §3.1 excludes
793
+ `target` and thirteen siblings BY CONSTRUCTION, not by omission, and the
794
+ pen refuses the request rather than emitting a document the loop would
795
+ bind `null` for. The way through is §5.4's own worked example: a host
796
+ registers an extractor under a name of its own, which maps the host
797
+ object to a JSON value at the boundary, and the binding requests THAT
798
+ name.
799
+
800
+ ```js
801
+ bind('pickFiles', { event: ['files'] }) // JL0102
802
+ bind('pickFiles', { event: ['fileTokens'] }) // fine — the host's extractor answers it
803
+ ```
804
+
805
+ An extractor registered under one of the fourteen excluded names would
806
+ shadow the exclusion the format exists to state, so the pen refuses
807
+ those names whatever the registry holds; every OTHER name is allowed,
808
+ because an extractor wins over the built-in allow-list (§3.1's
809
+ precedence order) and the pen cannot see the host's registry.
810
+
811
+ **An initial state no `default()` describes.** The rule §1.2 states is
812
+ what makes this a refusal rather than a shrug: the state a default
813
+ describes IS the state the document carries, so a required member with
814
+ no default leaves the document with no value for it, and the app fails
815
+ its own `validateState` on the boot transaction — `JA0007`, fatal,
816
+ after everything acquired is rolled back. The pen sees it at build time
817
+ and names the pointer. An OPTIONAL member with no default is fine and
818
+ simply does not appear.
819
+
820
+ **A view binding an action `actions` does not declare.** APP-FORMAT §4
821
+ leaves an unknown name to run time — `JA2001` per dispatch, and the
822
+ user's click does nothing at all, silently. `defineApp()` sees the whole
823
+ document at once and refuses it, naming the declared alternatives so a
824
+ typo is one glance from fixed. [§3.7](#37-a-view-binding-an-action-and-the-refusal-when-it-is-undeclared)
825
+ shows both binding forms it reads and the one it cannot.
826
+
827
+ ### 4.4 `JL0104` — the closed worlds
828
+
829
+ | The spelling that trips it | The message | The spelling that works |
830
+ |---|---|---|
831
+ | `action((st, x) => transition({ state: x.clock }))` | `an action() rule cannot bind 'clock' — its query evaluates with exactly 2 externals, 'event' and 'payload'; anything else has nothing to bind to — the whole state is the first argument; §3.1 binds $event and $payload and nothing else, and a host value reaches an action through a binding's payload or an event-field extractor` | `x.event`, `x.payload`, or the state |
832
+ | `sub('r', { withQuery: (st, x) => x.item })` | `a sub() withQuery rule cannot bind 'item' — its query evaluates with no externals at all; anything else has nothing to bind to — a subscription's withQuery is compiled closed-world against the state alone; $item exists only under a "for" declaration (APP-FORMAT §5.3)` | add `for`, or read the state |
833
+ | `sub('r', { when: (st, x) => x.item, for: fn })` | `a sub() when rule cannot bind 'item' — … — a subscription's when is compiled closed-world against the state alone; $item exists only under a "for" declaration (APP-FORMAT §5.3)` | `when` sees the state alone |
834
+
835
+ Three scopes, and the pen mirrors each exactly:
836
+
837
+ | Where | `$` | Externals |
838
+ |---|---|---|
839
+ | an action, and a patch path inside it | the whole state | `$event`, `$payload` |
840
+ | a subscription's `when` and `for` | the whole state | none |
841
+ | a subscription's `withQuery` and `key` | the whole state | `$item`, and only under a `for` |
842
+
843
+ The mirror is the point. A document the pen writes cannot be the
844
+ `JA0008` a free variable would be at `createApp` time, because the pen
845
+ raised `JL0104` at the line that wrote it — where the fix has a name and
846
+ a stack — rather than letting an unbound external reach the loop's
847
+ compiler.
848
+
849
+ Two of the three messages are the shared capture's, so they open by
850
+ counting the EXTERNALS and then name the whole vocabulary in the advice
851
+ the pen appends. "Exactly 2 externals" for an action means `$event` and
852
+ `$payload`; the state is the first argument and is never an external.
853
+
854
+ The app pen writes no keyword of its own onto a schema, so the other
855
+ half of `JL0104` — a pen-owned keyword written through `meta()` — is
856
+ reachable here only through the schema pen that types the state
857
+ ([SCHEMA-PEN.md §4.4](SCHEMA-PEN.md#44-jl0104--the-keyword-and-the-external)).
858
+
859
+ ## 5. The types
860
+
861
+ ```ts
862
+ import { createApp } from '@jarenjs/app';
863
+ import { JarenValidator } from '@jarenjs/validate';
864
+ import { action, append, bind, defineApp, effect, replace, sub, transition } from '@jarenjs/linq/app';
865
+ import type {
866
+ ActionScope, ActionsOf, AppDocument, AppResult, Binding, EffectDeclaration,
867
+ EventSlice, FanScope, PatchOp, PatchPath, StateOf, SubDeclaration, Transition,
868
+ } from '@jarenjs/linq/app';
869
+ import { rule } from '@jarenjs/linq/jslt';
870
+ import * as s from '@jarenjs/linq/schema';
871
+ import type { Expr } from '@jarenjs/linq';
872
+ import type { Infer } from '@jarenjs/linq/schema';
873
+
874
+ const State = s.object({
875
+ todos: s.array(s.object({ text: s.string(), done: s.boolean().default(false) })).default([]),
876
+ draft: s.string().default(''),
877
+ });
878
+ type State = Infer<typeof State>;
879
+ type Action = 'todo/add' | 'todo/toggleAt';
880
+
881
+ const Payload = s.object({ text: s.string() });
882
+
883
+ const todo = defineApp({
884
+ state: State,
885
+ view: [rule('$', (v: Expr<State>) => ['main', {},
886
+ ['button', { on: { click: bind<Action>('todo/add', { payload: { text: v.draft } }) } }, 'add'],
887
+ ])],
888
+ actions: {
889
+ // the state is typed by ANNOTATION; the payload by its own declaration
890
+ 'todo/add': action((st: Expr<State>, x) => transition({
891
+ patch: [append((c: Expr<State>) => c.todos, { text: x.payload.text, done: false })],
892
+ }), { payload: Payload }),
893
+ 'todo/toggleAt': action((st: Expr<State>, x) => transition({
894
+ patch: [replace((c: Expr<State>, y: ActionScope<{ i: number }>) =>
895
+ c.todos.at(y.payload.i).done, true)],
896
+ })),
897
+ },
898
+ subs: [sub('interval', { with: { ms: 1000 }, when: (st: Expr<State>) => st.todos.all().count().gt(0) })],
899
+ });
900
+
901
+ type Shape = StateOf<typeof todo>; // the state the document describes
902
+ type Names = ActionsOf<typeof todo>; // 'todo/add' | 'todo/toggleAt'
903
+
904
+ const { document, stateSchema } = todo;
905
+ // no narrow and no cast: the `state: <builder>` overload can never
906
+ // answer null, and AppResult carries that in its third phantom (§5.4)
907
+ createApp(document, { node, validateState: new JarenValidator().compile(stateSchema) });
908
+
909
+ bind<Names>('todo/add'); // fine
910
+ bind<Names>('nope'); // does not compile — and JL0102 at defineApp()
911
+ ```
912
+
913
+ ### 5.1 The two phantoms, and what reads them
914
+
915
+ `AppDocument<State, Actions>` carries two phantoms: the state SHAPE, and
916
+ the declared action NAMES as a literal union. Both are read off the
917
+ declarations themselves — `defineApp`'s `A extends Record<string,
918
+ ActionDeclaration<any>>` is `const`, so the keys stay literal — and both
919
+ come back out:
920
+
921
+ | Type | Reads | What it is for |
922
+ |---|---|---|
923
+ | `StateOf<A>` | the state shape | what a host reading `app.getState()` needs |
924
+ | `ActionsOf<A>` | the literal action names | what a `bind<Names>()` is checked against |
925
+
926
+ `AppResult<State, Actions>` is the pair `defineApp()` answers,
927
+ `{ document, stateSchema }`. Both `StateOf<>` and `ActionsOf<>` accept
928
+ either the result or the document, so a consumer that stores only the
929
+ document loses nothing.
930
+
931
+ ### 5.2 The honest limits, both of them TypeScript's own
932
+
933
+ A function's type arguments are all-or-none, and a sibling member's
934
+ inferred type cannot contextually type a callback beside it. Everything
935
+ awkward about this pen's types follows from those two:
936
+
937
+ - **An action's state is typed by ANNOTATION** — `action((s: Expr<State>,
938
+ x) => …)`, and a patch path the same — because `action()` is evaluated
939
+ before `defineApp()` sees the `state` builder. What `defineApp({ state
940
+ })` types is the DOCUMENT (`StateOf<>`). `x.payload` needs NO
941
+ annotation: `action(fn, { payload })` declares it on the same call, and
942
+ `{ event: [...] as const }` adds the requested members to `x.event`
943
+ beside APP-FORMAT §3.1's default slice — which is what `EventSlice<Fields>`
944
+ spells, four known members plus one `unknown` per requested field.
945
+ - **`bind()`'s action name is checked by annotating the call** —
946
+ `bind<Action>('todo/add')`, with `Action` declared or read back through
947
+ `ActionsOf<>`. `defineApp()` checks the other direction at run time
948
+ over the whole view, which is the half a type cannot reach: by then a
949
+ view is a compiled stylesheet.
950
+ - **A view body's value is the honest top until it is annotated**
951
+ (`rule('$', (v: Expr<State>) => …)`), exactly as the JSLT pen's body
952
+ is ([JSLT-PEN.md §5](JSLT-PEN.md#5-the-types)).
953
+
954
+ `PatchPath<State, Payload>` is the union of the lambda and the pointer
955
+ string, and its lambda sees the same scope the action does — so a
956
+ computed index may read `$payload`, annotated the same way
957
+ (`(c: Expr<State>, y: ActionScope<P>) => …`). `FanScope<Item>` is the
958
+ one-member scope a subscription's `withQuery`/`key` gets under a `for`.
959
+
960
+ ### 5.3 A member named like a chain method captures as the method
961
+
962
+ The trap a reader meets in their first hour, and it is not the pen's:
963
+ the capture proxy answers a chain METHOD before it answers a member of
964
+ the same name, so a state whose member shares a name with one of them
965
+ hands the lambda a function instead of an expression. The set is
966
+ [QUERY-PEN.md §4](QUERY-PEN.md#4-the-mapping-table)'s "expression
967
+ methods" row, and a state schema meets it more often than a reader
968
+ expects — `count`, `sum`, `min`, `max`, `avg`, `length`, `at`, `all`,
969
+ `get`, `exists`, `contains`, `replace` and `add` are all ordinary member
970
+ names and all shadowed. Names that are NOT shadowed include the sequence
971
+ terminals: `first`, `map` and `filter` are chain methods on a SEQUENCE,
972
+ and an action's state is an expression, so those read as members.
973
+
974
+ ```js
975
+ const app = defineApp({
976
+ state: s.object({ count: s.integer().default(0) }),
977
+ view: [rule('$', (v) => ['output', {}, v.count])],
978
+ // …
979
+ });
980
+ // JL0005: a captured expression cannot embed a function value
981
+ ```
982
+
983
+ The same spelling in an action reads the same way —
984
+ `action((st) => transition({ state: st.count }))` is the same `JL0005`
985
+ — and the message names the symptom rather than the cause, because by
986
+ the time the capture sees a function it no longer knows the name that
987
+ produced it. The escape is the one `get()` exists for, and it lowers to
988
+ exactly what a member read would have:
989
+
990
+ ```js
991
+ rule('$', (v) => ['output', {}, v.get('count')]) // "$['count']"
992
+ replace((c) => c.get('count'), 1) // { "op": "replace", "path": "/count" }
993
+ ```
994
+
995
+ Naming the member something else is the other fix and usually the better
996
+ one. Nothing in the state's SCHEMA is affected — the member is a member
997
+ either way, and only the lambda that reads it has to spell it
998
+ differently.
999
+
1000
+ ### 5.4 The schema slot is typed by the overload that filled it
1001
+
1002
+ `defineApp()` answers `null` for `stateSchema` when the state is a plain
1003
+ JSON value with no `schema` beside it, and a document for the two
1004
+ overloads that were handed a builder. `AppResult` carries that as its
1005
+ THIRD phantom rather than one union across all three, so the presence of
1006
+ a schema survives into the type:
1007
+
1008
+ | The call | `stateSchema` |
1009
+ |---|---|
1010
+ | `defineApp({ state: <builder>, … })` | `JsonSchema \| boolean` |
1011
+ | `defineApp({ state: <json>, schema: <builder>, … })` | `JsonSchema \| boolean` |
1012
+ | `defineApp({ state: <json>, … })` | `null` |
1013
+
1014
+ That is what lets the one line every consumer writes compile with no
1015
+ narrow and no cast — and it is exactly the line that does not compile
1016
+ when the slot is one union, because `JarenValidator.compile()` takes
1017
+ `boolean | JSONSchemaLike` and not `null`:
1018
+
1019
+ ```ts
1020
+ const { stateSchema } = defineApp({ state: s.object({ n: s.integer().default(0) }), view: [] });
1021
+ new JarenValidator().compile(stateSchema); // fine
1022
+
1023
+ const plain = defineApp({ state: { n: 1 }, view: [] });
1024
+ new JarenValidator().compile(plain.stateSchema); // does not compile: there is no schema
1025
+ ```
1026
+
1027
+ The third parameter defaults to the whole union, so `AppResult<State,
1028
+ Actions>` still names any result and no consumer annotation has to spell
1029
+ it. `test/consumer/linq-app.ts` pins all four lines, the negative
1030
+ included.
1031
+
1032
+ ### 5.5 What the pins hold
1033
+
1034
+ `test/consumer/linq-app.ts` carries this pen's compile-level half beside
1035
+ the forms pen's — one file, because they are the two pens a form-shaped
1036
+ application uses together. Compiled by `npm run test:types`, it holds:
1037
+
1038
+ - `StateOf<typeof todo>` EQUAL (not merely assignable) to `Infer<typeof
1039
+ State>`, and `ActionsOf<typeof todo>` equal to the declared name union
1040
+ — both by the strict `Equals<>` test, in both directions;
1041
+ - `todo.document` assignable to `AppDocument<State, Action>`, and a
1042
+ `@ts-expect-error` on `todo.document.stateSchema`: the two are two
1043
+ members and the document is not one of them;
1044
+ - a payload member the declaration does not carry, a state member the
1045
+ schema does not carry, and an `$event` field the binding did not
1046
+ request — three `@ts-expect-error`s, each the type half of a refusal
1047
+ §4 states at run time;
1048
+ - `bind<Action>('nope')` and `bind<ActionsOf<typeof todo>>('nope')`,
1049
+ both `@ts-expect-error`;
1050
+ - `transition({ nope: 1 })` and `sub('interval', { with: () => 1 })`,
1051
+ the two closed vocabularies;
1052
+ - `PatchOp`, `EffectDeclaration<'save'>` and `SubDeclaration<'interval'>`
1053
+ as the concrete types the three helpers answer, with
1054
+ `effect('save').run` pinned to the literal `'save'`.
1055
+
1056
+ ## 6. What it cannot spell
1057
+
1058
+ Every construct the app pen refuses as unspellable is `JL0102`, and §4.3
1059
+ carries the four conditions with the spelling that works for each: a
1060
+ patch path that is not a chain of member reads and subscripts, an
1061
+ `$event` field APP-FORMAT §3.1 excludes by construction, an initial
1062
+ state no `default()` describes, and a view binding an action `actions`
1063
+ does not declare. Those are limits of the FORMAT reached through the
1064
+ pen, and each has a way through.
1065
+
1066
+ One limit is the format's own and has no way through today: **an action
1067
+ document cannot express AWAITING.** A transition is a pure function of
1068
+ the pre-transition state to a next state plus a list of effects; an
1069
+ effect runs after the transition settles and reaches back only by
1070
+ dispatching another action. That is deliberate — it is what makes a
1071
+ transition replayable and a document serializable — but it means a
1072
+ sequence like "call this, then patch with the answer, then call that" is
1073
+ written as a chain of actions and effect callbacks rather than as one
1074
+ document.
1075
+
1076
+ The convention for it exists and is the host's, not the format's:
1077
+ `createTaskEffect` and the async-task pattern of
1078
+ [TASKS.md](../../app/docs/TASKS.md) give the sequence a key, a
1079
+ concurrency mode and a structured failure, and APP-FORMAT §9 is the
1080
+ normative half. What is open is expressing the awaiting IN the action
1081
+ document, which is an entry in `docs/ROADMAP.md` under `@jarenjs/app`;
1082
+ this pen writes whatever that grammar grows, because it has no opinion
1083
+ of its own about it.
1084
+
1085
+ Two smaller things the pen deliberately does not do, so a reader does
1086
+ not look for them:
1087
+
1088
+ - **It writes no state schema into the document.** The format has no
1089
+ slot for one, and inventing an `x-` member for it would make the pen's
1090
+ documents a dialect. `stateSchema` is answered beside the document and
1091
+ goes to `options.validateState`.
1092
+ - **It judges nothing the loop must judge anyway.**
1093
+ [§2.6](#26-what-the-pen-does-not-judge) is the list, and the reason is
1094
+ the binder's rule 1: a pen refuses only what it can see, and inventing
1095
+ a check the engine does not make would be a second, weaker validator.
1096
+
1097
+ ### 6.1 When not to reach for this pen
1098
+
1099
+ - **The app document is data.** One read from a file, authored in the
1100
+ studio, projected from a machine by `fsmToApp`
1101
+ ([FLOW-PEN.md](FLOW-PEN.md)), or written by a model is a value;
1102
+ `createApp` takes it directly.
1103
+ - **You are writing a component, not an application.** An app document is
1104
+ one state, one view and one action map — the unit `createApp` runs. A
1105
+ reusable widget is a view function and a set of props, and it lives in
1106
+ `@jarenjs/view` where it can take children and callbacks; an app
1107
+ document has no slot for either.
1108
+ - **The logic is mostly awaiting.** §6 above says it plainly: a
1109
+ transition is pure and an effect reaches back only by dispatching. An
1110
+ application whose interesting part is a sequence of awaited calls is
1111
+ mostly host code with an app document attached, and the reading order
1112
+ should follow — write the tasks, then the document that dispatches
1113
+ them.
1114
+ - **The state does not fit in a document.** Every value in the state is
1115
+ JSON: no DOM nodes, no class instances, no functions, no handles. A
1116
+ canvas, a media element or a socket lives in the host and reaches the
1117
+ document only as the data it produces.
1118
+ - **You would be fighting the patch.** A state shape that needs deep
1119
+ rewrites on every event is a shape that wants normalizing before it
1120
+ wants a pen. §3.1 is the first place this shows: `add` at a path that
1121
+ names an array replaces it, and a reader who wanted `append` had a
1122
+ shape question rather than a spelling one.
1123
+
1124
+ ## 7. Cost
1125
+
1126
+ `@jarenjs/linq/app` builds to **<!--fact:bundle.app-->47,444<!--/fact--> bytes** as a minified,
1127
+ tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1128
+ measures and `npm run test:tree-shaking` reports, published rounded
1129
+ beside the other nine subpath prices in
1130
+ [docs/CONSUMING.md](../../../docs/CONSUMING.md). It carries the schema
1131
+ pen and the JSLT pen (state, and views), and no chain module, no
1132
+ `@jarenjs/app` or `@jarenjs/view` byte and no other pen.
1133
+
1134
+ It is the second-largest pen bundle after the client, and the two pens
1135
+ it carries are most of it. The three figures the same probe measures,
1136
+ side by side: `@jarenjs/linq/schema` <!--fact:bundle.schema-->33,156<!--/fact--> bytes,
1137
+ `@jarenjs/linq/jslt` <!--fact:bundle.jslt-->19,856<!--/fact-->, `@jarenjs/linq/app` <!--fact:bundle.app-->47,444<!--/fact-->. The subpath sums do not add — all
1138
+ three carry the capture, the expression lowering and the JSON boundary,
1139
+ which each bundle counts once — so what the app pen costs a consumer who
1140
+ already imports the schema pen is the difference the numbers do state:
1141
+ **14,435 bytes**, the JSLT pen's non-shared half plus this pen's own
1142
+ `defineApp`, action capture, seven patch operations, pointer lowering,
1143
+ `bind()`, `sub()` and their refusal messages.