@jarenjs/json 0.9.2 → 0.34.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 (74) hide show
  1. package/ARCHITECTURE.md +86 -13
  2. package/README.md +248 -23
  3. package/dist/types/canonical.d.ts +37 -0
  4. package/dist/types/cow.d.ts +28 -0
  5. package/dist/types/errors.d.ts +45 -0
  6. package/dist/types/index.d.ts +3 -0
  7. package/dist/types/jslt/errors.d.ts +15 -8
  8. package/dist/types/jslt/index.d.ts +22 -0
  9. package/dist/types/jslt/packs/finance.d.ts +119 -0
  10. package/dist/types/jslt/packs/index.d.ts +310 -0
  11. package/dist/types/jslt/packs/math.d.ts +159 -0
  12. package/dist/types/jslt/packs/stats.d.ts +48 -0
  13. package/dist/types/jslt/registry.d.ts +65 -0
  14. package/dist/types/jtlt/errors.d.ts +3 -6
  15. package/dist/types/option-variants.d.ts +29 -0
  16. package/dist/types/patch.d.ts +214 -0
  17. package/dist/types/path.d.ts +139 -9
  18. package/dist/types/pointer.d.ts +100 -9
  19. package/dist/types/query/compile.d.ts +12 -0
  20. package/dist/types/query/errors.d.ts +72 -8
  21. package/dist/types/query/index.d.ts +317 -25
  22. package/dist/types/query/normalize.d.ts +24 -0
  23. package/dist/types/query/operators.d.ts +241 -1
  24. package/dist/types/query/runtime.d.ts +5 -8
  25. package/dist/types/query/types.d.ts +34 -0
  26. package/dist/types/segments.d.ts +31 -0
  27. package/dist/types/write.d.ts +204 -0
  28. package/dist/types/xquery/parse.d.ts +2 -3
  29. package/docs/JSLT-FORMAT.md +74 -3
  30. package/docs/JSLT-PRELUDE.md +1 -1
  31. package/docs/QUERY-FORMAT.md +695 -33
  32. package/package.json +18 -4
  33. package/schemas/geojson.draft-07.schema.json +323 -0
  34. package/schemas/geojson.jaren.schema.json +863 -0
  35. package/schemas/geojson.schema.json +172 -0
  36. package/schemas/jaren-jslt.authoring.schema.json +142 -0
  37. package/schemas/jaren-jslt.draft-07.schema.json +152 -11
  38. package/schemas/jaren-jslt.llm-profile.schema.json +782 -0
  39. package/schemas/jaren-jslt.schema.json +152 -11
  40. package/schemas/jaren-query.draft-07.schema.json +152 -11
  41. package/schemas/jaren-query.llm-profile.schema.json +619 -0
  42. package/schemas/jaren-query.schema.json +82 -15
  43. package/src/basic.js +1 -1
  44. package/src/canonical.js +170 -0
  45. package/src/cow.js +106 -0
  46. package/src/errors.js +68 -0
  47. package/src/index.js +3 -0
  48. package/src/jslt/dispatch.js +178 -28
  49. package/src/jslt/errors.js +19 -14
  50. package/src/jslt/index.js +37 -29
  51. package/src/jslt/packs/finance.js +49 -0
  52. package/src/jslt/packs/index.js +18 -0
  53. package/src/jslt/packs/math.js +46 -0
  54. package/src/jslt/packs/stats.js +65 -0
  55. package/src/jslt/registry.js +200 -0
  56. package/src/jslt/stylesheet.js +14 -23
  57. package/src/jtlt/desugar.js +2 -3
  58. package/src/jtlt/errors.js +6 -12
  59. package/src/jtlt/index.js +12 -29
  60. package/src/jtlt/template.js +9 -18
  61. package/src/option-variants.js +54 -0
  62. package/src/patch.js +1052 -0
  63. package/src/path.js +319 -52
  64. package/src/pointer.js +225 -44
  65. package/src/query/compile.js +790 -75
  66. package/src/query/errors.js +72 -12
  67. package/src/query/index.js +274 -42
  68. package/src/query/normalize.js +489 -78
  69. package/src/query/operators.js +620 -23
  70. package/src/query/runtime.js +5 -19
  71. package/src/query/types.js +213 -0
  72. package/src/segments.js +409 -64
  73. package/src/write.js +660 -0
  74. package/src/xquery/parse.js +37 -53
package/README.md CHANGED
@@ -8,9 +8,11 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
8
8
 
9
9
  | Import | Contents |
10
10
  |---|---|
11
- | `@jarenjs/json` | everything below |
11
+ | `@jarenjs/json` | the addressing and query modules below (`jslt`/`jtlt`/`xquery` are subpath-only) |
12
12
  | `@jarenjs/json/basic` | JSON, JSON Pointer and JSONPath string validation |
13
13
  | `@jarenjs/json/pointer` | the JSON Pointer and Relative JSON Pointer compiler |
14
+ | `@jarenjs/json/patch` | JSON Patch and JSON Merge Patch: compiled apply + structural diff |
15
+ | `@jarenjs/json/write` | compiled write operations: set/insert/remove at pointers, normalized paths, or every JSONPath match |
14
16
  | `@jarenjs/json/path` | the JSONPath compiler |
15
17
  | `@jarenjs/json/query` | the Jaren JSON Query engine |
16
18
  | `@jarenjs/json/jslt` | the Jaren JSLT stylesheet compiler and dispatcher |
@@ -24,9 +26,14 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
24
26
  | Standard | Functions |
25
27
  |---|---|
26
28
  | JSON validation | `isValidJSON`, `isValidJSONCheap` (a fast "definitely not JSON" pre-test) |
27
- | JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) | `compileJSONPointer`, `parseJSONPointer`, `isValidJSONPointer`, `isValidJSONPointerUriFragment` |
29
+ | JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) | `compileJSONPointer`, `parseJSONPointer`, `parseJSONPointerPath`, `isValidJSONPointer`, `isValidJSONPointerUriFragment` |
28
30
  | Relative JSON Pointer | `compileRelativeJSONPointer`, `parseRelativeJSONPointer`, `compileDataRef`, `isValidRelativeJSONPointer` |
29
- | JSONPath ([RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html)) | `compileJSONPath`, `queryJSONPath`, `parseJSONPath`, `isValidJSONPathStrict` |
31
+ | JSON Patch ([RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)) | `compileJSONPatch`, `applyJSONPatch`, `createJSONPatch`, `isValidJSONPatch`, `JsonPatchCompileError`, `JsonPatchRuntimeError` |
32
+ | JSON Merge Patch ([RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396)) | `compileMergePatch`, `applyMergePatch`, `createMergePatch` |
33
+ | Write operations | `compileJSONPointerSetter`/`Inserter`/`Remover`, `compileJSONPathSetter`/`Inserter`/`Remover`, one-shot `setAtJSONPointer`, `removeAtJSONPath`, ..., `JsonWriteError` |
34
+ | Addressing bridge | `jsonPointerFromJSONPath`, `jsonPathFromJSONPointer` |
35
+ | JSONPath ([RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html)) | `compileJSONPath`, `queryJSONPath`, `parseJSONPath`, `isValidJSONPathStrict`, `isValidJSONPathSegments` |
36
+ | Canonical JSON ([RFC 8785](https://www.rfc-editor.org/rfc/rfc8785.html)) | `canonicalizeJson`, `JsonCanonicalizeError` |
30
37
  | Jaren JSON Query | `compileJsonQuery`, `queryJson`, `JsonQueryCompileError`, `JsonQueryRuntimeError` |
31
38
  | Jaren JSLT | `compileJsltStylesheet`, `transformJson`, `JsltCompileError`, `JsltRuntimeError` |
32
39
  | Jaren JTLT | `compileJtltStylesheet`, `renderText`, `JtltCompileError`, `JtltRuntimeError` |
@@ -57,8 +64,118 @@ const getName = compileRelativeJSONPointer('0#');
57
64
  getName(doc, '/limits/min'); // 'min' (the member name of the location)
58
65
  ```
59
66
 
67
+ The `#` form has two modes, chosen once at compile time, because the spec's
68
+ answer and the fast answer are not the same thing. Relative JSON Pointer says
69
+ `#` yields the member *name* for an object member and the *index* — a number —
70
+ for an array element, and telling those apart means looking at the container:
71
+
72
+ ```javascript
73
+ const asString = compileRelativeJSONPointer('0#'); // default
74
+ const asNumber = compileRelativeJSONPointer('0#', { hashIndex: 'number' });
75
+
76
+ asString({ a: ['x', 'y'] }, '/a/1'); // '1' — string, answered from the path alone
77
+ asNumber({ a: ['x', 'y'] }, '/a/1'); // 1 — number, the draft's answer
78
+ asNumber({ o: { 1: 'v' } }, '/o/1'); // '1' — a member named "1" is still a name
79
+ ```
80
+
81
+ `hashIndex: 'string'` is the default and never touches the document, which is
82
+ what keeps it a string operation of tens of nanoseconds; it is also what the
83
+ validator's `$data` keyword has always seen, so `{"$data": "0#"}` compared
84
+ against a number-typed keyword gets a string. `hashIndex: 'number'` walks to the
85
+ parent of the location to check whether it is an array, and falls back to the
86
+ string when that parent cannot be reached — nothing proves a position is an
87
+ index. An unknown mode is a `TypeError` at compile time rather than a silent
88
+ fallback. `compileDataRef` forwards the option.
89
+
90
+ Neither mode verifies the location itself exists: the caller passes a location
91
+ it actually reached. The root has no name, so `0#` there is
92
+ `JSONPOINTER_NOTHING` and stays distinguishable from the member a document can
93
+ genuinely name `''`.
94
+
60
95
  `compileDataRef(ref)` compiles the union the validator accepts — `''` for the data root, a leading `/` for an absolute pointer, a leading digit for a relative one — deciding the dispatch once at compile time. On a realistic `$data` workload the compiled resolvers are 4–20x faster than the interpretive resolver they replaced, and beat the `jsonpointer` npm package on every scenario (`npm run benchmark:jsonpointer`, 2026-07-17: absolute pointers 12–16x, relative pointers 4–16x, `compileDataRef` dispatch 7–20x).
61
96
 
97
+ The write-side encode is there too: `encodeJSONPointerSegment(key)` escapes one reference token (`~` → `~0`, `/` → `~1`), `decodeJSONPointerSegment(token)` is its inverse for one token (decoding `~1` before `~0`, so a member named `~1` survives the round trip), and `formatJSONPointer(segments)` is the inverse of `parseJSONPointer`.
98
+
99
+ `parseJSONPointerPath(pointer)` is the typed variant: it returns
100
+ `(string|number)[]`, narrowing canonical array indexes to numbers
101
+ (`'/items/0/id'` → `['items', 0, 'id']`) while leaving anything RFC 6901 does
102
+ not make an index — `01`, `1e0`, `-`, `1abc` — a string. RFC 6901 has no
103
+ types, so the classification is lexical rather than resolved against a
104
+ document, using the same index rule the pointer compiler uses. This is the
105
+ path shape error reporters and diffing tools expect, so it pairs directly
106
+ with a validation error's `instancePath`.
107
+
108
+ ### JSON Patch and JSON Merge Patch
109
+
110
+ Partial updates follow the same compile-once discipline. `compileJSONPatch(patch)` validates an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) patch document once (`JsonPatchCompileError`, `JP0xxx`, with a `docPath` pointing into the *patch* document), pre-parses every `path`/`from` through the RFC 6901 parser and specializes one closure per operation. Applying is **copy-on-write**: the input document is never mutated, untouched subtrees are shared by reference with the result, and application is atomic — a failing operation (`JsonPatchRuntimeError`, `JP2xxx`, carrying both the patch `docPath` and the target `dataPath`) leaves nothing behind, exactly as RFC 6902 section 5 requires. The engine passes the complete official [json-patch-tests](https://github.com/json-patch/json-patch-tests) suite:
111
+
112
+ ```javascript
113
+ import { compileJSONPatch, applyJSONPatch, createJSONPatch } from '@jarenjs/json';
114
+
115
+ // compile once, apply many times (hot path)
116
+ const apply = compileJSONPatch([
117
+ { op: 'test', path: '/version', value: 5 },
118
+ { op: 'replace', path: '/user/name', value: 'Bob' },
119
+ { op: 'add', path: '/user/tags/-', value: 'admin' },
120
+ ]);
121
+ const next = apply(doc); // doc is untouched; unchanged subtrees are shared
122
+
123
+ // one-shot
124
+ applyJSONPatch(doc, patch);
125
+
126
+ // structural diff: a change feed for @jarenjs/forms and friends
127
+ applyJSONPatch(a, createJSONPatch(a, b)); // deep-equals b
128
+ ```
129
+
130
+ Because an application only clones the spine it writes through — and clones it once, no matter how many operations touch the same region — the compiled applier beats the usual clone-and-interpret shape by 5–170x depending on document size (`npm run benchmark:jsonpatch`). Two options tune the copy discipline, mirroring JSLT's `share`/`fresh` dispositions: `values: 'fresh'` deep-copies inserted operation values per application (the default `'share'` inserts them by reference, so treat results as immutable), and `mutate: true` patches in place for the last bit of speed at the cost of atomicity.
131
+
132
+ `createJSONPatch(source, target)` trims each array's deep-equal common prefix and suffix and pairs the rest up index-wise, which is linear and already minimal for in-place edits and head/tail insertions — but turns a mid-array insertion into a run of per-index replaces. `createJSONPatch(source, target, { arrayDiff: 'minimal' })` aligns the changed middle instead, so the insertion is one `add`:
133
+
134
+ ```javascript
135
+ const before = [{ id: 1 }, { id: 2 }, { id: 3, n: 0 }];
136
+ const after = [{ id: 1 }, { id: 9 }, { id: 2 }, { id: 3, n: 1 }];
137
+
138
+ createJSONPatch(before, after);
139
+ // 4 ops: replace /1/id, replace /2/id, remove /2/n, add /3
140
+
141
+ createJSONPatch(before, after, { arrayDiff: 'minimal' });
142
+ // 2 ops: add /1 {id:9}, replace /3/n 1
143
+ ```
144
+
145
+ The alignment minimizes the patch itself (edit distance with substitutions — *not* a longest common subsequence, which maximizes kept elements and so pays a delete plus an insert on a permutation where one rewrite would do), and therefore never takes more steps than the default. It stays opt-in because it is O(m·n) in the length of the changed middle; past a fixed budget a single array falls back to the linear diff, so the option can never make a large diff quadratic. Both modes reproduce `target` exactly.
146
+
147
+ A third option makes the engine a **change feed**: `changes: true` specializes the applier to return `{ doc, changes }`, where `changes` holds one JSON Pointer per successful write, in application order. The reported pointers are *invalidation-sound* — object writes, array replaces and appends report the written location itself; shifting array inserts and removes report the parent array; a root write reports `''` — which is exactly the primitive dirty-path consumers (view re-rendering in `@jarenjs/app`, rule dependency memoization in `@jarenjs/forms`) need. `test` operations report nothing, and the option costs nothing when off: the tracking branch is compiled out of the applier.
148
+
149
+ [RFC 7396](https://datatracker.ietf.org/doc/html/rfc7396) merge patches ride the same machinery: `compileMergePatch(patch)` pre-splits the patch into remove/set/merge plans, and applying is identity-preserving — a merge that changes nothing returns the target by reference, so it doubles as a cheap change detector. `createMergePatch(source, target)` emits the merge patch (with the RFC's documented `null`-member representability caveat), and `applyMergePatch(doc, patch)` is the one-shot form.
150
+
151
+ ### Write operations
152
+
153
+ When a whole patch document is more ceremony than the job needs, the standalone write operations expose the same copy-on-write core directly — `set`, `insert` and `remove`, compiled once per target:
154
+
155
+ ```javascript
156
+ import {
157
+ compileJSONPointerSetter, removeAtJSONPointer,
158
+ compileJSONPathSetter, removeAtJSONPath,
159
+ } from '@jarenjs/json';
160
+
161
+ // a target is an RFC 6901 pointer, a normalized path, or ANY singular
162
+ // query — '/store/book/0/title', "$['store']['book'][0]['title']" and
163
+ // '$.store.book[-1].title' (negative = from the end) all compile
164
+ const setZip = compileJSONPointerSetter('/address/zip');
165
+ const next = setZip(doc, '10999'); // doc untouched, spine cloned once
166
+
167
+ setZip(doc, (old) => old ?? '10115'); // setters take updater functions
168
+
169
+ // ...or write at EVERY node a JSONPath query selects
170
+ const addVat = compileJSONPathSetter('$..price');
171
+ addVat(doc, (price) => price * 1.21);
172
+ removeAtJSONPath(doc, '$.store.book[?@.price > 20]');
173
+ ```
174
+
175
+ Set replaces (creating a missing final member; `/arr/-` appends), insert has RFC 6902 `add` semantics (array elements shift right), remove deletes with shift. The query-selected writers apply matched locations in **reverse document order**, so multiple removals or inserts in one array — and nested matches — compose without index bookkeeping, and matching nothing is a no-op that returns the input. Everything is copy-on-write with an in-place `{ mutate: true }` escape hatch; failures (`JsonWriteError`, `JW0001`/`JW2xxx` with the target as `dataPath`) leave the input untouched.
176
+
177
+ The addressing bridge rounds this out: `jsonPointerFromJSONPath(singularQuery)` and `jsonPathFromJSONPointer(pointer)` convert between the two location languages (digit tokens become index selectors — RFC 6901's one-token-two-forms ambiguity, resolved by convention), so pointers, normalized paths and query results compose freely.
178
+
62
179
  ## The JSONPath compiler
63
180
 
64
181
  JSONPath selects *many* locations: a query like `$.store.book[?@.price < 10].title` describes a whole nodelist. The `path.js` module implements the complete RFC 9535 specification as a two-stage compiler, mirroring the philosophy of the schema validator: **parse and decide everything once, then run a specialized function**.
@@ -81,6 +198,7 @@ query(other); // compiled once, reusable on any document
81
198
  | `query(data)` / `query.values(data)` | array of matched values, in document order |
82
199
  | `query.first(data)` | the first matched value, or `undefined` |
83
200
  | `query.exists(data)` | `true` when at least one node matches |
201
+ | `query.iterate(data)` | a generator yielding matched values on demand, in document order |
84
202
  | `query.nodes(data)` | array of `{ path, value }` pairs with normalized paths |
85
203
  | `query.paths(data)` | array of normalized paths (RFC 9535 §2.7), e.g. `$['store']['book'][0]['title']` |
86
204
  | `query.source` | the original query string |
@@ -88,6 +206,29 @@ query(other); // compiled once, reusable on any document
88
206
 
89
207
  For one-off queries there is `queryJSONPath(source, data)`, which keeps a cache of compiled queries (512 entries, FIFO), and `isValidJSONPathStrict(source)` for a boolean grammar check — this is what the `json-path` format in [`@jarenjs/formats`](../formats) uses.
90
208
 
209
+ `first`, `exists` and `iterate` are one lazy walk: they stop at the first node they need instead of building the whole nodelist, so `q.first(doc)` on `$.items[?@.price < 10].name` over 10 000 items costs about 2 µs where `q.values(doc)` costs about 240 µs. Nothing is buffered along the way — a filter evaluates its predicate only until one passes, and a descendant segment abandons the walk mid-subtree.
210
+
211
+ #### Custom function extensions
212
+
213
+ `compileJSONPath(source, { pathFunctions })` registers function extensions ([RFC 9535 §2.4](https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions)) alongside the five built-ins. An entry declares its parameter and result types, which is what lets the parser type-check call sites the same way it checks `length()` or `match()`:
214
+
215
+ ```js
216
+ const pathFunctions = {
217
+ is_even: {
218
+ params: ['value'], // 'value' | 'nodes' | 'logical'
219
+ returns: 'logical',
220
+ evaluate: (v) => typeof v === 'number' && v % 2 === 0,
221
+ },
222
+ };
223
+
224
+ const q = compileJSONPath('$.items[?is_even(@.n)].n', { pathFunctions });
225
+ q({ items: [{ n: 1 }, { n: 2 }, { n: 3 }, { n: 4 }] }); // [2, 4]
226
+ ```
227
+
228
+ A `value` parameter arrives as a JSON value or `JSONPATH_NOTHING`, a `nodes` parameter as an array of the selected values, a `logical` parameter as a boolean — and a `logical` parameter accepts the full filter grammar at the call site, so `both(@.a > 1, !@.b)` parses. A name that would redefine a built-in is rejected (§2.4.1), and a malformed registry raises a `TypeError`.
229
+
230
+ The same option reaches everywhere a path string is embedded: the JSONPath-addressed writers, `compileJsonQuery`, and JSLT match paths and rule bodies. The `json-path` and `json-path-segments` string formats deliberately do **not** see it — a format is a property of the string, so it has to mean the same thing in every schema regardless of which extensions a host installed.
231
+
91
232
  ### Supported syntax
92
233
 
93
234
  Everything in RFC 9535, with no extensions and no omissions:
@@ -211,17 +352,72 @@ One phrase carries the whole pipeline. Clauses apply in **fixed semantic order r
211
352
 
212
353
  | Key | Value | Presence |
213
354
  |---|---|---|
214
- | `$for` | iteration bindings `{ name: source, ... }`, positional form `{ "$in": expr, "$at": "i" }` | at least one of `$for`/`$let` |
215
- | `$let` | sequence bindings (no iteration) | at least one of `$for`/`$let` |
355
+ | `$fold` | one accumulator binding `{ name: initExpr }` | at least one of `$fold`/`$for`/`$let` |
356
+ | `$for` | iteration bindings `{ name: source, ... }`, extended form `{ "$in": expr, "$at": "i" }` | at least one of `$fold`/`$for`/`$let` |
357
+ | `$let` | sequence bindings (no iteration) | at least one of `$fold`/`$for`/`$let` |
358
+ | `$as` | schema assertions on this phrase's bindings | optional |
216
359
  | `$where` | tuple filter (effective boolean value) | optional |
217
360
  | `$groupby` | grouping-key bindings | optional |
218
361
  | `$orderby` | key spec or list: `{ "$key": e, "$dir": "desc", "$empty": "greatest" }` | optional |
219
362
  | `$count` | variable name for the 0-based tuple index | optional |
220
363
  | `$return` | the result expression per surviving tuple | required |
221
364
 
222
- Semantic order: `$for → $let → $where → $groupby → $orderby → $count → $return`. Quantifiers are their own two-key phrases: `{"$some": bindings, "$satisfies": expr}` / `{"$every": ...}`, with short-circuit evaluation.
365
+ Semantic order: `$fold → $for → $let → $as → $where → $groupby → $orderby → $count → $return`. Quantifiers are their own two-key phrases: `{"$some": bindings, "$satisfies": expr}` / `{"$every": ...}`, with short-circuit evaluation.
366
+
367
+ **`$fold` turns the phrase into a reduction.** The accumulator's initial value is evaluated once, `$return` names its next value per surviving tuple, and the phrase evaluates to the final accumulator instead of the collected sequence. This is how the language gets a general fold without the JSON encoding needing to spell a *function value* — the accumulator is a binding, not a lambda parameter. It composes with the rest, so `$orderby` folds over sorted tuples and `$groupby` updates once per group:
368
+
369
+ ```js
370
+ queryJson({
371
+ $fold: { total: 0 },
372
+ $for: { b: '$.store.book[*]' },
373
+ $where: { $lt: ['$b.price', 10] },
374
+ $return: { $add: ['$total', '$b.price'] },
375
+ }, bookstore); // 17.939999999999998 — every number is an IEEE double (D1)
376
+ ```
377
+
378
+ Because `$get` is a real dynamic lookup, a fold over a runtime path is a pointer walk: `{"$fold": {"cur": "$.doc"}, "$for": {"seg": "$.path[*]"}, "$return": {"$get": ["$cur", "$seg"]}}`.
223
379
 
224
- The operator library (58 operators: comparisons, IEEE-double arithmetic, logic, strings with I-Regexp `$match`/`$search`/`$replace`, aggregates, sequence tools like `$distinct`/`$subsequence`/`$range`, type predicates and casts, `$coalesce`) is cataloged in [QUERY-FORMAT.md §8](./docs/QUERY-FORMAT.md#8-operators).
380
+ **Extended `$for` bindings** cover the two remaining XQuery iteration shapes. `{"$in": e, "$allowing-empty": true}` is outer-join iteration: when the source yields no tuple, one tuple is emitted with the variable bound to the empty sequence (position `-1` if `$at` is present), so the enclosing row survives. `{"$in": e, "$window": "tumbling"|"sliding", "$size": n, "$step": m}` iterates runs instead of items — a tumbling window *partitions* the stream, so its short final window is kept; a sliding window is a fixed-width moving view, so only full windows are emitted.
381
+
382
+ ```js
383
+ queryJson({
384
+ $for: { w: { $in: '$[*]', $window: 'sliding', $size: 3 } },
385
+ $return: { $avg: '$w' },
386
+ }, [1, 2, 3, 4, 5]); // [2, 3, 4] — a 3-point moving average
387
+ ```
388
+
389
+ The operator library (93 operators: comparisons, IEEE-double arithmetic, logic, strings with I-Regexp `$match`/`$search`/`$replace`, aggregates, sequence tools like `$distinct`/`$subsequence`/`$range`, type predicates and casts, `$coalesce`, the RFC 3339 date family, and the spatial family) is cataloged in [QUERY-FORMAT.md §8](./docs/QUERY-FORMAT.md#8-operators).
390
+
391
+ **Extending the vocabulary (host opt-in).** The 93 are closed, but a host can add more the way `@jarenjs/validate` gains formats from `@jarenjs/formats`: `createJsltRegistry().use(mathPack).use(financePack)` composes packs of pure `@jarenjs/core` functions into a compiler, so `{ "$sqrt": "$.variance" }` and `{ "$npv": ["$.rate", "$.cashflows[*]"] }` work in a stylesheet, a bare query, and `@jarenjs/linq` — while a document compiled *without* the registry still rejects them. Aggregators fold a `seq` operand to an array before the pure call. See [JSLT-FORMAT.md §13](./docs/JSLT-FORMAT.md#13-registered-operators-host-opt-in-non-normative).
392
+
393
+ **Dates are RFC 3339 strings** ([§8.13](./docs/QUERY-FORMAT.md#813-dates-and-times)): `$is-date`/`$is-time`/`$is-datetime`/`$is-duration` test the lexical forms, `$year`…`$seconds` and `$offset` read components *lexically, in the value's own offset* (so "group by month" means what you expect), `$week`/`$week-year`/`$quarter`/`$weekday` add the derived calendar fields, and `$epoch`/`$datetime` convert to and from epoch milliseconds — the one place a value is shifted to UTC, and therefore the way to compare instants across offsets. There is deliberately no `current-dateTime`: a compiled query is cached by document identity and saved as a rule, so it must answer the same for the same input forever.
394
+
395
+ Dates also *move*. `$date-add`/`$date-sub` shift by an ISO 8601 duration or by an amount and a unit, `$start-of`/`$end-of` truncate to a calendar unit, `$date-diff` counts whole units, and `$date-format` renders through a Unicode LDML pattern compiled once with the query. Two rules keep it predictable: the **lexical form is preserved** (a date stays a date, and a date-time keeps its own offset instead of being normalized to UTC), and **month arithmetic clamps**, so 31 January plus a month is 28 February — with `$date-diff` counting to match, so adding its answer back never overshoots.
396
+
397
+ ```js
398
+ queryJson({
399
+ $for: { e: '$[*]' },
400
+ $groupby: { w: { '$start-of': ['$e.on', 'week'] } },
401
+ $orderby: ['$w'],
402
+ $return: { week: '$w', count: { $count: '$e' } },
403
+ }, [{ on: '2026-01-05' }, { on: '2026-01-08' }, { on: '2026-01-20' }]);
404
+ // [{ week: '2026-01-05', count: 2 }, { week: '2026-01-19', count: 1 }]
405
+ ```
406
+
407
+ Bucketing by week is the shape components alone cannot express, because a week boundary is arithmetic rather than a field. Patterns stay locale-independent — `MMMM` and `EEEE` are rejected rather than silently rendered in English — because localized text belongs to the presentation layer, not to a query.
408
+
409
+ **Geography is GeoJSON** ([§8.14](./docs/QUERY-FORMAT.md#814-spatial)), for the same reason dates are RFC 3339 strings: it is what the document already holds. Operands are a bare `[longitude, latitude]` position, a geometry, a `Feature` or a `FeatureCollection`, and wrappers unwrap for you. `$distance`, `$area` and `$length` answer in metres on the WGS 84 sphere — never planar, because a Euclidean answer over raw degrees is wrong by two thirds over a kilometre at Dutch latitudes. `$within` tests containment, `$bbox`/`$centroid` measure, and `$bbox-intersects` is named for exactly what it tests, since an `$intersects` that compared only boxes would be a lie the first time two L-shapes shared one.
410
+
411
+ ```js
412
+ queryJson({
413
+ $for: { c: '$.cities[*]' },
414
+ $where: { $within: ['$c.at', '$.region'] },
415
+ $orderby: [{ $key: { $distance: ['$c.at', '$.centre'] } }],
416
+ $return: '$c.name',
417
+ }, data); // a spatial filter and a spatial sort, in the language's own clauses
418
+ ```
419
+
420
+ A geohash needs no operator of its own to be useful: it is a string, so proximity is `$starts-with` on a prefix and spatial bucketing is `$groupby` over `$substring`. `$geohash` only produces it.
225
421
 
226
422
  ### External parameters
227
423
 
@@ -259,6 +455,28 @@ The complete structural grammar of the language is published as JSON Schema, twi
259
455
 
260
456
  Both exist because the schema is authored in a draft-neutral keyword subset (no `$ref` siblings, no `unevaluated*`, no tuples), so ecosystems pinned to draft-07 — several structured-output stacks among them — get an identical grammar for one `$defs`→`definitions` rename. Every fixture in the test suite validates under both drafts.
261
457
 
458
+ ### GeoJSON, fully validated
459
+
460
+ The same machinery answers a problem the geospatial ecosystem has lived with for years. The [official GeoJSON JSON Schema](https://github.com/geojson/schema) states that it **cannot** express that a linear ring is closed, or that rings follow RFC 7946's right-hand rule — neither is a structural property — so every validator bolts on custom code, and unclosed rings and reversed winding (which makes a renderer fill the entire globe) remain among the commonest defects in real data.
461
+
462
+ Three artifacts ship here:
463
+
464
+ - [`schemas/geojson.schema.json`](./schemas/geojson.schema.json) — **portable**: plain draft-neutral JSON Schema, no Jaren extension, usable by any validator;
465
+ - [`schemas/geojson.draft-07.schema.json`](./schemas/geojson.draft-07.schema.json) — its mechanically derived draft-07 twin;
466
+ - [`schemas/geojson.jaren.schema.json`](./schemas/geojson.jaren.schema.json) — the same grammar plus the two missing invariants, expressed with [`$query`](../validate).
467
+
468
+ The Jaren artifact is a mechanical *restriction* of the portable one — structurally identical, with a `$query` on the `polygon` and `multiPolygon` definitions — so the two cannot drift, and a consumer without `$query` support still gets the standard grammar. Ring closure is `$eq` of the first and last position; winding is the sign of the shoelace sum, computed with `$fold`:
469
+
470
+ ```javascript
471
+ import { JarenValidator } from '@jarenjs/validate';
472
+
473
+ const unclosed = { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 1]]] };
474
+ new JarenValidator().compile(portableSchema)(unclosed); // true — structurally fine
475
+ new JarenValidator().compile(jarenSchema)(unclosed); // false — the ring never closes
476
+ ```
477
+
478
+ The test suite pins six defect classes that slip past structural validation and are caught here, including one buried inside a `FeatureCollection`.
479
+
262
480
  ## Generating queries with LLMs
263
481
 
264
482
  A query language whose entire grammar is one JSON Schema is a natural fit for **constrained decoding** — the structured-output mode of every major LLM API. Hand the schema to the provider and the model cannot emit an unknown operator, a three-argument `$eq`, or a mixed `$`/plain-key object; what remains is validated and compiled in two lines, and every failure carries a `docPath` you can feed back to the model for repair:
@@ -279,7 +497,11 @@ const query = compileJsonQuery(doc); // JsonQueryCompileError.docPath on
279
497
  query(data); // JsonQueryRuntimeError.docPath at runtime
280
498
  ```
281
499
 
282
- The validate step is not redundant: provider structured-output implementations support varying JSON Schema subsets regardless of the draft they declare (`patternProperties` and `propertyNames`, which the schema uses for binding names and map constructors, are not universally enforced). Validating locally catches whatever the provider's subset let through. A simplified "LLM profile" of the schema — trading some precision for the lowest-common-denominator subset — is a possible follow-up; it does not exist today.
500
+ The validate step is not redundant: provider structured-output implementations support varying JSON Schema subsets regardless of the draft they declare (`patternProperties` and `propertyNames`, which the schema uses for binding names and map constructors, are not universally enforced, and strict modes often reject `oneOf`). Validating locally catches whatever the provider's subset let through.
501
+
502
+ For those strict subsets each grammar also ships an **LLM profile** — [`jaren-query.llm-profile.schema.json`](./schemas/jaren-query.llm-profile.schema.json) and [`jaren-jslt.llm-profile.schema.json`](./schemas/jaren-jslt.llm-profile.schema.json) — a mechanically derived, pure *relaxation* of the canonical artifact: `patternProperties`, `propertyNames` and asserted `format`s are removed (each restated in the nearest `description`, which the model still reads) and every `oneOf` becomes `anyOf`. That last swap costs no precision: the canonical grammar discriminates its branches by member name — a map constructor has no `$`-prefixed key, every phrase pins its own (`$const`, `$map`, `$call`, `$return`, …) or draws its single key from a disjoint operator enum — so at most one branch can ever match a document, and exactly-one and at-least-one accept the same language. It is also *required* once the name constraints are gone: `oneOf` would then reject `{"$eq": [1, 2]}` for matching two now-overlapping branches. Every canonical-valid document validates under the profile; the reverse is deliberately not guaranteed, which is exactly why the local validate-then-compile step above stays mandatory. Hand the profile to the provider's constrained decoder, validate locally against the canonical schema. (`@jarenjs/ai`'s `createStructuredOutput` packages this handshake.)
503
+
504
+ A relaxation, though, is not a *shrink*: restating every removed constraint in a `description` means the JSLT profile is 19,158 characters against the canonical 18,736, and on a small model an oversized `response_format` is the difference between a document and an empty reply. So the JSLT grammar also ships an **authoring profile** — [`jaren-jslt.authoring.schema.json`](./schemas/jaren-jslt.authoring.schema.json), **3,491 characters** — the same mechanical derivation with one more step: the `queryDocument` `$ref` that pulls the entire expression language into the document grammar is left *open*. What survives is the document shape (envelope, rules, match specs, modes); what leaves is the ~16 kB of phrase shapes. It is a pure widening of the canonical language, so nothing the engine can run becomes inexpressible, and it is deliberately weaker — a body of pure nonsense decodes under it. That is the trade, and it is only safe because the two authorities after the decoder are unchanged: validate against the canonical schema, then compile. The vocabulary the cut removes belongs in the prompt instead, where `@jarenjs/ai`'s `operatorCrib` puts it (operator names grouped by arity, ~1.1 kB, read off the query artifact). `@jarenjs/ai`'s [`createStylesheetAuthor`](../ai/README.md#when-the-grammar-is-too-big-to-decode-measured) packages the whole handshake.
283
505
 
284
506
  Query documents are plain JSON, so they travel through the rest of an LLM toolchain as-is: function-call arguments, retrieval filters, audit logs, replay.
285
507
 
@@ -298,30 +520,31 @@ q(null, { doc: data }); // [ 'Sayings of the Century', 'Moby Dick' ]
298
520
 
299
521
  `npm run benchmark:jsonquery` runs the scenario matrix against [fontoxpath](https://www.npmjs.com/package/fontoxpath) (a real XQuery 3.1 engine in JavaScript — the closest honest comparison) and [jsonata](https://www.npmjs.com/package/jsonata) (the popular practical alternative), asserting result equivalence on every document before timing anything. Each engine runs the same scenario written idiomatically in its own language (`benchmark/adaptors/jsonquery/`).
300
522
 
301
- Measured with `npm run benchmark:jsonquery:profile` (2026-07-17, Node v24.14.0; ratios are that engine's time over Jaren's):
523
+ Measured with `npm run benchmark:jsonquery:profile` (2026-07-27, Node v22.22.2; ratios are that engine's time over Jaren's):
302
524
 
303
525
  | Scenario | Jaren | fontoxpath 3.34 | jsonata 2.2 |
304
526
  |---|---|---|---|
305
527
  | **4-book bookstore** | | | |
306
- | singular access `$b.title` | 382 ns (2.6M/s) | 7.3 µs (19x) | 5.7 µs (15x) |
307
- | filter + project (spec A.2) | 1.9 µs (527k/s) | 36.1 µs (19x) | 27.0 µs (14x) |
308
- | join (spec A.3) | 5.6 µs (178k/s) | 77.5 µs (14x) | 111.7 µs (20x) |
309
- | group + aggregate (spec A.4) | 4.3 µs (234k/s) | n/a | 47.7 µs (11x) |
310
- | deep reshape | 2.6 µs (393k/s) | 190.5 µs (75x) | 82.9 µs (33x) |
528
+ | singular access `$b.title` | 451 ns (2.2M/s) | 12.0 µs (27x) | 7.3 µs (16x) |
529
+ | filter + project (spec A.2) | 3.6 µs (281k/s) | 60.6 µs (17x) | 26.2 µs (7.4x) |
530
+ | join (spec A.3) | 3.4 µs (291k/s) | 102.0 µs (30x) | 96.6 µs (28x) |
531
+ | group + aggregate (spec A.4) | 4.4 µs (229k/s) | n/a | 52.6 µs (12x) |
532
+ | deep reshape | 2.4 µs (426k/s) | 173.1 µs (74x) | 55.8 µs (24x) |
311
533
  | **10,000-book bookstore** | | | |
312
- | singular access | 299 ns (3.3M/s) | 16.6 µs (56x) | 4.2 µs (14x) |
313
- | filter + project | 1.5 ms | 328.3 ms (215x) | 68.0 ms (45x) |
314
- | join (measured at 1,000 books) | 27.8 ms | 1.57 s (57x) | 1.75 s (63x) |
315
- | group + aggregate | 2.5 ms | n/a | 39.9 ms (16x) |
316
- | deep reshape | 3.6 ms | 422.0 ms (118x) | 119.0 ms (33x) |
317
- | **compile, µs per query** | 24.3 µs | 460.7 µs (19x) | 66.1 µs (2.7x) |
534
+ | singular access | 324 ns (3.1M/s) | 9.0 µs (28x) | 4.6 µs (14x) |
535
+ | filter + project | 1.33 ms | 343.2 ms (259x) | 70.4 ms (53x) |
536
+ | join (measured at 1,000 books) | 331 µs | 1.64 s (4952x) | 1.71 s (5175x) |
537
+ | group + aggregate | 2.47 ms | n/a | 37.9 ms (15x) |
538
+ | deep reshape | 3.51 ms | 452.0 ms (129x) | 117.0 ms (33x) |
539
+ | **compile, µs per query** | 37.4 µs | 562.5 µs (15x) | 118.3 µs (3.2x) |
318
540
 
319
541
  Honest caveats — what each competitor is optimized for:
320
542
 
321
543
  - **fontoxpath** is an XML-first XPath/XQuery engine; JSON rides on XDM maps and arrays. The benchmark pre-converts each document to XDM *once, outside the timed loop* (per-call conversion would cost ~12 ms alone at 10k books), and fontoxpath has no public compile-only API, so its compile number is fresh-source evaluation minus cached re-evaluation. It does not implement `group by`. Its engineering effort goes into DOM navigation, buckets and XQuery Update — not JSON throughput.
322
544
  - **jsonata** is a tree-walking interpreter whose `evaluate()` is async since 2.x; its numbers include that promise overhead because its API imposes it. It is optimized for expressiveness and embeddability, not raw speed.
323
545
  - **Jaren**'s compile number includes `JSON.parse` of the query text, since the competitors parse text too.
324
- - The join is a naive O(books × ratings) nested loop in **all three** engines (Jaren's hash-join optimizer is roadmap); it is measured at 1,000 books.
546
+ - The join row is the one place these engines are not doing the same work, and the four-digit ratio says so rather than hiding it. fontoxpath and jsonata run it as a naive O(books × ratings) nested loop; Jaren's planner recognizes the uncorrelated equijoin and answers it from a hash table, which is O(books + ratings). At four books that is worth nothing (3.4 µs against 96–102 µs, the same 28–30x as every other small-document row); at 1,000 books it is the whole difference. Read the 4,952x as "different algorithm", not "faster engine" — and note the rewrite is declined whenever it would be observable (a `$as`, a `$let`, a correlated probe side), in which case Jaren runs the same nested loop they do. The row stays capped at 1,000 books because raising it would grow their cost quadratically and Jaren's linearly, measuring the cap instead of the engines.
547
+ - The 4-book `singular` cell reads slower than the 1,000-book one (451 ns vs 335 ns) run to run. That is JIT/IC noise across the cell sequence, not a real cost curve; pinning it down is a `--cell-order` shuffle on the roadmap.
325
548
 
326
549
  ## JSLT — declarative JSON transformation
327
550
 
@@ -400,6 +623,8 @@ const annotateBooks = compileJsltStylesheet([
400
623
  ], { compileTypeTest: createTypeTestCompiler() });
401
624
  ```
402
625
 
626
+ For render-loop consumers there is one more lever: `compileJsltStylesheet(doc, { memo: true })` memoizes rule outputs by (location, value reference). A compile-time analysis marks every rule whose output provably depends on nothing but the matched value (no `$root`/`$path`/user externals, transitively through `$apply`, and no root references inside match-path filters); those rules return their previous output **by reference** when the same subtree reference shows up at the same location. Across copy-on-write updates an unchanged document transforms in O(1) and a one-field change re-evaluates one spine — the fuel for the `@jarenjs/view` patcher's reference-equality fast path, which is why `@jarenjs/app` compiles every view with it. The cache is two-generational (entries unused for one transform retire), and memoized outputs must be treated as immutable.
627
+
403
628
  The complete stylesheet grammar is published for validators and LLM constrained decoding as [`jaren-jslt.schema.json`](./schemas/jaren-jslt.schema.json) (draft 2020-12) and its mechanically derived [`jaren-jslt.draft-07.schema.json`](./schemas/jaren-jslt.draft-07.schema.json) twin. Rule-body definitions are mechanically copied from the query artifact and extended only with `$apply`, so the query vocabulary stays closed. Provider structured-output implementations still support uneven schema subsets; validate the generated document locally before compiling it, as described in [JSLT-FORMAT Appendix B](./docs/JSLT-FORMAT.md#appendix-b-llm-structured-output-non-normative).
404
629
 
405
630
  ### JSLT benchmark
@@ -464,8 +689,8 @@ Unmatched nodes follow the XSLT built-in template rules, restated for JSON: cont
464
689
 
465
690
  ## Roadmap
466
691
 
467
- This package's roadmap lives in the repository-wide [ROADMAP](../../ROADMAP.md), under its `@jarenjs/json` sections: the query filter optimizer and hash joins, lazy sequences, the JSLT single-walk matcher, write operations and JSON Patch, custom JSONPath function extensions, canonical JSON, XQuery front-end `xs:*` casts, and more. Recently landed from that list: JSON Schema as the query type system (`$valid`/`$assert`/`$as`), the inverse [`$query` keyword](../validate/README.md) in the validator, compiled JSON Pointers, and the complete JSLT template layer.
692
+ This package's roadmap lives in the repository-wide [ROADMAP](../../docs/ROADMAP.md), under its `@jarenjs/json` sections: hoisting `$`-absolute comparables out of filter loops, first-class function values, the JSLT single-walk matcher, XQuery front-end `xs:*` casts, and more. Recently landed from that list: hash-joined equijoins and counting-loop `$range` iteration, the `$fold` accumulator clause, `$allowing-empty` and window bindings, the RFC 3339 date operators, closed-world compilation and the `steps`/`depth` execution limits.
468
693
 
469
694
  ## Development
470
695
 
471
- Unit tests live in `test/json/` at the repository root (`npm run test:json`); the JSONPath tests are built from the RFC's own examples, the query and JSLT tests from their normative fixtures (each schema corpus validates against both artifact drafts), and every example in this README runs in `test/json/readme-examples.test.js`. This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document. Benchmarks (all documented in the [benchmark workspace README](../../benchmark/README.md)): `benchmark/jsonpath.js` (JSONPath compliance + performance), `benchmark/jsonpointer.js` (compiled pointers vs the interpretive resolver and the `jsonpointer` npm package), `benchmark/jsonquery.js` (query engine vs fontoxpath/jsonata), `benchmark/jslt.js` (stylesheet engine vs native JS/JSONata), `benchmark/qt3-runner.js` (W3C QT3 scorecard through the XQuery front-end). See the repository [README](../../README.md) and [ARCHITECTURE](../../ARCHITECTURE.md) for the monorepo picture.
696
+ Unit tests live in `test/json/` at the repository root (`npm run test:json`); the JSONPath tests are built from the RFC's own examples, the query and JSLT tests from their normative fixtures (each schema corpus validates against both artifact drafts), and every example in this README runs in `test/json/readme-examples.test.js`. This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document. Benchmarks (all documented in the [benchmark workspace README](../../benchmark/README.md)): `benchmark/jsonpath.js` (JSONPath compliance + performance), `benchmark/jsonpointer.js` (compiled pointers vs the interpretive resolver and the `jsonpointer` npm package), `benchmark/jsonquery.js` (query engine vs fontoxpath/jsonata), `benchmark/jslt.js` (stylesheet engine vs native JS/JSONata), `benchmark/qt3-runner.js` (W3C QT3 scorecard through the XQuery front-end). See the repository [README](../../README.md) and [ARCHITECTURE](../../docs/ARCHITECTURE.md) for the monorepo picture.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Error thrown when a value cannot be canonicalized because it is not
3
+ * JSON data (RFC 8785 section 3.2.1). `dataPath` is an RFC 6901 JSON
4
+ * Pointer to the offending value, so a rejected document says which
5
+ * member was at fault rather than only that one was.
6
+ *
7
+ * Extends `TypeError`: passing a non-JSON value to a canonicalizer is a
8
+ * programming error, the same class `JSON.stringify` raises for a
9
+ * BigInt or a cycle.
10
+ */
11
+ export declare class JsonCanonicalizeError extends TypeError {
12
+ dataPath: any;
13
+ constructor(message: any, dataPath: any);
14
+ }
15
+ /**
16
+ * Serialize a JSON value to its canonical form (RFC 8785 / JCS): object
17
+ * members sorted by the UTF-16 code units of their names, numbers in
18
+ * the ECMAScript `Number::toString` form, strings with the shortest
19
+ * JSON escapes and no escaping of non-ASCII, and no insignificant
20
+ * whitespace. Two structurally equal values always produce identical
21
+ * text, which is what makes the result safe to hash or sign.
22
+ *
23
+ * Unlike `JSON.stringify`, nothing is dropped or coerced: `undefined`,
24
+ * a function, a symbol, a BigInt, `NaN`, `Infinity`, an unpaired
25
+ * surrogate, a circular reference and a class instance are all errors,
26
+ * because a canonicalizer that quietly rewrote its input would produce
27
+ * a signature over a document nobody sent.
28
+ *
29
+ * @param {any} value - The JSON value to canonicalize
30
+ * @returns {string} The canonical JSON text
31
+ * @throws {JsonCanonicalizeError} When the value is not JSON data
32
+ * @example
33
+ * canonicalizeJson({ b: 1, a: [2, 3] }); // '{"a":[2,3],"b":1}'
34
+ * canonicalizeJson({ 'ä': 1, 'a': 2 }); // '{"a":2,"ä":1}'
35
+ * canonicalizeJson(1e21); // '1e+21'
36
+ */
37
+ export declare function canonicalizeJson(value: any): string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The mutable state of one write application. `owned` is the set of
3
+ * nodes this application created and may mutate freely; `null` means
4
+ * in-place mode (every node is owned).
5
+ */
6
+ export declare function makeState(root: any, owned: any): {
7
+ root: any;
8
+ owned: any;
9
+ };
10
+ /** Ensure the root is owned before the first write into it. */
11
+ export declare function ownedRoot(state: any): any;
12
+ /**
13
+ * Return an owned version of `child`, writing the clone back into the
14
+ * (already owned) parent slot when one is taken.
15
+ */
16
+ export declare function ownedChild(state: any, parent: any, child: any, key: any): any;
17
+ /**
18
+ * The concrete array index a step addresses in `parent`, or -1 when the
19
+ * step cannot address an array element. Typed index steps resolve
20
+ * negative indexes from the end; the RFC 6901 form never does.
21
+ */
22
+ export declare function stepArrayIndex(parent: any, name: any, index: any): any;
23
+ /**
24
+ * Read the location `steps[0..len)` in `root` without cloning anything.
25
+ * Returns the shared NOTHING-style `miss` sentinel value passed in when
26
+ * the location does not exist (callers pick their own sentinel).
27
+ */
28
+ export declare function readSteps(root: any, names: any, indexes: any, len: any, miss: any): any;
@@ -0,0 +1,45 @@
1
+ import { CodedError } from '@jarenjs/core/errors';
2
+ /**
3
+ * Base for errors that carry a stable `code` and a `docPath`, an RFC
4
+ * 6901 JSON Pointer into the offending document. A wrapped error is
5
+ * exposed through the native `cause` option when one is given. This is
6
+ * the positional adapter over `@jarenjs/core`'s {@link CodedError}: a
7
+ * trailing `undefined` cause means "no cause" (no own property), per
8
+ * the base's contract.
9
+ */
10
+ export declare class CodedDocPathError extends CodedError {
11
+ /**
12
+ * @param {string} name - The public class name for `error.name`
13
+ * @param {string} code - Stable diagnosis code
14
+ * @param {string} reason - What is wrong (bare; the message is composed)
15
+ * @param {string} docPath - JSON Pointer into the offending document
16
+ * @param {unknown} [cause] - Wrapped error, when there is one
17
+ */
18
+ constructor(name: string, code: string, reason: string, docPath: string, cause?: unknown);
19
+ }
20
+ /**
21
+ * Base for syntax errors over a source string: `source` and `position`
22
+ * locate the offending token, and the message is prefixed with a
23
+ * human-readable language label. Deliberately NOT a {@link CodedError}:
24
+ * this family carries no stable code and locates by source offset, not
25
+ * by document pointer.
26
+ */
27
+ export declare class LabeledSyntaxError extends SyntaxError {
28
+ source: string;
29
+ position: number;
30
+ /**
31
+ * @param {string} name - The public class name for `error.name`
32
+ * @param {string} label - Language label for the message prefix
33
+ * @param {string} message - What is wrong
34
+ * @param {string} source - The offending source text
35
+ * @param {number} position - 0-based index of the offending token
36
+ */
37
+ constructor(name: string, label: string, message: string, source: string, position: number);
38
+ }
39
+ /**
40
+ * Build a `fail(code, message, docPath)` throw-adapter for a coded
41
+ * error class; compilers use it to keep rejection one-liners readable.
42
+ * @param {new (code: string, message: string, docPath: string) => Error} ErrorClass
43
+ * @returns {(code: string, message: string, docPath: string) => never}
44
+ */
45
+ export declare function failerFor(ErrorClass: new (code: string, message: string, docPath: string) => Error): (code: string, message: string, docPath: string) => never;
@@ -1,4 +1,7 @@
1
1
  export * from './basic.js';
2
+ export * from './canonical.js';
2
3
  export * from './pointer.js';
4
+ export * from './patch.js';
3
5
  export * from './path.js';
6
+ export * from './write.js';
4
7
  export * from './query/index.js';
@@ -1,18 +1,25 @@
1
+ import { CodedError } from '@jarenjs/core/errors';
2
+ import { CodedDocPathError } from '../errors.js';
1
3
  /**
2
4
  * Error thrown when a JSLT stylesheet is rejected at compile time
3
5
  * (`JT0xxx` codes).
4
6
  */
5
- export declare class JsltCompileError extends Error {
6
- code: any;
7
- docPath: any;
8
- constructor(code: any, message: any, docPath: any, cause?: undefined);
7
+ export declare class JsltCompileError extends CodedError {
8
+ /**
9
+ * @param {string} code
10
+ * @param {string} reason
11
+ * @param {string} docPath
12
+ * @param {...unknown} cause - When a fourth argument is passed AT
13
+ * ALL, it is retained as an own `cause` — even `undefined`, so a
14
+ * host hook that threw `undefined` stays distinguishable from "no
15
+ * cause".
16
+ */
17
+ constructor(code: string, reason: string, docPath: string, ...cause: unknown[]);
9
18
  }
10
19
  /**
11
20
  * Error thrown when evaluating a compiled JSLT stylesheet fails
12
21
  * (`JT2xxx` codes).
13
22
  */
14
- export declare class JsltRuntimeError extends Error {
15
- code: any;
16
- docPath: any;
17
- constructor(code: any, message: any, docPath: any, cause?: undefined);
23
+ export declare class JsltRuntimeError extends CodedDocPathError {
24
+ constructor(code: any, reason: any, docPath: any, cause?: undefined);
18
25
  }
@@ -1,4 +1,6 @@
1
1
  export { JsltCompileError, JsltRuntimeError } from './errors.js';
2
+ export { createJsltRegistry } from './registry.js';
3
+ export { mathPack, financePack, statsPack, allPacks } from './packs/index.js';
2
4
  /**
3
5
  * Compile a Jaren JSLT 0.1 stylesheet into a reusable transformation.
4
6
  *
@@ -17,6 +19,20 @@ export { JsltCompileError, JsltRuntimeError } from './errors.js';
17
19
  * [options.compileTypeTest] - validator-agnostic hook compiling schema
18
20
  * match conditions and schema literals inside query bodies
19
21
  * @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
22
+ * @param {boolean} [options.memo=false] - memoize rule outputs by
23
+ * (location, value reference): across repeated transforms of
24
+ * copy-on-write-updated documents, unchanged subtrees return the
25
+ * PREVIOUS output by reference — the fuel for reference-equality
26
+ * fast paths downstream (the @jarenjs/view patcher). Only rules whose
27
+ * output provably depends on nothing but the matched value are cached
28
+ * (no $root/$path/user externals, transitively through $apply, and no
29
+ * root references inside match-path filters); everything else runs
30
+ * normally. Memoized outputs MUST be treated as immutable, and the
31
+ * cache retains the previous transform's outputs (two generations).
32
+ * @param {Record<string, import('../path.js').JSONPathFunction>}
33
+ * [options.pathFunctions] - custom JSONPath function extensions (RFC
34
+ * 9535 section 2.4), available in rule match paths and in the path
35
+ * strings inside rule bodies
20
36
  * @returns {function} reusable `transform(data, externals?)` function
21
37
  * @throws {import('./errors.js').JsltCompileError} when compilation fails
22
38
  * @example
@@ -29,6 +45,7 @@ export { JsltCompileError, JsltRuntimeError } from './errors.js';
29
45
  export declare function compileJsltStylesheet(doc: any, options?: {
30
46
  compileTypeTest?: (schemaJson: any, docPath: string) => ((value: any) => boolean);
31
47
  maxDepth?: number;
48
+ memo?: boolean;
32
49
  }): Function;
33
50
  /**
34
51
  * Transform a JSON value with a JSLT stylesheet in one call.
@@ -41,6 +58,9 @@ export declare function compileJsltStylesheet(doc: any, options?: {
41
58
  * @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
42
59
  * [options.compileTypeTest] - schema type-test compiler
43
60
  * @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
61
+ * @param {boolean} [options.memo=false] - memoize rule outputs
62
+ * @param {Record<string, import('../path.js').JSONPathFunction>}
63
+ * [options.pathFunctions] - custom JSONPath function extensions
44
64
  * @returns {any} `undefined`, one JSON item, or an array of result items
45
65
  * @throws {import('./errors.js').JsltCompileError} when compilation fails
46
66
  * @throws {import('./errors.js').JsltRuntimeError} when dispatch fails
@@ -50,4 +70,6 @@ export declare function compileJsltStylesheet(doc: any, options?: {
50
70
  export declare function transformJson(stylesheet: any, data: any, externals?: object, options?: {
51
71
  compileTypeTest?: (schemaJson: any, docPath: string) => ((value: any) => boolean);
52
72
  maxDepth?: number;
73
+ memo?: boolean;
74
+ pathFunctions?: Record<string, import('../path.js').JSONPathFunction>;
53
75
  }): any;