@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,955 @@
1
+ # The Jaren JSLT pen
2
+
3
+ > `./jslt` — `$jslt` 0.1 stylesheets: the envelope and its rules, whose
4
+ > bodies are captured over the matched value. **Read it when** you are
5
+ > transforming one document into another
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 one shape of JSON and you need another one — a record reshaped
18
+ for an API, a document rendered as a view, a value converted everywhere it
19
+ appears in a tree. A stylesheet is the declarative way to say that, and
20
+ writing one by hand means writing JSONPath strings and operator objects
21
+ into JSON literals with nothing checking either. This pen lets you write
22
+ each rule's body as a JavaScript arrow function and hands you the document
23
+ it recorded.
24
+
25
+ ```js
26
+ import { stylesheet, rule, body, apply, op } from '@jarenjs/linq/jslt';
27
+ ```
28
+
29
+ writes `$jslt` 0.1 stylesheets ([JSLT-FORMAT](../../json/docs/JSLT-FORMAT.md)):
30
+ the envelope and its rules, whose bodies are callbacks captured over `$` —
31
+ the matched value — through the chain's recording proxy, with the two
32
+ externals the engine binds on every dispatch (`root`, `path`) and the
33
+ parameters a body declares. The document is what `compileJsltStylesheet`
34
+ takes unchanged; the grammar it validates under is `jaren-jslt`
35
+ (`packages/json/schemas/jaren-jslt.schema.json`, and its draft-07 twin);
36
+ the engine is `@jarenjs/json`'s stylesheet dispatcher.
37
+
38
+ The pen imports no engine and judges nothing the compiler judges — path
39
+ syntax (`JT0003`), the body's operators (`JT0007`), a `schema` match's
40
+ hook (`JT0006`), the depth guard (`JT2001`) — with one exception it can
41
+ see earlier: the `[]` idiom of JSLT-FORMAT §6.3.
42
+
43
+ **The running example.** §3 is one publisher's catalogue, seen eight ways:
44
+ summarised, walked as a book of chapters, repriced in another currency,
45
+ rendered twice under two modes, appraised through a registered operator,
46
+ sorted by an importer's log records, rendered as a view, and matched by a
47
+ schema builder. Each rule set stands alone — that is what the gate runs —
48
+ but they are all over the same kind of document, so a member you meet in
49
+ §3.1 means the same thing in §3.8.
50
+
51
+ **A stylesheet is recursive, and a two-rule example hides it.** The whole
52
+ point of a template language is that a rule's body dispatches BACK into
53
+ the rule set for its children, so the shape a reader should have in mind
54
+ is a walk, not a mapping:
55
+
56
+ ```js
57
+ import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
58
+
59
+ export const outline = stylesheet([
60
+ rule('$', (v) => ({ name: v.name, children: [apply(v.children.all())] })),
61
+ rule('$..children[*]', (v) => ({ name: v.name, children: [apply(v.children.all())] })),
62
+ ]);
63
+ // the second rule applies to its own children, so one rule walks a tree
64
+ // of any depth: { name: '$.name', children: [ { $apply: '$.children[*]' } ] }
65
+ ```
66
+
67
+ Both rules emit the same body; the second one reaching its own matches is
68
+ what makes the stylesheet a recursive walk. §3.2 shows the same shape with
69
+ two different bodies, which is what a real transform looks like, and §3.4
70
+ shows the same walk split across two modes.
71
+
72
+ ### 1.1 How a JavaScript callback becomes a document
73
+
74
+ This is the one thing about this pen a reader cannot get from
75
+ JSLT-FORMAT, and it is stated here once. The flow pen
76
+ ([FLOW-PEN.md](FLOW-PEN.md) §5), the migration pen
77
+ ([MIGRATION-PEN.md](MIGRATION-PEN.md) §4.3) and the app pen
78
+ ([APP-PEN.md](APP-PEN.md)) capture through the same machine and link
79
+ this section rather than restate it.
80
+
81
+ A body is not parsed and it is not serialized. It is **run once, at build
82
+ time, against a recording proxy** — the same proxy the chain's `select()`
83
+ records — and the document is what the recording left behind. Five
84
+ consequences, in the order a writer meets them:
85
+
86
+ 1. **A member read records a path.** `v.author.name` is not a value; it
87
+ is a proxy that answers another proxy for every member read and
88
+ remembers the walk. When the callback returns, that walk has become the
89
+ string `"$.author.name"`. `v.chapters.all()` records the wildcard
90
+ segment (`"$.chapters[*]"`), `v.get('sub title')` records a bracket
91
+ segment (`"$['sub title']"`), and an operator method records the
92
+ operator (`v.price.mul(1.21)` → `{ "$mul": ["$.price", 1.21] }`). The
93
+ operators are the chain's own and are documented once, in
94
+ [QUERY-PEN.md](QUERY-PEN.md) §4.
95
+ 2. **The callback runs exactly once, with no data — so JavaScript's own
96
+ logic is not the document's.** There is no input value to branch on,
97
+ and the ways that goes wrong divide in two. `>`, `<`, `+` and template
98
+ interpolation throw a plain `TypeError` ("Cannot convert object to
99
+ primitive value"): loud, uncoded, and impossible to miss. But `!`,
100
+ `&&`, `||`, the ternary, `in`, `typeof` and `Object.keys` all evaluate
101
+ against the PROXY and silently write the wrong document —
102
+ `() => ({ live: !v.deleted })` writes `{ "live": false }`,
103
+ `() => ({ both: v.a && v.b })` keeps only the right operand
104
+ (`{ "both": "$.b" }`), and `v.flag ? 'a' : 'b'` is always `'a'`. A
105
+ condition belongs in the document: the expression surface's `.and()`,
106
+ `.or()`, `.not()`, an operator (`op('$if', …)`), or a second rule with
107
+ a narrower `match`. [QUERY-PEN.md](QUERY-PEN.md) §3 states this once
108
+ for every capture in the suite.
109
+ 3. **The externals argument is a closed world.** `body()`'s second
110
+ argument answers exactly the names the engine will bind: `root` and
111
+ `path` always (JSLT-FORMAT §8.2), plus each name the body declared in
112
+ `{ externals: [...] }` (§8.1). Any other name is `JL0104` at build
113
+ time, where the fix can be named — see §4.3. That is the whole reason
114
+ the argument is a proxy and not an object: an object would answer
115
+ `undefined` and the mistake would surface at run time as a missing
116
+ binding.
117
+ 4. **A returned literal is a CONSTRUCTOR, not a constant.** The chain
118
+ folds a pure data tree into one `$const` ([QUERY-PEN.md](QUERY-PEN.md)
119
+ §3, expression capture); a body does not. `() => ({ level: 'unknown' })`
120
+ writes `{ "level": "unknown" }` — the format's own object constructor,
121
+ JSLT-FORMAT Appendix A.6's spelling — because that is what a
122
+ stylesheet author reads and edits.
123
+ The pen passes `fold: false` to the shared capture for exactly this.
124
+ The cost is that a string is now ambiguous with a path, so a string
125
+ value that starts with `$` is escaped `$$` on the way out
126
+ (`() => '$x'` writes `"$$x"`); the engine unescapes it.
127
+ 5. **The document is a value.** What comes back is plain, deep-frozen
128
+ JSON — never the caller's object, never a proxy. Round-tripping it
129
+ through `JSON.stringify` and `JSON.parse` deep-equals it, two builds
130
+ of one spelling are one document, and an array the caller passed in
131
+ stays unfrozen. Captures
132
+ also nest: a `body()` built inside another body's callback is its own
133
+ document, and embedding it in the outer literal makes it DATA there
134
+ (the `$$` escape says so).
135
+
136
+ The machine underneath is `captureQuery` in
137
+ `packages/linq/src/capture-root.js` — a value rooted at `$` plus a list
138
+ of named externals — and `body()` is the JSLT pen's entry point into it
139
+ (`packages/linq/src/jslt/body.js`). The migration pen calls `body()`
140
+ itself, so a `jslt` migration step's externals are `root` and `path` too.
141
+ The flow and app pens call `captureQuery` directly with an EMPTY external
142
+ list, because their engines bind nothing; [FLOW-PEN.md](FLOW-PEN.md) §5
143
+ states what that changes.
144
+
145
+ ## 2. The mapping table
146
+
147
+ Five exported names — `stylesheet`, `rule`, `body`, `apply` and `op` —
148
+ and seventeen rows, because every member and option a call takes earns
149
+ one of its own (`match`'s three forms, `mode`, `priority`, `unmatched`,
150
+ `modes`, and the two things that can stand where a body is taken). The
151
+ completeness gate in `test/linq/pen-docs.test.js` asserts that every
152
+ exported callable name appears somewhere in this section; rows
153
+ legitimately outnumber names here, which is why that gate is
154
+ one-directional, and this pen is its clearest case.
155
+
156
+ | Method | Emits | Type reading | Status |
157
+ |---|---|---|---|
158
+ | `stylesheet(rules, { unmatched?, modes? })` | `{ $jslt: '0.1', unmatched?, modes?, rules }` — the envelope (§2.1), in that member order; the bare-array form is the rules array itself | `Stylesheet<In, Out>`: the FIRST rule's phantoms, or the author's (`stylesheet<In, Out>(…)`) | native; a non-array, another option, a rule that is not an object, a rule without `body`, a rule that is not JSON `JL0101` |
159
+ | `unmatched` | `unmatched: 'share' \| 'fresh' \| 'error'` (§5) | `Disposition` | native; another value `JL0101` |
160
+ | `modes` | `modes: { name: { unmatched } }` (§2.1) | — | native; another member, a mode that is not `{ unmatched }`, a map whose prototype a `__proto__:` literal replaced `JL0101` |
161
+ | `rule(match, body, { mode?, priority? })` | `{ mode?, match?, priority?, body }` — the rule object (§2.2), in that member order | `Rule<In, Out>` | native; a non-object options, another option `JL0101` |
162
+ | `match` as a JSONPath string | `match: '$..price'` (§3.1) | the honest top | native |
163
+ | `match` as `{ path?, schema? }` | the object; `schema` a schema-pen builder's document, or a schema verbatim | a builder types the body's value (`Infer<>`) | native; `{}` is `JL0102`; another member, a non-string `path`, a `schema` that is not JSON `JL0101` |
164
+ | `match` `null` or absent | no `match` member — the unconditional rule (default priority −1, §4) | the honest top | native |
165
+ | `mode` | `mode: 'toc'` (§7) | `string` | native; a non-string `JL0101` |
166
+ | `priority` | `priority: 2` (§4) | `number` | native; a non-finite number, or `-0`, `JL0101` |
167
+ | `body(fn, { externals? })` | the captured query document — `fn(v, x)` with `v` at `$`, `x.root`/`x.path` (§8.2) and the declared parameters as `$name` externals (§8.1); a returned literal is a constructor, a string starting `$` is escaped `$$` | `BodyDocument<In, Out>`: `In` from the annotated `v` (`(v: Expr<Book>) => …`), `Out` the unwrapped return; a declared parameter is `UnknownExpr` until `x` is annotated (`x: Externals<{ rate: number }>`) | native; a non-callback, a non-object options, another option, a non-array or non-identifier external `JL0101`; an undeclared external `JL0104`; `root`/`path` declared `JL0104` |
168
+ | a callback where a body is taken | `body(fn)` with no parameters | as above; the match's builder types `v` | native |
169
+ | a query document where a body is taken | the document, verbatim (a `body()` result, or by hand) | a `body()` document carries its phantoms; a hand-written one is `unknown` | native; not JSON `JL0101` |
170
+ | `apply(selector)` | `{ $apply: selector }` — the rule's own mode (§6.2); the selector an expression (`v.chapters.all()`), a path string verbatim (`'$.chapters[*]'`), or data (`[1, 2]` embeds as `$const`) | `UnknownExpr` — a dispatch to other rules | native; no selector `JL0101`; outside `body()` `JL0102` |
171
+ | `apply(selector, mode)` | `{ $apply: [selector, mode] }` — the argument-list form (§6.2) | `UnknownExpr` | native; a non-string mode `JL0101` |
172
+ | `[apply(…)]` as a member value | `[{ $apply: … }]` — the `[]` idiom (§6.3) | `unknown[]` | native |
173
+ | `apply(…)` as a bare member value | — | — | refused (`JL0102`): the engine fails at run time on the second child (`JQ2001`) |
174
+ | `op(name, operands)` | `{ [name]: operands }` — a registered operator (§13), spelled without judging it; one operand or a list | `UnknownExpr` | native; the engine's `JQ0002` decides; a name without `$` `JL0101`; outside any capture `JL0005` |
175
+
176
+ Three rules the table implies, spelled out:
177
+
178
+ - **A body's literal is a constructor, not a constant.** §1.1 point 4 is
179
+ where the reason lives; the consequence for the table is that a `body`
180
+ cell never shows a `$const` unless the value reached the document as
181
+ DATA (an `apply()` selector that is plain data, an `op()` operand that
182
+ is an object).
183
+ - **`root` and `path` need no declaration; a parameter needs one.** The
184
+ engine binds the two on every dispatch (§8.2) and shadows any binding
185
+ of the same name, so declaring them is the mistake and is refused. A
186
+ parameter is `body(fn, { externals: ['rate'] })`, and
187
+ `transform.externals` lists exactly the declared names the body used
188
+ (§8.3).
189
+ - **The pen judges nothing the compiler judges.** A path that does not
190
+ parse, an operator no registry answers, a `schema` match compiled
191
+ without a hook, a self-applying loop: each is the engine's own error
192
+ (`JT0003`, `JT0007`/`JQ0002`, `JT0006`, `JT2001`), unwrapped. The one
193
+ refusal the pen adds is the one the engine would only raise at RUN
194
+ time.
195
+
196
+ ## 3. Worked examples
197
+
198
+ Every `js` fence below exports exactly one stylesheet (or rule list), and
199
+ the `json` fence that follows it is what the pen emits — executed by
200
+ `test/linq/pen-docs.test.js`, which imports each fence from the workspace
201
+ and compares its one export to the JSON beside it. The seven fixtures of
202
+ JSLT-FORMAT Appendix A are all rebuilt through the pen and held BYTE-equal
203
+ to the format doc's own fences by `test/linq/jslt-pen.test.js`; three of
204
+ them are here, and every document below also validates under both
205
+ published grammars.
206
+
207
+ ### 3.1 A rule at the root, with a `$const`-free body
208
+
209
+ The plainest thing the pen does: one rule matching the whole document,
210
+ and a body of member reads and operators. Nothing here is data, so
211
+ nothing here is `$const`.
212
+
213
+ ```js
214
+ import { stylesheet, rule } from '@jarenjs/linq/jslt';
215
+
216
+ export const summary = stylesheet([
217
+ rule('$', (v) => ({
218
+ isbn: v.isbn,
219
+ author: v.author.name.upper(),
220
+ pages: v.chapters.all().pages.sum(),
221
+ chapters: v.chapters.all().count(),
222
+ })),
223
+ ]);
224
+ ```
225
+
226
+ ```json
227
+ { "$jslt": "0.1",
228
+ "rules": [
229
+ { "match": "$",
230
+ "body": { "isbn": "$.isbn",
231
+ "author": { "$upper": "$.author.name" },
232
+ "pages": { "$sum": "$.chapters[*].pages" },
233
+ "chapters": { "$count": "$.chapters[*]" } } }
234
+ ] }
235
+ ```
236
+
237
+ Over `{ isbn: '978-1', author: { name: 'ada' }, chapters: [{ pages: 3 }, { pages: 4 }] }`
238
+ the compiled transform answers
239
+ `{ isbn: '978-1', author: 'ADA', pages: 7, chapters: 2 }`. Note
240
+ `v.chapters.all().count()`, not `v.chapters.count()`: the first counts the
241
+ ITEMS the wildcard yields, the second counts the one value `$.chapters` is.
242
+ That distinction is the chain's, not this pen's, and QUERY-PEN §4 is
243
+ where it is stated.
244
+
245
+ ### 3.2 An `apply()` splice, and the `[]` idiom
246
+
247
+ JSLT-FORMAT Appendix A.3 — the book example, done right. The brackets
248
+ around the `apply()` are not decoration: they are what splices the
249
+ dispatched sequence into one array.
250
+
251
+ ```js
252
+ import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
253
+
254
+ export const book = stylesheet([
255
+ rule({ schema: { type: 'object', required: ['isbn'] } },
256
+ (v) => ({ title: v.title, children: [apply(v.chapters.all())] })),
257
+ rule({ schema: { type: 'object', required: ['heading'] } },
258
+ (v) => ({ name: v.heading })),
259
+ ]);
260
+ ```
261
+
262
+ ```json
263
+ { "$jslt": "0.1",
264
+ "rules": [
265
+ { "match": { "schema": { "type": "object", "required": ["isbn"] } },
266
+ "body": { "title": "$.title",
267
+ "children": [ { "$apply": "$.chapters[*]" } ] } },
268
+ { "match": { "schema": { "type": "object", "required": ["heading"] } },
269
+ "body": { "name": "$.heading" } }
270
+ ] }
271
+ ```
272
+
273
+ Drop the brackets — `children: apply(v.chapters.all())` — and the pen
274
+ refuses with `JL0102` before anything runs, because the engine would
275
+ accept it and then fail on the second chapter. §6.1 shows both spellings
276
+ side by side with the run-time error the refusal prevents.
277
+
278
+ ### 3.3 A body captured directly: `root`, `path` and a declared parameter
279
+
280
+ Appendix A.7, in the bare-array form (a rules array is a stylesheet too).
281
+ This is `body()` used on its own, which is what the reader reaches for
282
+ when a body is shared between rules or built conditionally in JavaScript.
283
+
284
+ ```js
285
+ import { rule, body } from '@jarenjs/linq/jslt';
286
+
287
+ export const priced = [
288
+ rule('$..price', body(
289
+ (v, x) => ({ amount: v.mul(x.rate), currency: x.root.currency, at: x.path }),
290
+ { externals: ['rate'] })),
291
+ ];
292
+ ```
293
+
294
+ ```json
295
+ [ { "match": "$..price",
296
+ "body": { "amount": { "$mul": ["$", "$rate"] },
297
+ "currency": "$root.currency",
298
+ "at": "$path" } } ]
299
+ ```
300
+
301
+ Three different things become three different spellings: `v` is the
302
+ matched value, so it records `"$"`; `x.root` is the input document root,
303
+ so it records `"$root.…"`; `x.path` is the matched value's normalized
304
+ path, a STRING the engine supplies per dispatch. Only `rate` needed
305
+ declaring, and the compiled transform then reports
306
+ `transform.externals` as `['rate']` — a caller who omits it gets
307
+ `JT2004` wrapping `JQ2006` at run time, which is the engine's business,
308
+ not the pen's.
309
+
310
+ ### 3.4 Two modes, and the `modes` member
311
+
312
+ Appendix A.4 with dispositions added: the same sections rendered twice,
313
+ once as a table of contents and once as body copy, and each mode saying
314
+ what happens when nothing matches (§5).
315
+
316
+ ```js
317
+ import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
318
+
319
+ export const guide = stylesheet([
320
+ rule('$', (v) => ({
321
+ toc: [apply(v.sections.all(), 'toc')],
322
+ body: [apply(v.sections.all(), 'render')],
323
+ })),
324
+ rule('$.sections[*]', (v) => ({ ref: v.id, label: v.heading }), { mode: 'toc' }),
325
+ rule('$.sections[*]', (v) => ({ anchor: v.id, heading: v.heading, text: v.text }), { mode: 'render' }),
326
+ ], { unmatched: 'share', modes: { toc: { unmatched: 'error' }, render: { unmatched: 'fresh' } } });
327
+ ```
328
+
329
+ ```json
330
+ { "$jslt": "0.1",
331
+ "unmatched": "share",
332
+ "modes": { "toc": { "unmatched": "error" }, "render": { "unmatched": "fresh" } },
333
+ "rules": [
334
+ { "match": "$",
335
+ "body": { "toc": [ { "$apply": ["$.sections[*]", "toc"] } ],
336
+ "body": [ { "$apply": ["$.sections[*]", "render"] } ] } },
337
+ { "mode": "toc", "match": "$.sections[*]",
338
+ "body": { "ref": "$.id", "label": "$.heading" } },
339
+ { "mode": "render", "match": "$.sections[*]",
340
+ "body": { "anchor": "$.id", "heading": "$.heading", "text": "$.text" } }
341
+ ] }
342
+ ```
343
+
344
+ The envelope's member order is fixed (`$jslt`, `unmatched`, `modes`,
345
+ `rules`) whatever order the options were written in, and a rule's is
346
+ `mode`, `match`, `priority`, `body`. The `mode` argument of `apply()` is
347
+ a literal string, never an expression — dynamic mode selection is a
348
+ non-goal of 0.1 (§6.2) — and passing one records `JL0101` naming that.
349
+
350
+ ### 3.5 `op()` — an operator the chain cannot reach
351
+
352
+ `op()` exists because a registry may carry operators the chain has no
353
+ method for. `$npv` is one: `@jarenjs/json`'s `financePack` registers it,
354
+ and no chain builder spells it.
355
+
356
+ ```js
357
+ import { stylesheet, rule, op } from '@jarenjs/linq/jslt';
358
+
359
+ export const appraisal = stylesheet([
360
+ rule('$.titles[*]', (v) => ({
361
+ name: v.name,
362
+ npv: op('$npv', [v.rate, v.cashflows.all()]),
363
+ })),
364
+ ]);
365
+ ```
366
+
367
+ ```json
368
+ { "$jslt": "0.1",
369
+ "rules": [
370
+ { "match": "$.titles[*]",
371
+ "body": { "name": "$.name",
372
+ "npv": { "$npv": ["$.rate", "$.cashflows[*]"] } } }
373
+ ] }
374
+ ```
375
+
376
+ The pen writes the name it is given and stops there. Compiled through
377
+ `createJsltRegistry().use(financePack)` this runs; compiled through the
378
+ default `compileJsltStylesheet` it is `JT0007` wrapping `JQ0002`,
379
+ "unknown operator". That is deliberate: LINQ-FORMAT §1.1 rule 1 forbids
380
+ the pen from carrying its own opinion of which operators exist, because
381
+ the registry is the host's to choose. `op()` also works inside a chain
382
+ callback — it lifts into whatever capture is in progress — and outside
383
+ every capture it is the chain's `JL0005`.
384
+
385
+ ### 3.6 A constructor literal, and the `$$` escape
386
+
387
+ Appendix A.6's dispatch with an explicit fallback. Every body here
388
+ returns a JavaScript object literal, and every one of them lands in the
389
+ document as an object constructor rather than a `$const` — including the
390
+ string that starts with `$`, which is escaped so the engine reads it as
391
+ data.
392
+
393
+ ```js
394
+ import { stylesheet, rule } from '@jarenjs/linq/jslt';
395
+
396
+ export const levels = stylesheet([
397
+ rule({ schema: { type: 'object', required: ['error'] } }, (v) => ({ level: 'fatal', message: v.error })),
398
+ rule({ schema: { type: 'object', required: ['info'] } }, (v) => ({ level: 'note', message: v.info })),
399
+ rule(null, () => ({ level: 'unknown', hint: '$path is a literal here' })),
400
+ ], { unmatched: 'error' });
401
+ ```
402
+
403
+ ```json
404
+ { "$jslt": "0.1",
405
+ "unmatched": "error",
406
+ "rules": [
407
+ { "match": { "schema": { "type": "object", "required": ["error"] } },
408
+ "body": { "level": "fatal", "message": "$.error" } },
409
+ { "match": { "schema": { "type": "object", "required": ["info"] } },
410
+ "body": { "level": "note", "message": "$.info" } },
411
+ { "body": { "level": "unknown", "hint": "$$path is a literal here" } }
412
+ ] }
413
+ ```
414
+
415
+ Read the third rule closely. `'fatal'`, `'note'` and `'unknown'` are
416
+ data, and they are spelled as themselves because no path can be confused
417
+ with them. `'$path is a literal here'` starts with `$`, which in a
418
+ document means an expression, so the pen wrote `"$$path is a literal
419
+ here"` — and the engine unescapes it back to the string the callback
420
+ returned. The rule for a reader is short: **an object literal in a body
421
+ is always spelled as the format's own constructor, and a string in it is
422
+ data**, escaped where it has to be. There is no spelling of a body that
423
+ turns an object literal into an expression by accident; to write an
424
+ expression you call one (a member read, an operator, `apply()`, `op()`).
425
+
426
+ The `rule(null, …)` in third place is the unconditional rule, whose
427
+ default priority is −1 (§4) — it is what makes `unmatched: 'error'` safe
428
+ to declare, since nothing can reach the built-in rule.
429
+
430
+ ### 3.7 A stylesheet whose output is a view
431
+
432
+ A `jaren-vnode` tree is `[tag, props, children]` arrays, so a stylesheet
433
+ that writes one is an ordinary stylesheet whose bodies return arrays. This
434
+ is how an app document's view is written — [APP-PEN.md](APP-PEN.md) links
435
+ here for it — and how a `jslt` node in a dataflow renders
436
+ ([FLOW-PEN.md](FLOW-PEN.md) §3.6).
437
+
438
+ ```js
439
+ import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
440
+
441
+ export const catalogue = stylesheet([
442
+ rule('$', (v) => ['ul', { class: 'catalogue' }, [apply(v.books.all())]]),
443
+ rule('$.books[*]', (v) => ['li', { 'data-isbn': v.isbn }, v.title]),
444
+ ]);
445
+ ```
446
+
447
+ ```json
448
+ { "$jslt": "0.1",
449
+ "rules": [
450
+ { "match": "$",
451
+ "body": ["ul", { "class": "catalogue" }, [ { "$apply": "$.books[*]" } ]] },
452
+ { "match": "$.books[*]",
453
+ "body": ["li", { "data-isbn": "$.isbn" }, "$.title"] }
454
+ ] }
455
+ ```
456
+
457
+ The `[]` idiom is doing its usual work in the children position, and for
458
+ once the brackets look like what they are: an array constructor holding
459
+ one spliced sequence. Over two books this renders
460
+ `['ul', { class: 'catalogue' }, [['li', { 'data-isbn': '978-1' }, 'Ada'], ['li', { 'data-isbn': '978-2' }, 'Grace']]]`.
461
+
462
+ ### 3.8 An `.open()` match, and the honest top
463
+
464
+ A `schema` match that is a schema-pen builder types the body's value, so
465
+ `v.title` is `string` and `v.titel` does not compile. `.open()` changes
466
+ that reading and it is worth seeing why.
467
+
468
+ ```js
469
+ import * as s from '@jarenjs/linq/schema';
470
+ import { stylesheet, rule } from '@jarenjs/linq/jslt';
471
+
472
+ const Book = s.object({ isbn: s.string(), title: s.string() }).open();
473
+
474
+ export const listing = stylesheet([
475
+ rule({ schema: Book }, (v) => ({ title: v.title, isbn: v.isbn, extra: v.get('subtitle') })),
476
+ ]);
477
+ ```
478
+
479
+ ```json
480
+ { "$jslt": "0.1",
481
+ "rules": [
482
+ { "match": { "schema": { "type": "object",
483
+ "properties": { "isbn": { "type": "string" },
484
+ "title": { "type": "string" } },
485
+ "required": ["isbn", "title"] } },
486
+ "body": { "title": "$.title", "isbn": "$.isbn", "extra": "$['subtitle']" } }
487
+ ] }
488
+ ```
489
+
490
+ `.open()` removes `additionalProperties: false`, which is why the emitted
491
+ schema carries no such member. The type follows emit's reading of the
492
+ EMITTED document (LINQ-FORMAT §1.1 rule 4), so `Infer<typeof Book>`
493
+ becomes `{ isbn: string; title: string } & { [key: string]: unknown }` —
494
+ and that index signature is what a reader has to see coming, because on
495
+ the chain it changes EVERY member:
496
+
497
+ | | closed `s.object({ isbn, title })` | the same builder `.open()` |
498
+ |---|---|---|
499
+ | `Infer<>` | `{ isbn: string; title: string }` | the same, plus `{ [key: string]: unknown }` |
500
+ | `v.title` in the body | `string` | `unknown` |
501
+ | `v.get('subtitle')` | `unknown`, and nothing says the member exists | `unknown`, and the shape says it may |
502
+ | the emitted document | identical apart from `additionalProperties: false` | — |
503
+
504
+ The chain reads a member through the same lookup for both, and an index
505
+ signature answers `unknown` for every key — so opening the object buys
506
+ the honest statement that the shape has more in it than the schema
507
+ declares, and pays for it with the declared members' types. Both bodies
508
+ above emit exactly the same paths; only the compiler's opinion of them
509
+ differs. Where the members matter more than the openness, keep the
510
+ builder closed and reach the undeclared ones with `get()`; the pen never
511
+ blocks it.
512
+
513
+ ## 4. Refusals
514
+
515
+ The JSLT pen raises these three `LinqBuildError` codes and no others —
516
+ `test/linq/pen-docs.test.js` holds this list equal, in both directions,
517
+ to the codes `packages/linq/src/jslt/` throws. The full condition each
518
+ code states across every pen is the binder's,
519
+ [LINQ-FORMAT.md](LINQ-FORMAT.md) §1.3.
520
+
521
+ | Code | What this pen raises it for |
522
+ |---|---|
523
+ | `JL0101` | a value this pen cannot spell, an option it does not know, or a name → value map it cannot read |
524
+ | `JL0102` | a construct the format cannot carry: an `apply()` as a bare object member, a `match` of `{}`, an `apply()` outside a body |
525
+ | `JL0104` | an external a captured body did not declare, or `root`/`path` declared as one |
526
+
527
+ `packages/linq/src/jslt/` carries **28 throw sites** — 24 `JL0101`, 3
528
+ `JL0102` and 1 `JL0104` — and they collapse to the conditions below. Two
529
+ further conditions reach a caller through this pen without being thrown
530
+ in its directory: the JSON boundary (`requireJson`, `requireNameMap`) and
531
+ the shared capture's undeclared-external check
532
+ (`packages/linq/src/capture-root.js`), which is where the second
533
+ `JL0104` comes from.
534
+
535
+ Every message below is the one the pen raised when the spelling beside it
536
+ was run, with the code prefix (`JL0101: `) removed. `docPath`, where the
537
+ refusal carries one, is the JSON pointer of the node being assembled and
538
+ is appended to the message text as well (`… at /modes/m`); the rows name
539
+ it where it exists, and its absence in a row is not an oversight but the
540
+ pen's state today (§4.4).
541
+
542
+ ### 4.1 `JL0101` — the value, the option and the map
543
+
544
+ **`body()`** — five conditions, all at the door, before the callback runs.
545
+
546
+ | The spelling that trips it | The message | The spelling that works |
547
+ |---|---|---|
548
+ | `body(42)`, `body('$')` | `body() takes a callback (value, x) => …, got 42` | a callback; a document goes straight into `rule()` |
549
+ | `body(fn, 'rate')` | `body() options are { externals?: string[] }, got a string` | `{ externals: ['rate'] }` |
550
+ | `body(fn, { params: ['rate'] })` | `body() does not take 'params'` | `externals` — the format's word (§8.1) |
551
+ | `body(fn, { externals: 'rate' })` | `body() externals is an array of parameter names, got a string` | an array, even for one name |
552
+ | `body(fn, { externals: ['not a name'] })` | `body() externals are identifiers ('rate'), got a string` | an identifier: `[A-Za-z_][A-Za-z0-9_]*` |
553
+
554
+ **`apply()` and `op()`** — three conditions.
555
+
556
+ | The spelling that trips it | The message | The spelling that works |
557
+ |---|---|---|
558
+ | `body(() => [apply()])` | `apply() takes a selector: a path (v.chapters.all(), '$.chapters[*]') or an expression` | a selector |
559
+ | `body((v) => [apply(v, v.mode)])` | `apply() takes the target mode as a literal string (a mode is not an expression, JSLT-FORMAT §6.2), got an expression` | `apply(v, 'toc')` |
560
+ | `body(() => op('npv', []))` | `op() takes an operator name starting with '$' ('$npv'), got a string` | `op('$npv', …)` |
561
+
562
+ **`rule()`** — ten conditions, four of them about `match`.
563
+
564
+ | The spelling that trips it | The message | The spelling that works |
565
+ |---|---|---|
566
+ | `rule(42, fn)` | `rule() match is a JSONPath string or { path?, schema? }, got 42` — `docPath` `/match` | a path string, the object form, or `null` |
567
+ | `rule({ paths: '$' }, fn)` | `rule() match takes 'path' and/or 'schema', not 'paths' (JSLT-FORMAT §3.1)` — `docPath` `/match/paths` | `path`, `schema`, or both |
568
+ | `rule({ path: 42 }, fn)` | `rule() match.path is an RFC 9535 query string, got 42` — `docPath` `/match/path` | a query string |
569
+ | `rule({ schema: new Date(0) }, fn)` | `rule() match.schema 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 schema-pen builder, or a JSON schema |
570
+ | `rule('$')` | `rule() takes a body: a callback (value, x) => …, body(…), or a query document` — `docPath` `/body` | one of the three |
571
+ | `rule('$', { $const: Symbol('x') })` | `rule() body 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 JSON document |
572
+ | `rule('$', fn, 'toc')` | `rule() options are { mode?, priority? }, got a string` | `{ mode: 'toc' }` |
573
+ | `rule('$', fn, { modes: 'x' })` | `rule() does not take 'modes' (JSLT-FORMAT §2.2)` | `mode` on a rule; `modes` on the stylesheet |
574
+ | `rule('$', fn, { mode: 1 })` | `rule() mode is a string naming the rule's mode, got 1` — `docPath` `/mode` | a string; `''` is the unnamed mode |
575
+ | `rule('$', fn, { priority: 'high' })`, `{ priority: NaN }`, `{ priority: -0 }` | `rule() priority is a finite JSON number, got a string` — `docPath` `/priority` | a finite number |
576
+
577
+ **`stylesheet()`** — eleven conditions.
578
+
579
+ | The spelling that trips it | The message | The spelling that works |
580
+ |---|---|---|
581
+ | `stylesheet({ rules: [] })` | `stylesheet() takes an array of rules, got a Object instance` — `docPath` `/rules` | the rules array itself |
582
+ | `stylesheet([], 'fresh')` | `stylesheet() options are { unmatched?, modes? }, got a string` | `{ unmatched: 'fresh' }` |
583
+ | `stylesheet([], { version: '0.1' })` | `stylesheet() does not take 'version' (JSLT-FORMAT §2.1)` | the pen writes `$jslt` itself |
584
+ | `stylesheet([], { unmatched: 'copy' })` | `unmatched is one of 'share', 'fresh' or 'error' (JSLT-FORMAT §5), got 'copy'` — `docPath` `/unmatched` | one of the three |
585
+ | `stylesheet([], { modes: 'x' })` | `stylesheet() modes is { name: { unmatched } }, got a string` — `docPath` `/modes` | a name → `{ unmatched }` map |
586
+ | `stylesheet([], { modes: { __proto__: { unmatched: 'error' } } })` | `stylesheet() modes 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` `/modes` | `{ ['__proto__']: … }` |
587
+ | `stylesheet([], { modes: { m: {} } })`, `{ m: { unmatched: 'share', extra: 1 } }` | `stylesheet() mode 'm' is { unmatched } and nothing else (JSLT-FORMAT §2.1)` — `docPath` `/modes/m` | exactly `{ unmatched }` |
588
+ | `stylesheet([], { modes: { m: { unmatched: 'x' } } })` | `modes/m/unmatched is one of 'share', 'fresh' or 'error' (JSLT-FORMAT §5), got 'x'` — `docPath` `/modes/m/unmatched` | one of the three |
589
+ | `stylesheet(['$'])` | `stylesheet() rule 0 is an object — rule(match, body) — got a string` — `docPath` `/rules/0` | `rule(match, body)` |
590
+ | `stylesheet([{ match: '$' }])` | `stylesheet() rule 0 has no body (JSLT-FORMAT §2.2, the compiler's JT0002)` — `docPath` `/rules/0/body` | give the rule a body |
591
+ | a hand-written rule that is not JSON | `stylesheet() rule 0 received a … instance, which is not JSON — …` — `docPath` `/rules/0` | a JSON rule document |
592
+
593
+ The `modes` row that names `__proto__` is not a curiosity: it is
594
+ LINQ-FORMAT §1.1 rule 5 applied at this pen's one name → value map, and
595
+ the gate that proves it is the same one that proves it for the schema,
596
+ model, contract, flow, app and forms pens.
597
+
598
+ Two refusals a body can raise that are the CHAIN's rather than this
599
+ pen's, listed because a reader who hits one will look here first:
600
+ `rule('$', () => 1n)` is `JL0005` ("a captured expression cannot embed a
601
+ bigint value") and `op('$npv', [])` outside any capture is `JL0005` ("an
602
+ operator expression can only be lifted inside a capture callback — no
603
+ capture is in progress to bind it to"). Both are documented in
604
+ [QUERY-PEN.md](QUERY-PEN.md) §9.
605
+
606
+ ### 4.2 `JL0102` — the construct the format cannot carry
607
+
608
+ Three conditions, and every one of them is the pen seeing something the
609
+ engine would only object to later, or not at all.
610
+
611
+ | The spelling that trips it | The message | The spelling that works |
612
+ |---|---|---|
613
+ | `body((v) => ({ children: apply(v.chapters.all()) }))` | `an object member takes exactly one value — 'children' holds an apply(), which yields a SEQUENCE and fails at run time on the second child (JQ2001); wrap the apply in [] (JSLT-FORMAT §6.3: children: [apply(…)])` — `docPath` `/children` | `children: [apply(v.chapters.all())]` |
614
+ | `rule({}, fn)` | `rule() match {} would match nothing — write no match for the unconditional rule (JSLT-FORMAT §3.1, the compiler's JT0003)` — `docPath` `/match` | `rule(null, fn)` |
615
+ | `apply('$')` outside a body | `apply() spells $apply, which exists only inside a rule body (JSLT-FORMAT §6.1) — call it inside body()` | call it inside `body()` or a `rule()` callback |
616
+
617
+ The first is this pen's whole reason for existing beyond typing, and §6.1
618
+ tells the story with the run-time error beside it. The walk that finds it
619
+ descends the literal the callback returned — plain objects and arrays,
620
+ any depth — so `body((v) => ({ a: { b: [{ c: apply(v.x) }] } }))` refuses
621
+ with `docPath` `/a/b/0/c`, naming the exact member. §6.1 also names the
622
+ one place the walk does not reach.
623
+
624
+ The second is `JT0003` seen early. `{}` is a legal JSON object and an
625
+ illegal `match`, and the compiler would say so — but a stylesheet is
626
+ often assembled from a variable, and `rule(someMatch, fn)` with an empty
627
+ object is the shape that mistake takes.
628
+
629
+ The third is availability: `$apply` is injected into body compilation and
630
+ exists nowhere else (§6.1), so an `apply()` outside a capture has no
631
+ document to belong to.
632
+
633
+ ### 4.3 `JL0104` — the closed world of a body
634
+
635
+ A body may name `root`, `path` and its declared parameters. Nothing else,
636
+ because the engine binds nothing else — JSLT-FORMAT §8.1 and §8.2 are the
637
+ whole vocabulary, and a name outside it would compile and then fail at
638
+ run time as an unbound reference (`JQ2006`, wrapped as `JT2004`).
639
+
640
+ | The spelling that trips it | The message | The spelling that works |
641
+ |---|---|---|
642
+ | `body((v, x) => v.mul(x.rate))` | `a body() rule cannot bind 'rate' — its query evaluates with exactly 2 externals, 'root' and 'path'; anything else has nothing to bind to — a stylesheet parameter is declared first: body(fn, { externals: ['rate'] })` | declare it: `body(fn, { externals: ['rate'] })` |
643
+ | `body((v, x) => x.limit, { externals: ['rate'] })` | `a body() rule cannot bind 'limit' — its query evaluates with exactly 3 externals, 'root' and 'path' and 'rate'; anything else has nothing to bind to — a stylesheet parameter is declared first: body(fn, { externals: ['limit'] })` | add it: `{ externals: ['rate', 'limit'] }` |
644
+ | `body(fn, { externals: ['root'] })`, `['path']` | `'root' is engine-bound on every dispatch (JSLT-FORMAT §8.2) — it needs no declaration and is always present on the externals argument` | drop the declaration; read `x.root` directly |
645
+
646
+ The second message's advice names the ONE name that was missing, which is
647
+ the fix in a body that declared nothing else; a body that already
648
+ declares parameters adds to the list rather than replacing it, as the
649
+ "spelling that works" column says.
650
+
651
+ The third row is the interesting one. Declaring `root` is not harmless
652
+ and then ignored: §8.2 makes the two names reserved, and the engine
653
+ shadows any stylesheet parameter of the same name, so a body that
654
+ declared `root` and passed a value for it would silently read the
655
+ document root instead. Refusing the declaration is what keeps that from
656
+ being a run-time surprise.
657
+
658
+ ### 4.4 What carries a `docPath` and what does not
659
+
660
+ Where the pen knows the pointer of the node it is assembling, the
661
+ refusal carries it: every `match`, `mode`, `priority`, `modes` and
662
+ `rules` row above. Where the value crossed the JSON boundary
663
+ (`requireJson`) the pointer is not passed through, so
664
+ `rule({ schema: new Date(0) }, fn)` names the method in its message and
665
+ carries no `docPath` — the message is still unambiguous, and the
666
+ convention across the family is that a row states the pointer only when
667
+ one exists.
668
+
669
+ ## 5. The types
670
+
671
+ The declarations are `packages/linq/types/jslt.d.ts` (193 lines), and
672
+ every claim below is pinned at compile level in
673
+ `test/consumer/linq-jslt.ts` with a runtime twin in
674
+ `test/linq/jslt-pen.test.js`. This subpath exports **no builder class, no
675
+ constant and no type guard** — the three kinds the mapping table excludes
676
+ (LINQ-FORMAT's D5 reading) are empty here, so §2 names the whole runtime
677
+ surface and this section is about the compile-time one. The shapes below
678
+ are §3.2's book and its chapters, declared.
679
+
680
+ ```ts
681
+ import { stylesheet, rule, body, apply } from '@jarenjs/linq/jslt';
682
+ import type { Externals, Input, Output, Rule, Stylesheet, BodyDocument } from '@jarenjs/linq/jslt';
683
+ import type { Expr } from '@jarenjs/linq';
684
+ import * as s from '@jarenjs/linq/schema';
685
+
686
+ interface Chapter { heading: string }
687
+ interface Book { isbn: string; title: string; chapters: Chapter[] }
688
+ interface Item { sku: string; price: number }
689
+
690
+ const BookSchema = s.object({ isbn: s.string(), title: s.string() });
691
+
692
+ const chapter = rule({ schema: s.object({ heading: s.string() }) }, (v) => ({ name: v.heading }));
693
+ // ^ Rule<{ heading: string }, { name: string }> — the builder types v
694
+ const book = body((v: Expr<Book>) => ({ title: v.title, children: [apply(v.chapters.all())] }));
695
+ // ^ BodyDocument<Book, { title: string; children: unknown[] }> — a dispatch is unknown
696
+ const priced = body(
697
+ (v: Expr<Item>, x: Externals<{ rate: number }>) => ({ amount: v.price.mul(x.rate) }),
698
+ { externals: ['rate'] }); // x.limit does not compile
699
+
700
+ const sheet = stylesheet([rule(null, book), chapter]);
701
+ type Out = Output<typeof sheet>; // the FIRST rule's: { title: string; children: unknown[] }
702
+ const typed = stylesheet<Book, { title: string; children: { name: string }[] }>([rule(null, book), chapter]);
703
+ ```
704
+
705
+ ### 5.1 The phantoms, and where each side comes from
706
+
707
+ `BodyDocument<In, Out>`, `Rule<In, Out>` and `Stylesheet<In, Out>` all
708
+ carry two phantom members — `__in` and `__out`, declared and never
709
+ present at runtime — and `Input<>`/`Output<>` read them back. The
710
+ question a reader has is where each side gets its value.
711
+
712
+ | | `In` — what the body was written over | `Out` — what it produces |
713
+ |---|---|---|
714
+ | `body(fn)` | the annotation on the first argument (`(v: Expr<Book>) => …`); `unknown` without one | `Unwrap<>` of the callback's return |
715
+ | `rule(match, fn)` | the `schema` match's `Infer<>` when it is a schema-pen builder; the first argument's annotation otherwise; `unknown` for a path match alone | `Unwrap<>` of the callback's return |
716
+ | `rule(match, bodyDoc)` | the body document's `In`, or the match's when the body is untyped | the body document's `Out` |
717
+ | `rule(match, json)` | `unknown` | `unknown` |
718
+ | `stylesheet(rules)` | the FIRST rule's `In` | the FIRST rule's `Out` |
719
+ | `stylesheet<In, Out>(rules)` | the author's | the author's |
720
+
721
+ Two consequences worth stating rather than discovering:
722
+
723
+ - **Write the root rule first.** A stylesheet's phantoms are its first
724
+ rule's, which is a convention rather than an inference — Appendix A
725
+ writes the root rule first in every fixture, and this pen types on that
726
+ assumption. A stylesheet whose first rule is a leaf rule types as that
727
+ leaf.
728
+ - **Every `apply()` is `unknown`, and it has to be.** A dispatch lands on
729
+ whichever rule wins at run time, over a value the type system never
730
+ sees; `[apply(…)]` therefore unwraps to `unknown[]`, and
731
+ `apply(…).count()` to `number` (the operator is typed, its input is
732
+ not). Where the author knows the answer — a chapter always renders as
733
+ `{ name }` — the author states it with `stylesheet<In, Out>(…)`, and
734
+ the pin file holds that annotation as the way to say so.
735
+
736
+ The built-in rule's rebuilds (`share`/`fresh` around an unmatched
737
+ container, §5) are not typed at all: a stylesheet whose root is unmatched
738
+ is `Stylesheet<unknown, unknown>` unless annotated.
739
+
740
+ ### 5.2 `Externals<X, Root>` — a second argument that answers by name
741
+
742
+ `Externals` is the type of `body()`'s second parameter and it is built
743
+ from two halves: `root` and `path`, always present, and a mapped type
744
+ over the declared names.
745
+
746
+ ```ts
747
+ // root types by the second parameter of Externals<>; path is always a string expression
748
+ void body((v: Expr<Item>, x: Externals<{}, Book>) => ({ isbn: x.root.isbn.upper(), me: v.sku }));
749
+ // a declared name types through the annotation; the declaration and the annotation must agree
750
+ const p = body((v: Expr<Item>, x: Externals<{ rate: number }>) => v.price.mul(x.rate), { externals: ['rate'] });
751
+ // @ts-expect-error — the annotation names 'rate' and the declaration says 'limit'
752
+ void body((v: Expr<Item>, x: Externals<{ rate: number }>) => x.rate, { externals: ['limit'] });
753
+ // @ts-expect-error — a typed parameter takes its own kind
754
+ void body((v: Expr<Item>, x: Externals<{ rate: number }>) => x.rate.upper(), { externals: ['rate'] });
755
+ ```
756
+
757
+ Without an annotation a declared name is still reachable and is
758
+ `UnknownExpr` — the honest top — so `{ externals: ['rate'] }` alone buys
759
+ the runtime check without the type. Undeclared names are a compile error
760
+ whether or not `x` is annotated, which is the same shape the chain's
761
+ `params()` has and the reason the pin file checks both directions.
762
+
763
+ ### 5.3 What the pins hold
764
+
765
+ `test/consumer/linq-jslt.ts` (117 lines) is the compile-level record, and
766
+ it is worth knowing what it asserts because a claim not in it is a claim
767
+ this document should not make. It pins, with `Equals<A, B>` — identity in
768
+ both directions, never assignability — the `In`/`Out` of an annotated
769
+ body, an unannotated body, a scalar body, a schema-matched rule, an
770
+ annotated callback rule, a path-only rule, a rule built from a body
771
+ document, and a stylesheet in both its inferred and annotated forms. It
772
+ pins eleven negatives with `@ts-expect-error`, each of which FAILS the
773
+ build if it ever starts compiling: an undeclared external, an annotation
774
+ that disagrees with its declaration, an external used at the wrong kind,
775
+ a misspelled member on an annotated value, a misspelled member under a
776
+ schema match, a non-string mode, a non-number priority, an unknown
777
+ disposition, an operator name without `$`, an expression where a mode
778
+ string belongs, and a stylesheet asserted at an output its rule
779
+ contradicts.
780
+
781
+ ## 6. What it cannot spell
782
+
783
+ Four things, and only one of them is a limit of the FORMAT. The other
784
+ three are the pen refusing a spelling that would compile and then behave
785
+ wrongly, or a route the pen does not offer.
786
+
787
+ ### 6.1 The `[]` idiom — the refusal, and the run-time failure it prevents
788
+
789
+ This is the trap JSLT-FORMAT §6.3 names as "the one trap every author
790
+ hits", and it is the only place the pen refuses something the compiler
791
+ accepts.
792
+
793
+ An `$apply` yields a SEQUENCE. An object member holds exactly one value
794
+ (QUERY-FORMAT §3.1). So:
795
+
796
+ ```js
797
+ // refused at build time — JL0102, docPath /children
798
+ rule('$', (v) => ({ title: v.title, children: apply(v.chapters.all()) }))
799
+
800
+ // the spelling that works — the brackets splice the sequence into one array
801
+ rule('$', (v) => ({ title: v.title, children: [apply(v.chapters.all())] }))
802
+ ```
803
+
804
+ Write the refused spelling by hand — as a JSON rule document, which the
805
+ pen carries verbatim — and the engine compiles it happily. Over a book
806
+ with ONE chapter it even works, answering
807
+ `{ title: 'T', children: 1 }`; over a book with two it throws
808
+ `JT2004` wrapping `JQ2001`, "member 'children' evaluated to 2 items; an
809
+ object member takes exactly one". That is the failure mode this refusal
810
+ exists for: a transform that passes its first test and breaks on real
811
+ data.
812
+
813
+ **The check runs over the emitted document, not over what the callback
814
+ returned, and the difference matters.** An object literal handed to an
815
+ OPERATOR is lowered while the callback is still running —
816
+ `op('$if', [v.flag, { children: apply(v.chapters.all()) }, null])` — so by
817
+ the time the callback returns there is no marker left to find, only the
818
+ node it left behind in the document. Walking the document catches both
819
+ routes with one rule, and it reads member position exactly as the capture
820
+ wrote it: a map constructor has no `$`-prefixed key, an operator or FLWOR
821
+ phrase does, and `$map` — the phrase a `$`-keyed data object is spelled
822
+ as — carries its members as `[key, value]` pairs. So all four of these are
823
+ `JL0102`, at the pointer of the offending member:
824
+
825
+ | The spelling | `docPath` |
826
+ |---|---|
827
+ | `({ children: apply(v.x) })` | `/children` |
828
+ | `({ a: { b: [{ c: apply(v.x) }] } })` | `/a/b/0/c` |
829
+ | `({ out: op('$if', [v.flag, { children: apply(v.x) }, null]) })` | `/out/$if/1/children` |
830
+ | `({ $weird: apply(v.x) })` | `/$map/0/1` |
831
+
832
+ An `apply()` in an OPERAND position is not a member and stays legal:
833
+ `apply(v.items.all()).count()` is `{ "$count": { "$apply": … } }`, and
834
+ `[apply(…)]` is the idiom itself. A hand-written body is untouched by any
835
+ of this — the pen judges what it captured, and a rule document handed to
836
+ `rule()` rides verbatim (§2).
837
+
838
+ ### 6.2 A `match` of `{}`
839
+
840
+ The format has two ways to say "match everything": a path of `'$'`
841
+ (positional, the document root only) and no `match` at all (the
842
+ unconditional rule, which reaches every dispatched value). It has no way
843
+ to say it with an empty object, and the compiler's `JT0003` is what would
844
+ otherwise say so. `rule(null, fn)` is the spelling; `rule('$', fn)` is
845
+ the other one and means something different.
846
+
847
+ ### 6.3 `apply()` outside a body
848
+
849
+ `$apply` is not part of the query language. The stylesheet compiler
850
+ injects it into each body compile (§6.1), and `compileJsonQuery` rejects
851
+ it as `JQ0002`. So there is no way to build a fragment containing an
852
+ `apply()` outside a `body()` and splice it in later — the marker the pen
853
+ uses to find bare applies is created by the capture and only lives inside
854
+ one. Build the fragment as a callback and pass it where a body is taken,
855
+ or write the `{ $apply: … }` document by hand and accept that the `[]`
856
+ check will not see it.
857
+
858
+ ### 6.4 `from()` has no extensions pass-through — the one that surprises
859
+
860
+ This is not a limit of the stylesheet format at all, and it is here
861
+ because it is where readers of this document look for it. `op()` lifts an
862
+ operator into whatever capture is in progress, so it works in a chain
863
+ callback as well as in a body:
864
+
865
+ ```js
866
+ from([{ rate: 0.1, cashflows: [-100, 60, 60] }])
867
+ .select((r) => ({ v: op('$npv', [r.rate, r.cashflows.all()]) }))
868
+ ```
869
+
870
+ The DOCUMENT this records is correct and identical to the one a body
871
+ would record. But the chain's in-memory evaluation carries no operator
872
+ registry, so `explain()` and `toArray()` both raise the engine's
873
+ `JQ0002` — "unknown operator '$npv'" — with `docPath` `/$return/v`. A
874
+ registered operator is reachable from a chain only where the chain runs
875
+ against a provider that was given the registry, or from a stylesheet
876
+ compiled through one (`createJsltRegistry().use(financePack)`, §3.5).
877
+ `test/linq/jslt-pen.test.js` pins both directions.
878
+
879
+ ### 6.5 Everything else is the compiler's, by design
880
+
881
+ For completeness, because the boundary is what LINQ-FORMAT §1.1 rule 1
882
+ asks each pen document to state plainly. The pen does NOT check: whether
883
+ a `match` path parses (`JT0003`); whether a body's operators exist
884
+ (`JT0007` wrapping `JQ0002`); whether a `schema` match can be compiled
885
+ without the type-test hook (`JT0006`); whether a rule set recurses
886
+ without bottoming out (`JT2001`, the depth guard); whether an `$apply`
887
+ argument list has the right arity (`JQ0003` inside `JT0007`); or whether
888
+ two rules of equal priority conflict (§4 decides by document order and
889
+ never errors). Every one of those is built through the pen and asserted
890
+ at the engine's own code in `test/linq/jslt-pen.test.js` — the pen's
891
+ non-judgement is itself gated.
892
+
893
+ ### 6.6 When not to reach for this pen
894
+
895
+ - **The stylesheet is data.** A `$jslt` document loaded from a file,
896
+ authored by a model ([@jarenjs/ai](../../ai/README.md)'s authoring
897
+ profile writes them) or edited in the studio is a value;
898
+ `compileJsltStylesheet` takes it directly and nothing here has to be in
899
+ the path.
900
+ - **You are transforming one value, once, in JavaScript.** A stylesheet
901
+ is a document because it has to travel — into a migration step, into a
902
+ flow node, into an app's view, into a database. Code that runs in one
903
+ process and stays there should be a function: it can branch, loop and
904
+ call libraries, and §1.1's five consequences are five ways a callback
905
+ that looks like ordinary JavaScript is not.
906
+ - **The transform needs a condition JavaScript would express better.** A
907
+ body cannot use `if`, `&&`, `||` or a ternary — §1.1 rule 2 says what
908
+ they do instead, and it is silent and wrong. A transform whose shape is
909
+ mostly conditional is a set of rules with narrower `match` paths, or it
910
+ is not a stylesheet.
911
+ - **You need an operator the query language does not have.** `op()`
912
+ reaches a REGISTERED operator, so the escape exists — but registering
913
+ one is a decision about the engine everywhere it runs, not a local
914
+ convenience, and a transform that needs three of them is a program.
915
+ - **The output is not JSON.** A stylesheet's result is a JSON value. Text,
916
+ bytes and streams are somebody else's job — a `jaren-vnode` tree
917
+ (§3.7) is JSON and renders to HTML downstream, which is the pattern to
918
+ copy rather than the exception to it.
919
+
920
+ ## 7. Cost
921
+
922
+ `@jarenjs/linq/jslt` builds to **<!--fact:bundle.jslt-->19,124<!--/fact--> bytes** as a minified,
923
+ tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
924
+ measures and `npm run test:tree-shaking` reports, published rounded
925
+ (<!--fact:bundle.jslt.kb-->19<!--/fact--> kB) beside the other nine subpath prices in
926
+ [docs/CONSUMING.md](../../../docs/CONSUMING.md).
927
+
928
+ The probe is a gate, not a report. Building a stylesheet as a consumer
929
+ would, it asserts that the bundle carries:
930
+
931
+ - **no engine** — not one byte of `@jarenjs/json`, `@jarenjs/validate`,
932
+ `@jarenjs/emit`, `@jarenjs/db`, `@jarenjs/formats` or `@jarenjs/refs`,
933
+ which is what makes the "the pen imports no engine" rule a measurement
934
+ rather than a promise;
935
+ - **no chain module** — none of `sequence.js`, `document.js`, `async.js`,
936
+ `concurrency.js`, `provider.js`, `sources.js` or `schema-of.js`;
937
+ - **of the schema pen, only `brand.js`** — the builder brand, which
938
+ `rule()` needs to tell a `schema` match's builder from a hand-written
939
+ schema;
940
+ - **a ceiling** of 20,000 bytes; and the other direction, that neither the
941
+ chain's bundle nor the schema pen's carries a byte of
942
+ `packages/linq/src/jslt/`.
943
+
944
+ What it does carry is the recording proxy (`expression.js`), the shared
945
+ root capture (`capture-root.js`), the JSON boundary
946
+ (`json-boundary.js`), and this pen's own three files — 414 lines of
947
+ source. That makes it the SMALLEST of the nine pen bundles, and the
948
+ reason is that a stylesheet is mostly bodies, and a body is the shared
949
+ machine every pen already pays for.
950
+
951
+ Two figures worth reading beside it: `./migration` (<!--fact:bundle.migration-->23,599<!--/fact--> bytes)
952
+ carries this pen's `body()` and pays for it, which is why the two prices
953
+ sit so close; and `./flow` (<!--fact:bundle.flow-->19,181<!--/fact--> bytes) is within 60 bytes of this one
954
+ despite writing two formats, because it shares the same capture and adds
955
+ almost nothing but member checks and their messages.