@jarenjs/json 0.9.2

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 (57) hide show
  1. package/ARCHITECTURE.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +471 -0
  4. package/dist/types/basic.d.ts +32 -0
  5. package/dist/types/index.d.ts +4 -0
  6. package/dist/types/jslt/dispatch.d.ts +11 -0
  7. package/dist/types/jslt/errors.d.ts +18 -0
  8. package/dist/types/jslt/index.d.ts +53 -0
  9. package/dist/types/jslt/stylesheet.d.ts +8 -0
  10. package/dist/types/jtlt/desugar.d.ts +19 -0
  11. package/dist/types/jtlt/errors.d.ts +18 -0
  12. package/dist/types/jtlt/index.d.ts +57 -0
  13. package/dist/types/jtlt/template.d.ts +8 -0
  14. package/dist/types/jtlt/writer.d.ts +6 -0
  15. package/dist/types/path.d.ts +235 -0
  16. package/dist/types/pointer.d.ts +114 -0
  17. package/dist/types/query/compile.d.ts +21 -0
  18. package/dist/types/query/errors.d.ts +18 -0
  19. package/dist/types/query/index.d.ts +70 -0
  20. package/dist/types/query/normalize.d.ts +68 -0
  21. package/dist/types/query/operators.d.ts +424 -0
  22. package/dist/types/query/runtime.d.ts +93 -0
  23. package/dist/types/segments.d.ts +62 -0
  24. package/dist/types/xquery/index.d.ts +19 -0
  25. package/dist/types/xquery/parse.d.ts +20 -0
  26. package/docs/JSLT-FORMAT.md +861 -0
  27. package/docs/JSLT-PRELUDE.md +159 -0
  28. package/docs/JTLT-FORMAT.md +659 -0
  29. package/docs/QUERY-FORMAT.md +1221 -0
  30. package/docs/XQUERY-FRONTEND.md +321 -0
  31. package/package.json +81 -0
  32. package/schemas/jaren-jslt.draft-07.schema.json +776 -0
  33. package/schemas/jaren-jslt.schema.json +776 -0
  34. package/schemas/jaren-query.draft-07.schema.json +613 -0
  35. package/schemas/jaren-query.schema.json +375 -0
  36. package/src/basic.js +300 -0
  37. package/src/index.js +4 -0
  38. package/src/jslt/dispatch.js +934 -0
  39. package/src/jslt/errors.js +34 -0
  40. package/src/jslt/index.js +121 -0
  41. package/src/jslt/stylesheet.js +234 -0
  42. package/src/jtlt/desugar.js +231 -0
  43. package/src/jtlt/errors.js +34 -0
  44. package/src/jtlt/index.js +155 -0
  45. package/src/jtlt/template.js +130 -0
  46. package/src/jtlt/writer.js +110 -0
  47. package/src/path.js +977 -0
  48. package/src/pointer.js +453 -0
  49. package/src/query/compile.js +817 -0
  50. package/src/query/errors.js +33 -0
  51. package/src/query/index.js +150 -0
  52. package/src/query/normalize.js +1047 -0
  53. package/src/query/operators.js +1253 -0
  54. package/src/query/runtime.js +233 -0
  55. package/src/segments.js +627 -0
  56. package/src/xquery/index.js +35 -0
  57. package/src/xquery/parse.js +1647 -0
@@ -0,0 +1,861 @@
1
+ # The Jaren JSLT Format
2
+
3
+ **Version 0.1 — Specification**
4
+
5
+ Module: `@jarenjs/json/jslt`. This document is the language contract for the
6
+ Jaren JSLT template layer, the way [QUERY-FORMAT](./QUERY-FORMAT.md) is the
7
+ contract for the query engine it builds on. The design history lives in
8
+ [JSLT-PRELUDE](./JSLT-PRELUDE.md), which this specification supersedes.
9
+
10
+ > **Naming note (non-normative).** "JSLT" here names the Jaren template
11
+ > layer — *JSON stylesheet language for transformations*, the XSLT
12
+ > derivative of this stack. It is unrelated to Schibsted's JSLT language;
13
+ > final naming/branding is an open question and is deliberately not settled
14
+ > here, mirroring the query format's naming note.
15
+
16
+ ---
17
+
18
+ ## 1. Introduction
19
+
20
+ ### 1.1 What this language is
21
+
22
+ A JSLT **stylesheet** is a JSON document holding an ordered list of
23
+ **template rules**. Each rule says "when you meet a value shaped like this,
24
+ produce that", and a recursive dispatch engine — XSLT's `apply-templates`
25
+ idea — does the walking. XSLT's enduring pain is that "shaped like this"
26
+ (XPath patterns), "typed like this" (XML Schema), and the output vocabulary
27
+ are three disjoint languages bolted together. Here all three are
28
+ vocabularies this stack already compiles:
29
+
30
+ - **matching** — RFC 9535 JSONPath, already compiled by this package (§3);
31
+ - **typing** — JSON Schema, embedded through the same validator-agnostic
32
+ `compileTypeTest` hook the query engine uses (§9): a schema *is* a
33
+ pattern, with no schema-aware processor ceremony;
34
+ - **producing** — Jaren JSON Query documents (QUERY-FORMAT.md), whose
35
+ documents are JSON values the way XSLT stylesheets are XML documents.
36
+
37
+ The architecture pillars, stated once (non-normative but binding on the
38
+ reference implementation): **one stack, zero new dependencies**. JSLT adds
39
+ exactly **one operator** (`$apply`, §6) and a thin compiled dispatch
40
+ runtime. The layer lives at `packages/json/src/jslt/` — a module boundary
41
+ inside `@jarenjs/json`, like `xquery/` — NOT a new package: it needs the
42
+ query engine's internals (`normalizeQuery`, `compileNode`, the sequence
43
+ runtime), and a separate package would force those onto the public surface.
44
+ The standalone `@jarenjs/jslt` package sketched in the prelude (§7 there)
45
+ stays a roadmap note.
46
+
47
+ ### 1.2 Conformance and normative language
48
+
49
+ The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL
50
+ NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and
51
+ **OPTIONAL** in this document are to be interpreted as described in
52
+ RFC 2119.
53
+
54
+ - A **producer** emits stylesheet documents and MUST emit documents valid
55
+ per this specification.
56
+ - A **consumer** (stylesheet compiler + transformation engine) MUST accept
57
+ every valid stylesheet, MUST reject invalid ones with the compile errors
58
+ of §10, and MUST raise the runtime errors of §10 under the conditions
59
+ specified there.
60
+
61
+ Everything QUERY-FORMAT.md specifies for query documents applies unchanged
62
+ to rule bodies except where this document says otherwise; the only body
63
+ extension is `$apply` (§6).
64
+
65
+ ### 1.3 Terminology
66
+
67
+ - **Stylesheet** — the top-level JSON document handed to the compiler (§2).
68
+ - **Rule** — one template rule object (§2.2).
69
+ - **Body** — a rule's output expression: a Jaren JSON Query document
70
+ extended with `$apply`.
71
+ - **Dispatch** — finding and firing the winning rule for one value (§5).
72
+ - **Mode** — a named partition of the rule set (§7).
73
+ - **Location** — the normalized path (RFC 9535 §2.7) of a value inside the
74
+ input document, when it has one (§3.2, §6).
75
+ - **Disposition** — a mode's built-in behavior for unmatched values (§5).
76
+
77
+ ### 1.4 Changes from the prelude
78
+
79
+ JSLT-PRELUDE.md declared its spellings provisional; these are the final
80
+ calls, listed here so readers of the prelude are not misled:
81
+
82
+ 1. **`$schema-match` is gone.** A schema match is the `"schema"` member of
83
+ the `match` object (§3.1); position and shape conditions are two members
84
+ of one object, not two wrapper vocabularies.
85
+ 2. **The stylesheet envelope.** Besides the prelude's bare rule array, an
86
+ object form `{"$jslt": "0.1", "rules": [...]}` carries the version and
87
+ the stylesheet-level options `unmatched` and `modes` (§2.1).
88
+ 3. **Dispositions are `share` / `fresh` / `error`** under the `unmatched`
89
+ member (§5), replacing the prelude's deep-copy/shallow-copy pair. There
90
+ is **no XSLT-style deep-copy default** — see §5.2 for the rationale;
91
+ XSLT's literal deep-copy remains expressible per rule as
92
+ `{"match": m, "body": "$"}`.
93
+ 4. **Conflict resolution is fixed and small** (§4): explicit `priority`,
94
+ then three default priorities, then document order with later rules
95
+ winning. The prelude's schema-specificity metric (const > enum > type…)
96
+ is explicitly a non-goal of 0.1 and stays on the roadmap.
97
+ 5. **The `[]` idiom.** The prelude's §2 sketch wrote
98
+ `"children": {"$apply": "$.chapters[*]"}` — that bare member form is a
99
+ runtime error (JQ2001) as soon as a node has two or more chapters. The
100
+ correct idiom is `"children": [{"$apply": "$.chapters[*]"}]`, with the
101
+ array-constructor brackets (§6.3).
102
+ 6. **`$apply`'s mode argument** is spelled as the argument-list form
103
+ `{"$apply": [selector, mode]}` (§6.1), and an omitted mode defaults to
104
+ the **rule's own mode**, not XSLT 1.0's unnamed mode (§6.5).
105
+ 7. **Vocabulary placement is decided**: `$apply` lives in the `jslt`
106
+ module, injected per rule body by the stylesheet compiler. The core
107
+ query vocabulary and the published query-format schema are unchanged
108
+ (§6.1).
109
+ 8. **Reserved parameters.** `root` and `path` are engine-bound externals
110
+ available in every body (§8.2); the prelude had no parameter story.
111
+
112
+ ---
113
+
114
+ ## 2. The stylesheet document
115
+
116
+ ### 2.1 Top level
117
+
118
+ A stylesheet is either:
119
+
120
+ 1. a JSON **array of rules** (the shorthand form) — implies version `"0.1"`
121
+ and all defaults; or
122
+ 2. the **envelope object**:
123
+
124
+ ```json
125
+ { "$jslt": "0.1",
126
+ "rules": [],
127
+ "unmatched": "share",
128
+ "modes": { "toc": { "unmatched": "error" } } }
129
+ ```
130
+
131
+ `$jslt` and `rules` are REQUIRED (`rules` MUST be an array of rules);
132
+ `unmatched` and `modes` are OPTIONAL. Unknown envelope members are
133
+ compile error JT0001. A `$jslt` value other than the string `"0.1"` —
134
+ including non-string values — is compile error JT0004.
135
+
136
+ A stylesheet that is neither an array nor an object of the envelope shape
137
+ is JT0001.
138
+
139
+ - `"unmatched"`: one of `"share"` (the default), `"fresh"`, or `"error"` —
140
+ the built-in-rule disposition of §5; any other value is JT0001. The
141
+ envelope's `unmatched` sets the default for every mode.
142
+ - `"modes"`: an object `{ modeName: { "unmatched": disposition } }` of
143
+ per-mode overrides. Unknown members inside a mode object are JT0001.
144
+ Modes need NOT be declared to be used (§7); declaring one only overrides
145
+ its disposition.
146
+
147
+ ### 2.2 Rules
148
+
149
+ A **rule** is an object with the members
150
+
151
+ ```json
152
+ { "match": "$..price", "mode": "render", "priority": 2, "body": "$" }
153
+ ```
154
+
155
+ - `body` (REQUIRED) — a Jaren JSON Query document per QUERY-FORMAT.md,
156
+ extended with the `$apply` operator (§6). Everything the query format
157
+ specifies — encoding rules, FLWOR, operators, schema operators, errors —
158
+ applies inside a body verbatim.
159
+ - `match` (OPTIONAL) — what the rule fires on (§3). A rule with no `match`
160
+ member matches every value (an **unconditional rule**).
161
+ - `mode` (OPTIONAL) — a string naming the rule's mode (§7); default is the
162
+ unnamed mode `""`. A non-string `mode` is JT0002.
163
+ - `priority` (OPTIONAL) — any JSON number, for explicit conflict resolution
164
+ (§4). A non-number `priority` is JT0002.
165
+
166
+ A rule that is not an object, or that lacks `body`, is JT0002. Unknown rule
167
+ members are JT0002 — the vocabulary is **closed**, the same culture as the
168
+ query format: no silent annotations.
169
+
170
+ ---
171
+
172
+ ## 3. Matching
173
+
174
+ ### 3.1 The `match` member
175
+
176
+ `match` is either an RFC 9535 JSONPath query string, or an object with **at
177
+ least one** of the members `"path"` and `"schema"` and **no other members**
178
+ (violations are JT0003):
179
+
180
+ ```json
181
+ { "match": { "path": "$.store.book[*]",
182
+ "schema": { "type": "object", "required": ["isbn"] } } }
183
+ ```
184
+
185
+ - A bare string is shorthand for `{"path": string}`.
186
+ - `"path"` MUST be a valid RFC 9535 query string; a syntactically invalid
187
+ path is JT0003 (with the `JSONPathSyntaxError` as `cause`).
188
+ - `"schema"` is a **JSON Schema literal**, taken verbatim per the
189
+ schema-literal rules of QUERY-FORMAT §8.11: never evaluated as a query
190
+ expression, deep-copied and frozen, compiled exactly once at stylesheet
191
+ compile time by the `compileTypeTest` hook (§9).
192
+ - An empty object `{}` is JT0003 (it would match nothing meaningfully and
193
+ is always a mistake; write no `match` member for an unconditional rule).
194
+
195
+ When both `path` and `schema` are present, **both** conditions MUST hold
196
+ for the rule to match.
197
+
198
+ ### 3.2 Path matching is positional
199
+
200
+ A value matches a `"path"` condition **iff it sits at a location the path
201
+ selects from the input document root**. The path is a plain RFC 9535 query
202
+ over the input document: filters inside it see `$` as the **input root**
203
+ (not the candidate value), exactly as in any absolute path.
204
+
205
+ Normative consequence: values that have **no location** — values computed
206
+ by a rule body, or selected by a non-path `$apply` selector (§6.4) — can
207
+ NEVER match a path condition. Only `schema` conditions and unconditional
208
+ rules apply to them.
209
+
210
+ ```json
211
+ { "match": "$..price", "body": { "$mul": ["$", 1.21] } }
212
+ ```
213
+
214
+ matches every value sitting at a `price` member anywhere in the input — and
215
+ does not match the number `10` produced by another rule's body, however
216
+ price-like it looks.
217
+
218
+ ### 3.3 Schema matching is shape-based
219
+
220
+ A value matches a `"schema"` condition **iff it satisfies the schema**,
221
+ tested by the compiled predicate the `compileTypeTest` hook returned — the
222
+ same hook, contract, and per-item semantics as QUERY-FORMAT §8.11. Shape
223
+ matching needs no location: it applies to every dispatched value, wherever
224
+ it came from.
225
+
226
+ ```json
227
+ { "match": { "schema": { "type": "object", "required": ["isbn"] } },
228
+ "body": { "title": "$.title" } }
229
+ ```
230
+
231
+ ### 3.4 Implementation note (non-normative)
232
+
233
+ The intended mechanism constrains the semantics, so it is recorded here:
234
+ the consumer evaluates all match paths against the input **once per
235
+ transformation** (lazily per mode), yielding the set of matched locations;
236
+ dispatching a located value tests set membership plus the schema
237
+ predicates. This is sound because documents are **immutable during a
238
+ transform**: rule bodies construct fresh values per the query engine's
239
+ constructor rules and never mutate the input, so no body ever observes
240
+ partial pre-pass state.
241
+
242
+ ---
243
+
244
+ ## 4. Conflict resolution
245
+
246
+ The whole algorithm:
247
+
248
+ 1. **Rank** = (`priority`, document order).
249
+ 2. `priority` is any JSON number. When absent, the **default priority** is:
250
+
251
+ | rule | default priority |
252
+ |---|---|
253
+ | both `path` and `schema` present | `1` |
254
+ | exactly one of `path` / `schema` | `0` |
255
+ | unconditional (no `match`) | `-1` |
256
+
257
+ 3. Higher priority wins. Ties break by document order with **later rules
258
+ winning** — user rules appended after a library's override it.
259
+ 4. The first match in rank order **fires**; there are no ambiguity errors
260
+ and no warnings.
261
+
262
+ Deep schema-specificity metrics (a `const` beating an `enum` beating a
263
+ `type`, …) are explicitly a **non-goal of 0.1**; they stay on the roadmap.
264
+ XSLT's default-priority table is the cautionary tale this section is
265
+ designed to beat by being small.
266
+
267
+ ```json
268
+ [ { "match": "$..price", "body": "$" },
269
+ { "match": "$..price", "body": { "$mul": ["$", 1.21] } } ]
270
+ ```
271
+
272
+ Both rules tie at priority `0`; the later one wins and every price is
273
+ multiplied.
274
+
275
+ ---
276
+
277
+ ## 5. Dispatch and the built-in rule
278
+
279
+ ### 5.1 Processing model
280
+
281
+ A **transformation** is the dispatch of the input document root in the
282
+ unnamed mode. **Dispatching** a value in a mode:
283
+
284
+ 1. Find the highest-ranking rule of the mode that matches the value (§3,
285
+ §4). If one fires, the dispatch result is its **body's result**, with
286
+ the matched value as the body's query input `$` (no context-item drift,
287
+ per the query format).
288
+ 2. If no rule fires, the **built-in rule** applies, per the mode's
289
+ `unmatched` disposition:
290
+
291
+ - **`"share"` (the default).** A scalar (string, number, boolean, `null`)
292
+ is returned **as-is**. A container is rebuilt member-by-member (object)
293
+ or element-by-element (array), **dispatching every child**; the rebuilt
294
+ container follows the query format's constructor rules: an object member
295
+ whose child dispatch is empty is **omitted**; a child dispatch of two or
296
+ more items for an object member is runtime error JT2002; array children
297
+ splice their result sequences flat. **When every child comes back
298
+ identical (`===`) and complete, the original container itself is
299
+ returned** — the output shares unmatched subtrees with the input. The
300
+ empty stylesheet is the identity transform, `===` included.
301
+ - **`"fresh"`.** The same semantics, but the built-in rule **always
302
+ returns the freshly rebuilt container**, never the original — callers
303
+ get a tree they may mutate without touching the input (the
304
+ forms/view-model case). Stated plainly: `fresh` governs **only built-in
305
+ rebuilds**; rule-body outputs are whatever the bodies return, and path
306
+ results inside bodies still share input subtrees (QUERY-FORMAT
307
+ behavior).
308
+ - **`"error"`.** An unmatched value is runtime error JT2003, naming the
309
+ value's location when it has one — the exhaustive-dispatch style.
310
+
311
+ ### 5.2 No deep-copy default (divergence from the prelude)
312
+
313
+ JSLT has **no XSLT deep-copy default**, deliberately. Under XSLT 3.0
314
+ `on-no-match="deep-copy"` semantics, an unmatched root freezes the whole
315
+ document: a stylesheet whose only rule matches `$..price` would never fire
316
+ it, because the root is unmatched and the deep copy stops all further
317
+ matching. That contradicts the surgical-override promise that motivates
318
+ template engines — rules must apply at every depth.
319
+
320
+ In JSLT, rules conceptually apply to **every value**; the `unmatched`
321
+ disposition only chooses what happens **between** matches. XSLT's literal
322
+ deep-copy — copy this subtree, stop matching inside it — remains
323
+ expressible per rule:
324
+
325
+ ```json
326
+ { "match": "$.assets", "body": "$" }
327
+ ```
328
+
329
+ ### 5.3 Sharing is normative
330
+
331
+ In `share` mode — and in rule bodies generally, since path results share
332
+ input subtrees per QUERY-FORMAT — **the output may alias input objects and
333
+ arrays; mutating the output mutates the input.** Consumers MUST implement
334
+ the `===` sharing of §5.1, and callers who intend to mutate the result MUST
335
+ use `fresh` (or copy). This is documented loudly on purpose: it is the
336
+ number-one operational surprise of value-sharing template engines.
337
+
338
+ ### 5.4 Recursion and the depth guard
339
+
340
+ Recursion happens ONLY through `$apply` (§6) and the built-in rule's child
341
+ dispatches. A consumer MUST enforce a dispatch depth guard: the compile
342
+ option `maxDepth` (default `1024`) bounds the dispatch nesting depth, and
343
+ exceeding it is runtime error JT2001 — the self-application loop
344
+
345
+ ```json
346
+ { "rules": [ { "body": { "$apply": ["$"] } } ], "$jslt": "0.1" }
347
+ ```
348
+
349
+ MUST die with JT2001, not a stack overflow.
350
+
351
+ ---
352
+
353
+ ## 6. `$apply` — the apply-templates operator
354
+
355
+ ### 6.1 Availability
356
+
357
+ `$apply` is available ONLY inside rule bodies: the stylesheet compiler
358
+ injects it into each body compile. The query format's vocabulary is
359
+ **unchanged** and its published schema untouched — plain `compileJsonQuery`
360
+ documents still reject `$apply` as JQ0002.
361
+
362
+ ### 6.2 Value forms
363
+
364
+ Following the `$orderby` disambiguation precedent of the query format
365
+ (QUERY-FORMAT §6.6, "an array value is always a list"):
366
+
367
+ - A **non-array** value is the selector expression; dispatch happens in the
368
+ **rule's own mode**:
369
+
370
+ ```json
371
+ { "$apply": "$.chapters[*]" }
372
+ ```
373
+
374
+ - An **array** value is ALWAYS the argument-list form `[selector]` or
375
+ `[selector, mode]`, where `mode` is a **literal JSON string** naming the
376
+ target mode (`""` names the unnamed mode). The mode is not an
377
+ expression — dynamic mode selection is a non-goal of 0.1:
378
+
379
+ ```json
380
+ { "$apply": ["$.sections[*]", "toc"] }
381
+ ```
382
+
383
+ - To apply templates to a **constructed array**, write the selector
384
+ explicitly: `{"$apply": [{"$const": [1, 2]}]}` or
385
+ `{"$apply": [{"$seq": [1, 2]}]}` — a bare array value never means "apply
386
+ to this array".
387
+
388
+ Wrong shapes — an empty or overlong argument list, a non-string mode — are
389
+ JQ0003 inside the body, surfacing as JT0007 at the stylesheet level (§10).
390
+
391
+ ### 6.3 Semantics
392
+
393
+ Evaluate the selector against the current input `$`; for each item of the
394
+ result, **in order**, dispatch it (§5) in the target mode; concatenate the
395
+ dispatch results into one sequence. An empty selector result is the empty
396
+ sequence.
397
+
398
+ `$apply` is an **ordinary operator** with respect to the query format: its
399
+ result composes under the constructor rules like any other expression.
400
+
401
+ > **The `[]` idiom — the one trap every author hits.** An object member
402
+ > holds exactly one value, so the array-of-children idiom is
403
+ >
404
+ > ```json
405
+ > { "title": "$.title", "children": [ { "$apply": "$.chapters[*]" } ] }
406
+ > ```
407
+ >
408
+ > — **with** the array-constructor brackets, which splice the sequence into
409
+ > one array. The bare member form
410
+ > `"children": { "$apply": "$.chapters[*]" }` assigns the sequence itself
411
+ > to the member and is runtime error **JQ2001** the moment a node has two
412
+ > or more chapters (an object member takes exactly one value,
413
+ > QUERY-FORMAT §3.1). The prelude's sketch got this wrong; see §1.4.
414
+
415
+ ### 6.4 Location propagation
416
+
417
+ Items selected by a selector that is a **path rooted at `$`** (the current
418
+ value) **or at `$root`** (§8.2) carry locations: the current value's (or
419
+ the root's) location extended with the path's normalized-path suffix. Such
420
+ items can match path rules (§3.2). Items produced any other way — FLWOR
421
+ phrases, operators, variable-rooted paths on other variables, literals —
422
+ are **location-less**: only schema and unconditional rules can match them.
423
+ The dispatch of the input root carries location `$`.
424
+
425
+ ### 6.5 Mode dispatch defaults
426
+
427
+ An omitted mode means the **rule's own mode** — a static fact, since each
428
+ rule belongs to exactly one mode (§7). This deviates from XSLT 1.0, whose
429
+ modeless `apply-templates` always targets the unnamed mode; recursive walks
430
+ *within* a mode are the dominant pattern, and XSLT 3.0 grew
431
+ `mode="#current"` for exactly this reason. JSLT makes the common thing the
432
+ default.
433
+
434
+ Applying into a mode that has no rules is legal — the mode's disposition
435
+ does all the work (§5). A rule's `mode` is a single string in 0.1; mode
436
+ lists are roadmap.
437
+
438
+ ---
439
+
440
+ ## 7. Modes
441
+
442
+ A rule's `mode` member (default: the unnamed mode `""`) partitions the rule
443
+ set. Each mode has its own ranked rule chain (§4), its own `unmatched`
444
+ disposition (§2.1, §5), and its own match pre-pass (§3.4). Modes are the
445
+ mechanism for walking the same input more than once with different
446
+ outputs — the same document rendered once as a table of contents and once
447
+ as body content is the canonical example (Appendix A.4).
448
+
449
+ Modes need not be declared: naming one in a rule's `mode` or an `$apply`
450
+ target creates it. The envelope's `modes` member (§2.1) exists only to
451
+ override a mode's disposition.
452
+
453
+ ---
454
+
455
+ ## 8. Externals and parameters
456
+
457
+ ### 8.1 Stylesheet parameters
458
+
459
+ Free variables in rule bodies are the stylesheet's **parameters**, exactly
460
+ like query externals (QUERY-FORMAT §9): use is the declaration, and the
461
+ caller binds them at transformation time —
462
+ `transform(data, { rate: 1.21 })`. Evaluating a reference to an unbound
463
+ parameter is JQ2006 inside the body, surfacing wrapped as JT2004 (§10).
464
+
465
+ ### 8.2 Reserved names: `root` and `path`
466
+
467
+ Two names are RESERVED and engine-bound per dispatch, shadowing any
468
+ caller-supplied binding of the same name:
469
+
470
+ - `root` — the input document root. `"$root"` and variable-rooted paths
471
+ like `"$root.currency"` read the whole input from any rule body.
472
+ - `path` — the current value's **normalized path** (RFC 9535 §2.7 string,
473
+ e.g. `"$['items'][0]['price']"`), or `null` for location-less values
474
+ (§6.4).
475
+
476
+ > **Contrast note.** The validator's `$query` keyword (the other direction
477
+ > of this stack: queries inside schemas) binds its `path` parameter as an
478
+ > **RFC 6901 JSON Pointer** — the schema-side convention. JSLT's matching
479
+ > language is JSONPath, so its `path` is an RFC 9535 **normalized path**.
480
+ > See the `@jarenjs/validate` README's "`$query` — cross-field assertions"
481
+ > section for the other half.
482
+
483
+ ### 8.3 `transform.externals`
484
+
485
+ The compiled transformation exposes `transform.externals`: the **user
486
+ parameters only** (reserved names excluded), as the union across all rule
487
+ bodies, in order of first appearance.
488
+
489
+ ---
490
+
491
+ ## 9. The type-test hook
492
+
493
+ ```
494
+ compileJsltStylesheet(doc, { compileTypeTest: (schemaJson, docPath) => (value => boolean) })
495
+ ```
496
+
497
+ One hook, three consumers: `schema` match conditions (§3.3), and the
498
+ `$valid`/`$assert` operators and `$as` clause inside rule bodies (the hook
499
+ is threaded through to every body compile). The contract, signature, and
500
+ once-per-literal-at-compile-time rule are exactly QUERY-FORMAT §8.11's;
501
+ `@jarenjs/validate/query`'s `createTypeTestCompiler` satisfies it, and the
502
+ `json` package never imports the validator — the dependency direction stays
503
+ validate → json.
504
+
505
+ A stylesheet using `schema` match conditions compiled **without** a hook is
506
+ compile error JT0006 (the mirror of JQ0008); a hook that rejects a match
507
+ schema (throws) is compile error JT0005 (the mirror of JQ0009). Inside
508
+ bodies, the query engine's own JQ0008/JQ0009 apply and surface as JT0007
509
+ (§10).
510
+
511
+ ---
512
+
513
+ ## 10. Errors
514
+
515
+ ### 10.1 Error objects
516
+
517
+ Consumers MUST raise compile-time errors as `JsltCompileError` and runtime
518
+ errors as `JsltRuntimeError`. Every error carries the same shape as the
519
+ query errors:
520
+
521
+ - `code` — a stable identifier from the registry below;
522
+ - `message` — human-readable, non-normative;
523
+ - `docPath` — an RFC 6901 JSON Pointer into the **stylesheet document**
524
+ (e.g. `/rules/2/match/path`);
525
+ - `cause` — the wrapped underlying error, where the registry says so.
526
+
527
+ In the bare-array shorthand (§2.1) the document has no `rules` member;
528
+ `docPath` pointers then start at the rule index (`/2/match/path`).
529
+
530
+ ### 10.2 Registry
531
+
532
+ | code | condition |
533
+ |---|---|
534
+ | JT0001 | stylesheet shape: not array/object, missing `rules`, unknown envelope/mode member, bad `unmatched` value |
535
+ | JT0002 | rule shape: not an object, missing `body`, unknown member, `mode`/`priority` of the wrong type |
536
+ | JT0003 | match: invalid shape/empty object, path not a valid RFC 9535 query (cause = `JSONPathSyntaxError`) |
537
+ | JT0004 | unknown `$jslt` version |
538
+ | JT0005 | `schema` match rejected by the hook (cause preserved) |
539
+ | JT0006 | `schema` match without a `compileTypeTest` hook |
540
+ | JT0007 | body failed to compile — wraps `JsonQueryCompileError`; `docPath` = `/rules/<i>/body` + the inner docPath, `code`/cause preserved on `cause` |
541
+ | JT2001 | dispatch depth exceeded `maxDepth` |
542
+ | JT2002 | built-in rebuild: an object member's child dispatch produced 2+ items (message carries the location + member name) |
543
+ | JT2003 | unmatched value under `"error"` disposition |
544
+ | JT2004 | rule body raised a runtime error — wraps `JsonQueryRuntimeError` (cause), message names the rule (`/rules/<i>`) and the node location when known; never double-wraps |
545
+
546
+ JT0xxx are compile errors (`JsltCompileError`), JT2xxx runtime errors
547
+ (`JsltRuntimeError`) — the same numbering convention as JQ0xxx/JQ2xxx.
548
+
549
+ ---
550
+
551
+ ## 11. Correspondence with XSLT (non-normative)
552
+
553
+ | XSLT | JSLT 0.1 |
554
+ |---|---|
555
+ | template rule (`xsl:template`) | rule object (§2.2) |
556
+ | `match` pattern (XPath) | `match.path` (RFC 9535 JSONPath, positional — §3.2) |
557
+ | schema-aware `type`/`element(*, T)` tests | `match.schema` (JSON Schema literal — §3.3) |
558
+ | `mode` | `mode` (§7) |
559
+ | `priority` + default-priority table | `priority` + the three-row default table (§4) |
560
+ | `xsl:apply-templates select="…" mode="…"` | `{"$apply": [selector, mode]}` (§6) |
561
+ | sequence constructor | rule body = Jaren JSON Query document |
562
+ | built-in rules / `on-no-match` | `unmatched` disposition per mode (§5) |
563
+ | `xsl:param` / `xsl:with-param` | stylesheet parameters = query externals (§8) |
564
+
565
+ Deliberate deviations, gathered:
566
+
567
+ 1. **Per-value semantics, not on-no-match copy modes** (§5.2): rules apply
568
+ at every depth; `unmatched` only picks the between-matches behavior.
569
+ There is no deep-copy default.
570
+ 2. **Static mode default** (§6.5): an omitted `$apply` mode targets the
571
+ rule's own mode (XSLT 3.0's `#current`), not the unnamed mode.
572
+ 3. **The `[]` idiom** (§6.3): sequences compose under JSON constructor
573
+ rules; there is no implicit "children become content" as in XML tree
574
+ construction.
575
+ 4. **No imports, no named templates, no stylesheet functions in 0.1** — all
576
+ roadmap. A stylesheet is one document; rule order and priority are the
577
+ only composition tools.
578
+ 5. **A schema is a pattern** (the prelude's thesis): where XSLT 2.0 needed
579
+ schema-aware processors and typed-value ceremony to let patterns see
580
+ types, here the match condition and the type condition are the same
581
+ kind of object, compiled by the same hook.
582
+
583
+ ---
584
+
585
+ ## 12. API note (non-normative)
586
+
587
+ ```
588
+ compileJsltStylesheet(doc, options) -> transform
589
+ transform(data, externals?) // plain JSON out
590
+ transform.externals // user parameter names (§8.3)
591
+ transform.doc // deeply frozen copy of the stylesheet
592
+ transformJson(stylesheet, data, externals?, options?) // one-shot, WeakMap-cached
593
+ ```
594
+
595
+ Module: `@jarenjs/json/jslt`. `transform` returns plain JSON with the query
596
+ API's sequence mapping: `undefined` for the empty sequence, the item itself
597
+ for a singleton, an array of items for a longer sequence. `options` carries
598
+ `compileTypeTest` (§9) and `maxDepth` (§5.4), both for
599
+ `compileJsltStylesheet` and as the optional fourth argument of
600
+ `transformJson`. The one-shot function is the counterpart of `queryJson`,
601
+ caching compiled stylesheets by document identity and compile-option
602
+ values in a WeakMap.
603
+
604
+ ---
605
+
606
+ ## Appendix A. Worked examples (normative fixtures)
607
+
608
+ Every example is complete and destined to run verbatim as engine tests and
609
+ schema fixtures. Unless noted, the disposition is the default `share` and
610
+ the mode is the unnamed mode.
611
+
612
+ ### A.1 The empty stylesheet is the identity transform
613
+
614
+ ```json
615
+ []
616
+ ```
617
+
618
+ Input:
619
+
620
+ ```json
621
+ { "store": { "book": [ { "title": "Sayings of the Century", "price": 8.95 } ] } }
622
+ ```
623
+
624
+ Output: the input document — not a copy: `output === input` (§5.1).
625
+
626
+ ### A.2 Surgical override — VAT on every price
627
+
628
+ ```json
629
+ [ { "match": "$..price", "body": { "$mul": ["$", 1.21] } } ]
630
+ ```
631
+
632
+ Input:
633
+
634
+ ```json
635
+ { "catalog": { "book": [ { "title": "A", "price": 10 }, { "title": "B", "price": 20 } ] },
636
+ "meta": { "publisher": { "name": "N" } } }
637
+ ```
638
+
639
+ Output:
640
+
641
+ ```json
642
+ { "catalog": { "book": [ { "title": "A", "price": 12.1 }, { "title": "B", "price": 24.2 } ] },
643
+ "meta": { "publisher": { "name": "N" } } }
644
+ ```
645
+
646
+ One rule, applied at every depth the path selects; every container on the
647
+ way is rebuilt by the built-in rule. Sharing is asserted: `output.meta ===
648
+ input.meta` — the untouched subtree is the input's own object (§5.1, §5.3).
649
+
650
+ ### A.3 The book example, done right
651
+
652
+ ```json
653
+ { "$jslt": "0.1",
654
+ "rules": [
655
+ { "match": { "schema": { "type": "object", "required": ["isbn"] } },
656
+ "body": { "title": "$.title",
657
+ "children": [ { "$apply": "$.chapters[*]" } ] } },
658
+ { "match": { "schema": { "type": "object", "required": ["heading"] } },
659
+ "body": { "name": "$.heading" } }
660
+ ] }
661
+ ```
662
+
663
+ Input:
664
+
665
+ ```json
666
+ { "isbn": "0-553-21311-3", "title": "Moby Dick",
667
+ "chapters": [ { "heading": "Loomings" }, { "heading": "The Carpet-Bag" } ] }
668
+ ```
669
+
670
+ Output:
671
+
672
+ ```json
673
+ { "title": "Moby Dick",
674
+ "children": [ { "name": "Loomings" }, { "name": "The Carpet-Bag" } ] }
675
+ ```
676
+
677
+ The schema match fires on the root (shape, not position); `$apply`
678
+ dispatches each chapter, and the chapter rule fires on shape again. Note
679
+ the `[]` around the `$apply` — without it, this stylesheet is JQ2001 at
680
+ runtime, because two chapter results cannot occupy one member (§6.3).
681
+
682
+ ### A.4 Two modes: table of contents + body rendering
683
+
684
+ ```json
685
+ { "$jslt": "0.1",
686
+ "rules": [
687
+ { "match": "$",
688
+ "body": { "toc": [ { "$apply": ["$.sections[*]", "toc"] } ],
689
+ "body": [ { "$apply": ["$.sections[*]", "render"] } ] } },
690
+ { "mode": "toc", "match": "$.sections[*]",
691
+ "body": { "ref": "$.id", "label": "$.heading" } },
692
+ { "mode": "render", "match": "$.sections[*]",
693
+ "body": { "anchor": "$.id", "heading": "$.heading", "text": "$.text" } }
694
+ ] }
695
+ ```
696
+
697
+ Input:
698
+
699
+ ```json
700
+ { "title": "Guide",
701
+ "sections": [
702
+ { "id": "intro", "heading": "Introduction", "text": "Start here." },
703
+ { "id": "usage", "heading": "Usage", "text": "Then this." }
704
+ ] }
705
+ ```
706
+
707
+ Output:
708
+
709
+ ```json
710
+ { "toc": [ { "ref": "intro", "label": "Introduction" },
711
+ { "ref": "usage", "label": "Usage" } ],
712
+ "body": [ { "anchor": "intro", "heading": "Introduction", "text": "Start here." },
713
+ { "anchor": "usage", "heading": "Usage", "text": "Then this." } ] }
714
+ ```
715
+
716
+ The same sections are walked twice — once per mode, each with its own rule
717
+ chain (§7). The selectors are `$`-rooted paths, so the section values carry
718
+ locations and match the path rules (§6.4).
719
+
720
+ ### A.5 `fresh` — an annotated copy the caller may mutate
721
+
722
+ ```json
723
+ { "$jslt": "0.1",
724
+ "unmatched": "fresh",
725
+ "rules": [
726
+ { "match": { "schema": { "type": "object", "required": ["price"] } },
727
+ "body": { "title": "$.title", "price": "$.price",
728
+ "taxed": { "$mul": ["$.price", 1.21] } } }
729
+ ] }
730
+ ```
731
+
732
+ Input:
733
+
734
+ ```json
735
+ { "products": [ { "title": "A", "price": 10 } ] }
736
+ ```
737
+
738
+ Output:
739
+
740
+ ```json
741
+ { "products": [ { "title": "A", "price": 10, "taxed": 12.1 } ] }
742
+ ```
743
+
744
+ The schema rule rebuilds each priced object with an extra member; the
745
+ `fresh` disposition makes the built-in rule rebuild the surrounding
746
+ containers too (`output !== input`, `output.products !== input.products`),
747
+ so the caller owns the result tree — the forms/view-model case (§5.1).
748
+
749
+ ### A.6 `error` — exhaustive dispatch with an explicit fallback
750
+
751
+ ```json
752
+ { "$jslt": "0.1",
753
+ "unmatched": "error",
754
+ "rules": [
755
+ { "match": "$", "body": [ { "$apply": "$.events[*]" } ] },
756
+ { "match": { "schema": { "type": "object", "required": ["error"] } },
757
+ "body": { "level": "fatal", "message": "$.error" } },
758
+ { "match": { "schema": { "type": "object", "required": ["info"] } },
759
+ "body": { "level": "note", "message": "$.info" } },
760
+ { "body": { "level": "unknown" } }
761
+ ] }
762
+ ```
763
+
764
+ Input:
765
+
766
+ ```json
767
+ { "events": [ { "info": "started" }, { "error": "disk full" }, { "beep": true } ] }
768
+ ```
769
+
770
+ Output:
771
+
772
+ ```json
773
+ [ { "level": "note", "message": "started" },
774
+ { "level": "fatal", "message": "disk full" },
775
+ { "level": "unknown" } ]
776
+ ```
777
+
778
+ Under `"error"`, nothing passes silently: remove the unconditional fallback
779
+ rule (default priority `-1`, §4) and the same input raises JT2003 at
780
+ `$['events'][2]`. With it, unexpected shapes flow to an explicit default.
781
+
782
+ ### A.7 Parameters and the reserved externals
783
+
784
+ ```json
785
+ [ { "match": "$..price",
786
+ "body": { "amount": { "$mul": ["$", "$rate"] },
787
+ "currency": "$root.currency",
788
+ "at": "$path" } } ]
789
+ ```
790
+
791
+ Called as `transform(input, { rate: 1.21 })`; `transform.externals` is
792
+ `["rate"]` — `root` and `path` are engine-bound and excluded (§8).
793
+
794
+ Input:
795
+
796
+ ```json
797
+ { "currency": "EUR", "items": [ { "sku": "a1", "price": 10 } ] }
798
+ ```
799
+
800
+ Output:
801
+
802
+ ```json
803
+ { "currency": "EUR",
804
+ "items": [ { "sku": "a1",
805
+ "price": { "amount": 12.1, "currency": "EUR",
806
+ "at": "$['items'][0]['price']" } } ] }
807
+ ```
808
+
809
+ `$rate` is a stylesheet parameter (§8.1); `$root.currency` reads the input
810
+ root from a deep rule; `$path` is the matched value's normalized path
811
+ (§8.2). Calling `transform(input)` without `rate` raises JT2004 wrapping
812
+ the body's JQ2006.
813
+
814
+ ---
815
+
816
+ ## Appendix B. LLM structured output (non-normative)
817
+
818
+ The complete stylesheet language is published as JSON Schema twins:
819
+
820
+ - [`../schemas/jaren-jslt.schema.json`](../schemas/jaren-jslt.schema.json) —
821
+ canonical draft 2020-12, `$id`
822
+ `https://jarenjs.dev/schemas/jaren-jslt/0.1`;
823
+ - [`../schemas/jaren-jslt.draft-07.schema.json`](../schemas/jaren-jslt.draft-07.schema.json) —
824
+ the mechanically derived draft-07 twin.
825
+
826
+ This extends the query format's
827
+ [structured-output story](./QUERY-FORMAT.md#appendix-b-llm-structured-output-non-normative)
828
+ to complete stylesheets. Constrained decoding against either artifact can
829
+ prevent unknown envelope/rule members, missing bodies, malformed match
830
+ objects, bad dispositions and versions, unknown body operators, and wrong
831
+ structurally expressible `$apply` arities before any compiler runs.
832
+
833
+ The rule-body grammar is not a hand-maintained copy. The canonical
834
+ stylesheet artifact deep-copies the committed query artifact's definition
835
+ map, adds the single body-local `applyPhrase`, and appends that phrase to
836
+ `objectExpression.oneOf`. Tests pin that derivation and separately pin the
837
+ canonical-to-draft-07 transform. A query-schema refactor therefore fails
838
+ the artifact test loudly instead of letting the stylesheet grammar drift;
839
+ the published query artifacts themselves remain unchanged.
840
+
841
+ As with generated queries, schema-valid does not mean semantically complete.
842
+ The compiler remains authoritative for rule ranking, reserved externals,
843
+ type-test hook availability and schema compilation, plus the positional
844
+ fact that the second item of `{"$apply": [selector, mode]}` is a literal
845
+ string. That last fact cannot be represented without tuple validation,
846
+ which the draft-neutral artifact policy deliberately excludes. The
847
+ remaining failures are the JT0xxx/JT2xxx errors of §10, carrying a
848
+ `docPath` into the stylesheet for a repair loop.
849
+
850
+ Provider "structured output" implementations also support different JSON
851
+ Schema subsets regardless of the draft they advertise. In particular,
852
+ recursive references, `patternProperties`, `propertyNames`, `format`, and
853
+ some composition keywords may be restricted or treated as annotations.
854
+ Always validate a generated stylesheet locally against the full artifact
855
+ before calling `compileJsltStylesheet`. A simplified lowest-common-
856
+ denominator LLM profile could trade precision for broader provider support,
857
+ but no such third artifact is defined in version 0.1.
858
+
859
+ Stylesheets remain ordinary JSON throughout the toolchain: they can be
860
+ function-call arguments, retrieved rule sets, reviewed diffs, audit-log
861
+ entries, and replayable transformation programs without a text parser.