@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
@@ -78,6 +78,13 @@ Two conformance roles exist:
78
78
  5. A sequence of exactly one item is identified with that item ("singleton ≡
79
79
  item"). A literal `42` and a one-item sequence containing `42` are
80
80
  indistinguishable.
81
+ 6. The **input document** a query is applied to MUST be an item — that is, a
82
+ JSON value. An implementation is NOT required to verify this (a deep check
83
+ would cost a full walk per call), so a non-JSON value passed in may simply
84
+ flow through as an opaque item. It MUST, however, reject a *missing*
85
+ document (JavaScript `undefined`) with `JQ2011`: a host API that spends
86
+ the absent value on the empty sequence would otherwise answer "empty" for
87
+ a query whose existence test says otherwise.
81
88
 
82
89
  ### 2.2 Effective boolean value (EBV)
83
90
 
@@ -190,11 +197,15 @@ evaluating to the empty sequence contributes none.
190
197
  Against a store with four book prices, this constructs a single 7-element
191
198
  array: `1`, the four prices, `2`, `3`.
192
199
 
193
- The same flattening applies to the member value expressions of a map
194
- constructor and to every other expression position; it is a property of the
195
- sequence data model, not of the array constructor alone. To embed an array
196
- *as a value* without evaluation, use `$const` (§3.5.1); to bind one without
197
- iteration, use `$let` (§6.3).
200
+ This flattening is a property of the sequence data model, not of the array
201
+ constructor alone; it applies in every expression position where a sequence can
202
+ flow. A **map-constructor member value** is the one position that cannot flatten
203
+ a JSON member holds exactly one value so it takes the single-value
204
+ cardinality rule instead: a member value that evaluates to the empty sequence
205
+ **omits the member**, a singleton becomes the member's value, and a value of two
206
+ or more items is runtime error `JQ2001`. To embed an array *as a value* without
207
+ evaluation, use `$const` (§3.5.1); to bind one without iteration, use `$let`
208
+ (§6.3).
198
209
 
199
210
  ### 3.5 Escape hatches
200
211
 
@@ -226,9 +237,12 @@ order:
226
237
 
227
238
  - *keyExpr* MUST evaluate to a single string; any other result (empty
228
239
  sequence, non-string, multi-item sequence) is runtime error `JQ2004`.
229
- - *valueExpr* is evaluated as an ordinary expression (its result flows per
230
- §3.4).
231
- - Later pairs win on duplicate keys.
240
+ - *valueExpr* is evaluated and taken as the member value under §3.4's
241
+ member-value cardinality: an empty result **omits the pair**, a singleton is
242
+ the value, and two or more items are runtime error `JQ2001`.
243
+ - Later pairs win on duplicate keys — but because an omitted (empty-valued) pair
244
+ is never written, an earlier pair for the same key **survives** a later pair
245
+ for that key whose value is the empty sequence.
232
246
 
233
247
  Each pair MUST be an array of exactly two expressions (`JQ0003` otherwise).
234
248
  An empty pair list constructs the empty object.
@@ -296,6 +310,17 @@ this, and later work uses it for static cardinality checks.
296
310
  An absolute path in any expression position reads from the one input
297
311
  document; there is no context-item drift — `$` is always the document root.
298
312
 
313
+ > **Footgun (variable-rooted filters).** A filter selector filters the
314
+ > *children* of each node it is applied to (RFC 9535 §2.3.5). So a filter
315
+ > written directly against a `$let`/`$for` variable tests that item's members,
316
+ > not the item itself. Given `{"$let": {"books": "$.store.book[*]"}}`, the path
317
+ > `"$books[?@.price > 20]"` applies the filter to the *members* of each book
318
+ > object and asks each member for a `.price` child — no member has one, so every
319
+ > book contributes the empty sequence and the whole path is empty. To filter the
320
+ > books themselves, place the predicate one level up, where the books are the
321
+ > children being filtered — `"$.store.book[?@.price > 20]"` — or iterate with
322
+ > `$for` and test in `$where` (§6.4).
323
+
299
324
  ### 5.2 The two filter dialects
300
325
 
301
326
  Two boolean mini-languages coexist in this format, and both keep the exact
@@ -340,13 +365,14 @@ choose the dialect deliberately when absent members are possible.
340
365
 
341
366
  ### 6.1 Shape and clause order
342
367
 
343
- A FLWOR phrase is an operator phrase containing `$for` and/or `$let`, plus
344
- optional clauses, plus the REQUIRED `$return`:
368
+ A FLWOR phrase is an operator phrase containing `$fold`, `$for` and/or
369
+ `$let`, plus optional clauses, plus the REQUIRED `$return`:
345
370
 
346
371
  | Key | Value | Presence |
347
372
  |---|---|---|
348
- | `$for` | binding object (§6.2) | at least one of `$for`, `$let` |
349
- | `$let` | binding object (§6.3) | at least one of `$for`, `$let` |
373
+ | `$fold` | one-member accumulator binding (§6.9) | at least one of `$fold`, `$for`, `$let` |
374
+ | `$for` | binding object (§6.2) | at least one of `$fold`, `$for`, `$let` |
375
+ | `$let` | binding object (§6.3) | at least one of `$fold`, `$for`, `$let` |
350
376
  | `$as` | schema assertion object (§6.8) | OPTIONAL |
351
377
  | `$where` | expression | OPTIONAL |
352
378
  | `$groupby` | binding object (§6.5) | OPTIONAL |
@@ -358,7 +384,7 @@ The clauses apply in **fixed semantic order regardless of their order in the
358
384
  JSON document** (**D7**):
359
385
 
360
386
  ```
361
- $for → $let → $as → $where → $groupby → $orderby → $count → $return
387
+ $fold → $for → $let → $as → $where → $groupby → $orderby → $count → $return
362
388
  ```
363
389
 
364
390
  JSON key order is not interoperable — several ecosystems (e.g. Go maps)
@@ -377,7 +403,9 @@ which is standard XQuery practice anyway:
377
403
  ```
378
404
 
379
405
  The FLWOR phrase evaluates to the concatenation of the `$return` results over
380
- the surviving tuple stream, in tuple order.
406
+ the surviving tuple stream, in tuple order — unless the phrase has a `$fold`
407
+ clause (§6.9), in which case it evaluates to the final accumulator and
408
+ `$return` names the accumulator's next value instead of an output item.
381
409
 
382
410
  ### 6.2 `$for` — iteration bindings
383
411
 
@@ -395,8 +423,8 @@ array, `$for` unpacks it into its members — one level only — and iterates
395
423
  those. This is the ergonomic default for the JSON data model, where `[*]`
396
424
  already produced values, not nodes. To bind an array *as a value*, use `$let`
397
425
  or `{"$const": [...]}`. Objects are NOT auto-iterated; iterate an object's
398
- values with a `[*]` path segment (a future `$entries` operator is the planned
399
- counterpart for member pairs).
426
+ values with a `[*]` path segment, or its member pairs with the `$entries`
427
+ operator (§8.9).
400
428
 
401
429
  **Multiple bindings** nest left-to-right in document key order, exactly like
402
430
  consecutive XQuery `for` clauses, and **may be correlated**: a later source
@@ -407,16 +435,18 @@ may reference variables bound earlier in the same `$for` object.
407
435
  > key-order-hostile stacks SHOULD emit one binding per phrase, nesting
408
436
  > phrases, instead of relying on multi-key binding objects.
409
437
 
410
- **Extended binding form** — a source written as
438
+ **Extended binding form** — a source written as an object with an `$in`
439
+ member iterates `$in` like a plain source, with options:
411
440
 
412
441
  ```json
413
442
  { "$in": expr, "$at": "posName" }
414
443
  ```
415
444
 
416
- iterates `expr` like a plain source and additionally binds *posName* to the
417
- **0-based** (D6) position of the current item within the iterated sequence.
418
- Both keys are REQUIRED in this form (a positionless binding is simply the
419
- plain form); *posName* MUST be a valid variable name.
445
+ binds *posName* to the **0-based** (D6) position of the current item within
446
+ the iterated sequence. `$in` is the only REQUIRED key (so a bare
447
+ `{"$in": expr}` is just the long spelling of the plain form); *posName* MUST
448
+ be a valid variable name. The two remaining options — `$allowing-empty` and
449
+ the `$window` family — are §6.10.
420
450
 
421
451
  ```json
422
452
  { "$for": { "b": { "$in": "$.store.book[*]", "$at": "i" } },
@@ -462,10 +492,14 @@ XQuery 3.1 group-by semantics:
462
492
 
463
493
  - Each *keyExpr* is evaluated per tuple; each *name* becomes a
464
494
  **grouping-key variable**, bound in every subsequent clause to that group's
465
- key value (a singleton per group).
495
+ key value (a singleton per group). A *keyExpr* MUST evaluate to the empty
496
+ sequence or a single item; a two-or-more-item key is runtime error `JQ2001`.
497
+ An **empty key is allowed** and groups with the other empty keys.
466
498
  - Tuples with equal key combinations form one group. Key equality is **deep
467
- structural JSON equality** (`equalsJson`-grade, the same relation as `$eq`
468
- item equality, §8.4), with numbers compared mathematically.
499
+ structural JSON equality** (`equalsJson`-grade), with numbers compared
500
+ mathematically. Unlike `$eq` item equality (§8.4), grouping treats **`NaN` as
501
+ equal to itself** (the XQuery grouping rule; §8.9's `$distinct` and `$sort`
502
+ cite the same relation).
469
503
  - Every other variable bound in the phrase is **rebound to the sequence** of
470
504
  its values across the group's tuples, in tuple order.
471
505
  - The tuple stream after `$groupby` has one tuple per group, in order of
@@ -497,11 +531,21 @@ A *keySpec* is either an expression (shorthand for ascending, empty-least) or
497
531
  the explicit form
498
532
 
499
533
  ```json
500
- { "$key": expr, "$dir": "asc" | "desc", "$empty": "least" | "greatest" }
534
+ { "$key": expr, "$dir": "asc" | "desc", "$empty": "least" | "greatest",
535
+ "$collation": "name" }
501
536
  ```
502
537
 
503
- with `$key` REQUIRED and `$dir` (default `"asc"`) and `$empty` (default
504
- `"least"`) OPTIONAL.
538
+ with `$key` REQUIRED and `$dir` (default `"asc"`), `$empty` (default
539
+ `"least"`) and `$collation` OPTIONAL.
540
+
541
+ `$collation` names a **registered pure compare function**
542
+ (`options.collations`, a trusted host capability like `options.functions`,
543
+ §8.12) applied to this key's *string* comparisons — the natural-language
544
+ orders (`Intl.Collator('nl').compare`, say) that the format's default
545
+ code-point order (§8.9) deliberately does not build in. Number keys keep
546
+ numeric order; an unregistered name is compile error `JQ0010`. Registered
547
+ collations appear in `query.dependencies`/`query.explain()` (§8.12), so a
548
+ saved rule declares the collations it needs.
505
549
 
506
550
  - An array value of `$orderby` is **always a list of key specs**, ordered
507
551
  major to minor, and MUST NOT be empty. Consequently a *single* key spec is
@@ -515,7 +559,9 @@ with `$key` REQUIRED and `$dir` (default `"asc"`) and `$empty` (default
515
559
  mathematically, strings by **Unicode scalar values** (code point order).
516
560
  Comparing any other combination (number with string, or a key that is a
517
561
  boolean, `null`, array, object, or multi-item sequence) is runtime error
518
- `JQ2005`.
562
+ `JQ2005`. A `NaN` key is a number: the comparator is total over `NaN`, ordering
563
+ it **equal to itself and less than every other number**, then falling through
564
+ to the next key on ties (§8.9's `$sort` cites this same rule).
519
565
  - Empty key sequences sort per `$empty`: `"least"` (default) places them
520
566
  first ascending / last descending; `"greatest"` the reverse.
521
567
 
@@ -577,6 +623,124 @@ instead.
577
623
  "$return": "$b.title" }
578
624
  ```
579
625
 
626
+ ### 6.9 `$fold` — the accumulator clause
627
+
628
+ ```json
629
+ "$fold": { name: initExpr }
630
+ ```
631
+
632
+ `$fold` turns the phrase from a *map* into a **reduction**. It binds exactly
633
+ one accumulator variable (a second member is `JQ0003`):
634
+
635
+ - *initExpr* is evaluated **once**, in the phrase's **enclosing** scope,
636
+ before the tuple stream starts. It therefore cannot reference this phrase's
637
+ own `$for`/`$let` bindings — such a name is free, and resolves as an
638
+ external (§9) rather than as the binding.
639
+ - *name* is in scope from `$let` onward — in `$let`, `$where`, `$groupby`,
640
+ `$orderby` and `$return` — but **not** in `$for` sources, which are
641
+ iterated once and must not depend on a value that changes per tuple.
642
+ - For each surviving tuple, `$return` is evaluated and the accumulator is
643
+ **rebound to its result**. `$return` names the accumulator's next value,
644
+ not an item of the output.
645
+ - The phrase evaluates to the **final accumulator**. If no tuple survives,
646
+ that is the initial value.
647
+
648
+ *name* counts as a binding of this phrase for the duplicate rule (`JQ0007`).
649
+ Because the accumulator is a binding rather than a lambda parameter, this
650
+ gives the language a general fold without giving the JSON encoding a way to
651
+ spell a **function value** — folds, running totals and pointer walks are
652
+ expressible; passing a function to an operator still is not.
653
+
654
+ `$fold` composes with every other clause. With `$orderby` the reduction runs
655
+ over the *sorted* tuples; with `$groupby` it updates once per group; `$where`
656
+ selects which tuples update it at all.
657
+
658
+ ```json
659
+ { "$fold": { "total": 0 },
660
+ "$for": { "b": "$.store.book[*]" },
661
+ "$where": { "$lt": ["$b.price", 10] },
662
+ "$return": { "$add": ["$total", "$b.price"] } }
663
+ ```
664
+
665
+ sums the prices of the cheap books. Because `$get` (§8.9) is a real dynamic
666
+ lookup, a fold over a runtime path is a pointer walk:
667
+
668
+ ```json
669
+ { "$fold": { "cur": "$.doc" },
670
+ "$for": { "seg": "$.path[*]" },
671
+ "$return": { "$get": ["$cur", "$seg"] } }
672
+ ```
673
+
674
+ resolves `$.path` — a sequence of member names and array indexes — against
675
+ `$.doc`, one segment per tuple.
676
+
677
+ ### 6.10 `$allowing-empty` and window clauses
678
+
679
+ Two further options of the extended `$for` binding form (§6.2).
680
+
681
+ **`$allowing-empty`** makes a binding **outer-join-style**: when the source
682
+ would yield no tuple at all, the clause emits exactly **one** tuple with the
683
+ variable bound to the **empty sequence**, so the enclosing tuple survives.
684
+
685
+ ```json
686
+ { "$in": expr, "$allowing-empty": true }
687
+ ```
688
+
689
+ The condition is "yields no tuple", not "the sequence was empty" — an item
690
+ that is an empty array contributes no members under D4 unpacking (§6.2) and
691
+ so also triggers it. When the binding also has `$at`, that tuple's position
692
+ is **-1**: every real position is a non-negative 0-based one (D6), so -1 is
693
+ the only value free to mean "no position".
694
+
695
+ Note the source is what must be empty. Because a path filter's `$` is the
696
+ input document and never a query variable (§3.2), a *correlated* source is
697
+ written as a nested phrase:
698
+
699
+ ```json
700
+ { "$for": { "b": "$.store.book[*]",
701
+ "r": { "$in": { "$for": { "x": "$.ratings[*]" },
702
+ "$where": { "$eq": ["$x.isbn", "$b.isbn"] },
703
+ "$return": "$x" },
704
+ "$allowing-empty": true } },
705
+ "$return": { "title": "$b.title", "stars": "$r.stars" } }
706
+ ```
707
+
708
+ keeps every book, rated or not; an unrated book's `stars` member is omitted
709
+ because an empty member value omits the member (§3.4).
710
+
711
+ **Windows** iterate consecutive *runs* of the item stream instead of single
712
+ items:
713
+
714
+ ```json
715
+ { "$in": expr, "$window": "tumbling" | "sliding", "$size": n, "$step": m, "$at": "w" }
716
+ ```
717
+
718
+ `$size` is REQUIRED with `$window` and MUST be a positive **integer literal**,
719
+ as MUST `$step` when present; they are not expressions, because a width that
720
+ varied per tuple could not be compiled into a specialized loop. `$size`/`$step`
721
+ without `$window`, or `$window` without `$size`, is `JQ0003`. The variable
722
+ binds the window's items **as a sequence** (not an array item); `$at` binds
723
+ the 0-based window number.
724
+
725
+ A window starts at item 0 and every `$step` items thereafter; `$step`
726
+ defaults to `$size` for `tumbling` and to `1` for `sliding`. The two kinds
727
+ differ only in what happens at the end of the stream:
728
+
729
+ - **`tumbling`** *partitions* the stream — every item belongs to exactly one
730
+ window — so a short final window IS emitted; dropping it would silently
731
+ lose data.
732
+ - **`sliding`** is a moving view of fixed width, so a short window is not one
733
+ of them: only full-width windows are emitted.
734
+
735
+ ```json
736
+ { "$for": { "w": { "$in": "$.readings[*]", "$window": "sliding", "$size": 3 } },
737
+ "$return": { "$avg": "$w" } }
738
+ ```
739
+
740
+ is a 3-point moving average; the same document with `"tumbling"` and no
741
+ `$step` averages disjoint blocks of three, including a final block of one or
742
+ two if the stream does not divide evenly.
743
+
580
744
  ---
581
745
 
582
746
  ## 7. Quantifier phrases
@@ -715,7 +879,10 @@ All arithmetic is IEEE double arithmetic (**D1**):
715
879
  XQuery *double* division semantics, not the decimal FOAR0001 error.
716
880
  - `$idiv` is **truncating division** (quotient rounded toward zero to an
717
881
  integral double); `$idiv` or `$mod` with a zero divisor is runtime error
718
- `JQ2002`.
882
+ `JQ2002`. Only a **zero divisor** errors: an `Infinity` or `NaN` *dividend*
883
+ over a finite non-zero divisor is not an error — both operators keep IEEE
884
+ double behavior (`Math.trunc` of the quotient, `%` for `$mod`) and yield
885
+ `NaN`, in keeping with D1's IEEE arithmetic.
719
886
  - `$mod` takes the sign of the dividend (XQuery `mod` semantics).
720
887
  - `$neg` is unary minus.
721
888
 
@@ -805,7 +972,7 @@ clause/operator collision note.
805
972
  | `$min` | empty | The least item (`fn:min`). Items MUST be all numbers or all strings (`JQ2001` otherwise, including mixed); numbers compare mathematically, strings by Unicode scalar values. A `NaN` item makes the result `NaN` (F&O). |
806
973
  | `$max` | empty | The greatest item (`fn:max`), same rules as `$min`. |
807
974
 
808
- ### 8.9 Sequence operators — `$distinct $reverse $sort $head $tail $subsequence $index-of $range $get`
975
+ ### 8.9 Sequence operators — `$distinct $reverse $sort $head $tail $subsequence $index-of $range $get $entries $from-entries`
809
976
 
810
977
  | Operator | Signature | Definition |
811
978
  |---|---|---|
@@ -818,6 +985,8 @@ clause/operator collision note.
818
985
  | `$index-of` | `[seq, item]` | The 0-based (D6) positions in *seq* of the items deep-equal to *item*, as a sequence, in order (`fn:index-of`). Equality is the `$eq` item relation (D2) — `NaN` matches nothing. *item* MUST be exactly one item (`JQ2001`). |
819
986
  | `$range` | `[start, end]` | The integers from *start* to *end* **inclusive** (the XQuery `to` operator). Either operand empty → empty; *start* > *end* → empty. A non-integral or non-number operand is `JQ2001`. A result of more than 2³² items is runtime error `JQ2007` (resource guard). |
820
987
  | `$get` | `[target, key]` | Dynamic lookup, the runtime counterpart of a path leaf: an object *target* with a string *key* yields the member value or empty; an array *target* with an integer *key* yields the element at that 0-based (D6) index — a negative index counts from the end, like the RFC 9535 index selector — or empty. **Every other combination** (wrong type pairing, non-integral index, empty or multi-item operands) is simply the empty sequence, never an error. |
988
+ | `$entries` | `{"$entries": e}` | The member-pair counterpart of a `[*]` path segment (which yields values only): each OBJECT item of the operand contributes one `{"key": name, "value": v}` pair per member, in member order; non-object items contribute nothing, and an empty operand yields the empty sequence. `$from-entries` is the inverse; the `$map` constructor (§3.5.2) is the fixed-arity form for computed keys. |
989
+ | `$from-entries` | `{"$from-entries": e}` | The inverse of `$entries`: assembles ONE object from the operand's `{"key": name, "value": v}` items, in sequence order — later pairs win on duplicate keys, exactly like the `$map` constructor. Items without a string `key` contribute nothing; a pair missing its `value` member reads as `null`; an empty operand constructs the empty object. |
821
990
 
822
991
  Combined example (also exercises the aggregates of §8.8):
823
992
 
@@ -927,6 +1096,265 @@ README's "`$query` — cross-field assertions" section.
927
1096
 
928
1097
  ---
929
1098
 
1099
+ ### 8.12 Registered functions, operators, collations, and execution limits
1100
+
1101
+ Three compile options make a compilation's **trusted host capabilities**
1102
+ explicit — none of them changes the closed format: a document using them
1103
+ compiles only against a host that registered them, and a host that
1104
+ registered nothing keeps exactly the spec vocabulary.
1105
+
1106
+ **`options.functions` and `$call`.** A registry of named pure functions;
1107
+ the `$call` phrase invokes one:
1108
+
1109
+ ```json
1110
+ { "$call": ["upper", "$b.title"] }
1111
+ ```
1112
+
1113
+ The first item MUST be a literal string naming a registered function
1114
+ (`JQ0010` otherwise). Argument expressions evaluate first; each crosses
1115
+ the boundary as plain JSON — a sequence as an array of items, the empty
1116
+ sequence as `undefined`. The function's return value is one item;
1117
+ `undefined` is the empty sequence. A throwing function is runtime error
1118
+ `JQ2010`. Functions MUST be pure over JSON: they are part of the query's
1119
+ semantics, not an effect hatch.
1120
+
1121
+ **`options.extensions` and host `$`-operators.** A registry of named
1122
+ operator entries that extend the closed operator vocabulary (§8) with
1123
+ host-provided `$`-operators. Each entry has the same shape the built-in
1124
+ operators use — `{ params, result, resultType, compile, normalize? }` —
1125
+ so a registered `{ "$sqrt": "$b.x" }` normalizes, type-annotates (its
1126
+ declared `resultType` flows through `annotateTypes`, Appendix C.8) and
1127
+ compiles exactly like a built-in, and appears in
1128
+ `query.dependencies.operators`. A name that collides with the closed
1129
+ vocabulary is a `TypeError` at compile — a host programming error, never
1130
+ a `JQ` document error. The intended way to build this registry is
1131
+ `createJsltRegistry()` from `@jarenjs/json/jslt` (JSLT-FORMAT §13), which
1132
+ composes plain-data packs of pure `@jarenjs/core` functions (math,
1133
+ finance, statistics) into `{ extensions, functions }` and works across
1134
+ stylesheets, bare queries and `@jarenjs/linq` — and, through
1135
+ `@jarenjs/db`, in the query residual and (for the `pushable:'scalar'`
1136
+ subset) as SQLite deterministic UDFs (MODEL-FORMAT §8.1–8.2). A document
1137
+ compiled *without* a registry keeps exactly the spec vocabulary — `$sqrt`
1138
+ is then `JQ0002`.
1139
+
1140
+ **`options.collations`.** A registry of named pure compare functions for
1141
+ `$orderby`'s `$collation` member (§6.6).
1142
+
1143
+ **`options.limits`.** Deterministic limits enforced *inside* the
1144
+ synchronous engine. Two of them are **output caps**: they bound what a
1145
+ phrase or the query hands onward, NOT memory, fan-out or intermediate
1146
+ accumulation.
1147
+
1148
+ - `sequenceItems` — bounds every FLWOR phrase materialization (the
1149
+ sequence a phrase *returns*) and tightens `$range`'s resource guard
1150
+ below its 2³² ceiling; exceeding it is `JQ2009` (`$range` keeps its
1151
+ historical `JQ2007`). A `$groupby`/`$orderby` barrier may accumulate
1152
+ arbitrarily many items — and a collation may run arbitrarily many
1153
+ comparisons — behind a small final output; bare paths, `$count` and
1154
+ other operators can likewise materialize above the cap internally.
1155
+ - `resultItems` — bounds the final result at the query boundary
1156
+ (`JQ2009`), checked after evaluation; `first()`, `exists()` and
1157
+ `ebv()` deliberately bypass it.
1158
+
1159
+ The other two bound the **query** rather than its output:
1160
+
1161
+ - `steps` — bounds **expression-node evaluations** (`JQ2009`). A step is
1162
+ one node evaluation, not one primitive operation: a node that loops
1163
+ internally — materializing a `$range`, a general comparison's cross
1164
+ product, a sort's comparisons — counts once. It is the only limit that
1165
+ bounds work rather than output, and the only one that costs: setting
1166
+ it compiles a counter check into every node. The counter resets per
1167
+ evaluation, so a compiled query stays reusable.
1168
+ - `depth` — bounds **expression nesting**, and is checked at **compile
1169
+ time** (`JQ0011`). The language has no recursion — no user-defined
1170
+ functions, no self-reference — so the compiled closure tree's maximum
1171
+ evaluation depth IS the document's static nesting. Checking it once is
1172
+ therefore exact, and costs nothing to evaluate.
1173
+
1174
+ An unknown limit name, or a value that is not a positive integer, is a
1175
+ host programming error (`TypeError`), because an accepted-but-unenforced
1176
+ limit would be a silent false guarantee.
1177
+
1178
+ Together these make trusted, developer-authored rules diagnosable, and
1179
+ give user- or model-authored queries a deterministic work bound. They
1180
+ still are not a sandbox: `steps` bounds evaluations, not memory, and a
1181
+ wall-clock or CPU limit is out of scope by design — a synchronous run on
1182
+ the caller's thread cannot be preempted, so a host needing hard
1183
+ termination owns a worker or isolate.
1184
+
1185
+ **Dependencies and explanation.** The compiled query reports what it
1186
+ needs: `query.dependencies` is a frozen `{ externals, operators,
1187
+ functions, collations }`, and `query.explain()` returns that plus the
1188
+ enforced limits as fresh plain JSON — the vetting surface for saved or
1189
+ machine-authored rules.
1190
+
1191
+ ### 8.13 Dates and times
1192
+
1193
+ JSON has no date type, so dates are **RFC 3339 strings** and these operators
1194
+ are ordinary string operators with a calendar's worth of rules. Every one of
1195
+ them is a **pure function of its operand**: there is deliberately no
1196
+ `current-dateTime`, because a compiled query must give the same answer for
1197
+ the same input document forever — it is cached by document identity, saved as
1198
+ a rule, and usable as a validation keyword.
1199
+
1200
+ **Type predicates.** Unary `{"$is-date": e}`, `$is-time`, `$is-datetime`,
1201
+ `$is-duration` — `true` iff `e` is a **singleton string** in that lexical
1202
+ form (`full-date`, `full-time`, `date-time`, and the RFC 3339 Appendix A
1203
+ duration grammar). Like the §8.10 `$is-*` family these never raise: the empty
1204
+ sequence, a multi-item sequence and a non-string are all `false`. The forms
1205
+ are disjoint — a `date-time` is not a `date`.
1206
+
1207
+ **Components.** Unary `$year`, `$month`, `$day`, `$hours`, `$minutes`,
1208
+ `$seconds`, `$offset`. Each propagates the empty sequence and returns a
1209
+ number. Components are read **lexically, in the value's own offset** — the
1210
+ `fn:year-from-dateTime` reading, and the one that makes "group by month"
1211
+ mean what an author expects. `$seconds` carries the fraction (`5.5`).
1212
+ `$offset` is minutes east of UTC, and is the one component that yields the
1213
+ **empty sequence** rather than an error when absent, because RFC 3339 leaves
1214
+ a bare `full-date` offset-less.
1215
+
1216
+ Asking a value for a component of a half it does not have — the `$hours` of a
1217
+ `full-date`, the `$year` of a `full-time` — is runtime error `JQ2001`, as is
1218
+ an operand that is not an RFC 3339 value at all.
1219
+
1220
+ **Instants.** Unary `$epoch` maps a value carrying a date to **milliseconds
1221
+ since 1970-01-01T00:00:00Z**, and `$datetime` maps such a number back to a
1222
+ canonical UTC `date-time` string. `$epoch` is the one place a value is
1223
+ shifted to UTC, which makes it the way to compare or subtract across
1224
+ offsets — as *strings*, `"…T14:00:00+02:00"` sorts after `"…T12:00:00Z"`
1225
+ though they are the same instant. A `full-time` has no instant to place
1226
+ (`JQ2001`); so does a number outside the range RFC 3339 can spell.
1227
+
1228
+ **Calendar arithmetic.** `$date-add` and `$date-sub` shift a value, either
1229
+ by an ISO 8601 duration (`[date, "P1M"]`) or by an amount and a unit
1230
+ (`[date, 3, "day"]`). `$start-of` and `$end-of` truncate to a unit, and
1231
+ `$date-diff` counts whole units from one value to another. The unit is
1232
+ **data**, not vocabulary — one of `year`, `quarter`, `month`, `week`, `day`,
1233
+ `hour`, `minute`, `second`, `millisecond` — so an unknown one is `JQ2001`
1234
+ rather than a compile error.
1235
+
1236
+ Two rules make these predictable:
1237
+
1238
+ - **The lexical form is preserved.** A `full-date` shifted by a day is still
1239
+ a `full-date`, and a `date-time` keeps its own offset rather than being
1240
+ normalized to UTC. A query that buckets dates must not silently start
1241
+ producing date-times. Consequently `$end-of` on a `full-date` yields that
1242
+ unit's last *day*, where on a `date-time` it yields the last millisecond —
1243
+ a `full-date` has nowhere to put one.
1244
+ - **Month arithmetic clamps.** `2026-01-31` plus one month is `2026-02-28`,
1245
+ because the alternative — overflowing into March — makes adding a month
1246
+ non-monotonic. `$date-diff` counts months to match, so adding its result
1247
+ back never overshoots: `2026-01-31` to `2026-02-28` is **one** month.
1248
+
1249
+ `$date-format` renders a value through a **Unicode LDML** pattern
1250
+ (`yyyy-MM-dd`, not moment's `YYYY-MM-DD`); a literal pattern compiles once
1251
+ with the query. Patterns are limited to the locale-independent tokens: month
1252
+ and weekday *names* would need locale data this format does not carry, so
1253
+ `MMMM`, `MMM`, `EEEE`, `EEE` and `a` are rejected — `JQ0003` for a literal
1254
+ pattern, `JQ2001` for one computed at runtime. Localized rendering belongs to
1255
+ the presentation layer, not to a query.
1256
+
1257
+ | Operator | Definition |
1258
+ |---|---|
1259
+ | `$is-date` `$is-time` `$is-datetime` `$is-duration` | singleton string in that RFC 3339 form → `true`; anything else → `false` |
1260
+ | `$year` `$month` `$day` | lexical date components; a value with no date is `JQ2001` |
1261
+ | `$hours` `$minutes` `$seconds` | lexical time components, `$seconds` including its fraction; a value with no time is `JQ2001` |
1262
+ | `$offset` | minutes east of UTC; a bare `full-date` → empty |
1263
+ | `$week` `$week-year` | ISO 8601 week number and its week-numbering year, which is not always the calendar year (2027-01-01 is week 53 of 2026) |
1264
+ | `$quarter` `$weekday` | calendar quarter 1-4; ISO weekday 1 (Monday) to 7 (Sunday) |
1265
+ | `$epoch` | date or date-time → milliseconds since the epoch (UTC); a `full-time` → `JQ2001` |
1266
+ | `$datetime` | epoch milliseconds → canonical UTC `date-time`; out of RFC 3339 range → `JQ2001` |
1267
+ | `$date-add` `$date-sub` | `[date, duration]` or `[date, amount, unit]` → a value of the same lexical form |
1268
+ | `$start-of` `$end-of` | `[date, unit]` → the unit's first / last instant, in the same lexical form |
1269
+ | `$date-diff` | `[from, to, unit]` → whole units, negative when `to` precedes `from` |
1270
+ | `$date-format` | `[date, pattern]` → the value rendered through an LDML pattern |
1271
+
1272
+ Every operator that *produces* a date produces it in the same canonical
1273
+ spelling, so one query can never emit two forms of one instant: a fractional
1274
+ second appears only when non-zero and without trailing zeros
1275
+ (`…:05.5Z`, never `…:05.500Z`).
1276
+
1277
+ Durations are recognized and applied, but never *decomposed* into a number:
1278
+ `P1M` is not a fixed count of milliseconds, so there is no honest length to
1279
+ report without a calendar anchor. Applying one to a date is where the anchor
1280
+ exists, which is what `$date-add` is for; fixed-width spans go through
1281
+ `$epoch` and ordinary `$sub`.
1282
+
1283
+ ```json
1284
+ { "$for": { "e": "$.events[*]" },
1285
+ "$groupby": { "w": { "$start-of": ["$e.on", "week"] } },
1286
+ "$orderby": ["$w"],
1287
+ "$return": { "week": "$w", "count": { "$count": "$e" } } }
1288
+ ```
1289
+
1290
+ buckets events into ISO weeks — the shape components alone could not express,
1291
+ because a week boundary is arithmetic, not a field.
1292
+
1293
+ ```json
1294
+ { "$for": { "e": "$.events[*]" },
1295
+ "$where": { "$is-datetime": "$e.at" },
1296
+ "$groupby": { "y": { "$year": "$e.at" }, "m": { "$month": "$e.at" } },
1297
+ "$orderby": ["$y", "$m"],
1298
+ "$return": { "year": "$y", "month": "$m", "count": { "$count": "$e" } } }
1299
+ ```
1300
+
1301
+ ### 8.14 Spatial
1302
+
1303
+ Geography enters the language the way dates did: through the format the data
1304
+ already has. Operands are **GeoJSON** ([RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946))
1305
+ — a bare position `[longitude, latitude]`, a geometry, a `Feature`, or a
1306
+ `FeatureCollection` — because that is what a JSON document holds. There is no
1307
+ geometry type to construct, and a wrapper is unwrapped for you: passing a
1308
+ `Feature` where a geometry is wanted is not an error, it is the common case.
1309
+
1310
+ Coordinates are longitude then latitude, in WGS 84 decimal degrees. RFC 7946
1311
+ removed alternative coordinate reference systems, so there is nothing to
1312
+ configure and no `crs` to honour.
1313
+
1314
+ **Measurements are geodesic, never planar.** A degree of longitude spans about
1315
+ 111 km at the equator and 68 km at 52°N, so a Euclidean answer over raw degrees
1316
+ is wrong by two thirds over a kilometre at Dutch latitudes. `$distance`,
1317
+ `$area` and `$length` answer in metres and square metres on the WGS 84 sphere,
1318
+ accurate to under half a percent. Convert with ordinary arithmetic —
1319
+ `{"$idiv": [{"$distance": [a, b]}, 1000]}` for kilometres.
1320
+
1321
+ `$distance` and `$within` measure a value by its **representative position**: a
1322
+ bare position or `Point` is itself, anything else is its centroid. That is
1323
+ stated rather than inferred because the alternative — the minimum distance
1324
+ between two shapes — needs point-to-segment geodesics and is a much larger
1325
+ piece of work this format does not yet do.
1326
+
1327
+ `$bbox-intersects` is named for exactly what it tests. An operator called
1328
+ `$intersects` that compared only bounding boxes would be a lie the first time
1329
+ two L-shaped regions shared a box and nothing else; real geometry-to-geometry
1330
+ intersection is overlay work and is deliberately absent.
1331
+
1332
+ | Operator | Definition |
1333
+ |---|---|
1334
+ | `$bbox` | any value → `[west, south, east, north]`; a value with no positions → empty |
1335
+ | `$area` | square metres of the value's polygons, exterior rings less holes; anything without a surface → `0` |
1336
+ | `$length` | metres of the value's lines and ring perimeters; a point → `0` |
1337
+ | `$centroid` | the mean of the value's positions, as a position. **Not** the area-weighted centre of mass: for a concave shape it can fall outside the polygon |
1338
+ | `$distance` | `[a, b]` → metres between the two representative positions |
1339
+ | `$within` | `[a, b]` → is `a`'s representative position inside `b`'s surface? Only a polygon has an inside, so a line or point as `b` is `false` |
1340
+ | `$bbox-intersects` | `[a, b]` → do the two bounding boxes overlap? Touching edges count |
1341
+ | `$geohash` | `[value]` or `[value, precision]` → the base-32 cell string; precision is 1-12, default 9 |
1342
+
1343
+ Note what needs **no** operator. A geohash is a string, so proximity is
1344
+ `$starts-with` on a prefix and spatial bucketing is `$groupby` over
1345
+ `$substring` — the existing vocabulary already indexes, groups and orders them.
1346
+
1347
+ ```json
1348
+ { "$for": { "c": "$.cities[*]" },
1349
+ "$where": { "$within": ["$c.at", "$.region"] },
1350
+ "$orderby": [{ "$key": { "$distance": ["$c.at", "$.centre"] } }],
1351
+ "$return": { "name": "$c.name",
1352
+ "km": { "$idiv": [{ "$distance": ["$c.at", "$.centre"] }, 1000] } } }
1353
+ ```
1354
+
1355
+ selects the cities inside a region and orders them by how far they are from a
1356
+ point — a spatial filter and a spatial sort, in the language's own clauses.
1357
+
930
1358
  ## 9. Variables, scoping, and external parameters
931
1359
 
932
1360
  1. Variables are introduced by `$for`, `$let`, `$at`, `$count`, `$groupby`
@@ -952,6 +1380,33 @@ README's "`$query` — cross-field assertions" section.
952
1380
 
953
1381
  `$minPrice` is free — an external the caller binds at call time.
954
1382
 
1383
+ **Closed-world compilation.** Because use is the declaration, a typo in a
1384
+ variable name is not an error: it quietly becomes a new external. A host that
1385
+ knows the parameters it intends to expose MAY compile **closed-world**,
1386
+ declaring them (`options.externals` in this implementation). Every free
1387
+ variable that is not declared is then compile error `JQ0005` at its own
1388
+ reference site, and an empty declaration list forbids externals entirely.
1389
+ This changes no document semantics — a document that compiles closed-world
1390
+ behaves identically compiled open — it only decides which documents compile.
1391
+
1392
+ > **Worked example (clause order and `$count`).** Scope follows the
1393
+ > **semantic** clause order of §6.1, not document key order, and `$count`
1394
+ > binds *after* `$where`. So a `$where` that mentions the phrase's own
1395
+ > `$count` name does not see the tuple number — the name is not in scope
1396
+ > yet, and rule 3 makes it an **external**:
1397
+ >
1398
+ > ```json
1399
+ > { "$for": { "b": "$[*]" },
1400
+ > "$where": { "$lt": ["$n", 2] },
1401
+ > "$count": "n",
1402
+ > "$return": ["$b", "$n"] }
1403
+ > ```
1404
+ >
1405
+ > `$n` in `$where` is external (the compiled query reports `["n"]`); `$n` in
1406
+ > `$return` is the tuple number. This is correct and surprising, which is why
1407
+ > it is worth stating: filtering by position is done with `$at` (§6.2), whose
1408
+ > variable is in scope from the binding onward, not with `$count`.
1409
+
955
1410
  ---
956
1411
 
957
1412
  ## 10. Errors
@@ -974,11 +1429,13 @@ runtime errors as `JsonQueryRuntimeError`. Every error carries:
974
1429
  | `JQ0002` | Unknown operator / `$`-key outside the vocabulary | XPST0017 |
975
1430
  | `JQ0003` | Known phrase with bad arity, value shape, or key combination | XPST0003 |
976
1431
  | `JQ0004` | String starting `$` is not a valid path or escape (§3.2) | XPST0003 |
977
- | `JQ0005` | Variable reference that is neither bound nor collectible as an external (reserved for closed-world compilation modes; see §9); also an `$as` member naming a variable not bound by its phrase's `$for`/`$let` (§6.8) | XPST0008 |
1432
+ | `JQ0005` | Variable reference that is neither bound nor a declared external under a closed-world compilation (§9); also an `$as` member naming a variable not bound by its phrase's `$for`/`$let` (§6.8) | XPST0008 |
978
1433
  | `JQ0006` | Version envelope with unknown or non-string `$query` (§4.2) | XQST0031 |
979
1434
  | `JQ0007` | Duplicate variable binding within one phrase (§6.3) | XQST0089 |
980
1435
  | `JQ0008` | Schema operator (`$valid`/`$assert`/`$as`) in a query compiled without a type-test compiler (§8.11) | XQST0009 |
981
1436
  | `JQ0009` | Schema literal rejected by the type-test compiler (invalid embedded schema, §8.11) | XQST0059 |
1437
+ | `JQ0010` | `$call`/`$collation` naming no registered function/collation (§8.12, §6.6) | XPST0017 |
1438
+ | `JQ0011` | Expression nesting deeper than `limits.depth` (§8.12) | XPDY0130 |
982
1439
 
983
1440
  ### 10.3 Runtime errors (`JQ2xxx`)
984
1441
 
@@ -992,6 +1449,9 @@ runtime errors as `JsonQueryRuntimeError`. Every error carries:
992
1449
  | `JQ2006` | Reference to an unbound external parameter (§9) | XPDY0002 |
993
1450
  | `JQ2007` | Resource guard: an operator result exceeding an implementation limit (`$range` over 2³² items, §8.9) | XPDY0130 |
994
1451
  | `JQ2008` | Schema assertion failure: an item rejected by `$assert`'s schema, or a bound variable rejected by its `$as` schema (§6.8, §8.11) | XPTY0004 |
1452
+ | `JQ2009` | An execution limit exceeded: `limits.sequenceItems` on a phrase materialization, `limits.resultItems` at the query boundary, or `limits.steps` expression evaluations (§8.12) | XPDY0130 |
1453
+ | `JQ2010` | A registered `$call` function threw (§8.12) | FOER0000 |
1454
+ | `JQ2011` | The input document is `undefined`, which is not a JSON value (§2.1) | XPDY0002 |
995
1455
 
996
1456
  ---
997
1457
 
@@ -1061,8 +1521,9 @@ The schema cannot express, and therefore leaves to the compiler (stated in
1061
1521
  `description`s in the artifacts): fixed clause ordering (semantic, not
1062
1522
  structural — every key order is valid JSON), variable scoping and duplicate
1063
1523
  detection (`JQ0005`/`JQ0007`), `$as` name binding (`JQ0005`), full grammar
1064
- of variable-rooted path segments (only the head is pattern-checked), and all
1065
- runtime typing rules. Schema-literal positions (§8.11) validate as `true`
1524
+ of variable-rooted path segments (only the head is pattern-checked), the
1525
+ co-occurrence rules of the extended `$for` binding (`$size`/`$step` require
1526
+ `$window`, `$window` requires `$size`, §6.10), and all runtime typing rules. Schema-literal positions (§8.11) validate as `true` —
1066
1527
  draft-neutral by definition; embedded JSON Schemas are deliberately **not**
1067
1528
  meta-validated by these artifacts (the type-test compiler is authoritative,
1068
1529
  `JQ0009`). Where the schema and this text disagree, this text wins and the
@@ -1210,6 +1671,20 @@ emit a bespoke query DSL as free text and parsing it hopefully — and the
1210
1671
  trade is: grammar errors eliminated by construction, semantic errors reduced
1211
1672
  to a machine-checkable, machine-repairable residue.
1212
1673
 
1674
+ Provider structured-output modes support different JSON Schema subsets
1675
+ regardless of the draft they advertise, and the strict ones enforce neither
1676
+ `patternProperties`/`propertyNames` nor asserted `format`s and reject
1677
+ `oneOf`. For those, a third artifact ships beside the two of §12.1:
1678
+ `packages/json/schemas/jaren-query.llm-profile.schema.json`, `$id`
1679
+ `https://jarenjs.dev/schemas/jaren-query/0.1/llm-profile` — a mechanically
1680
+ derived, pure *relaxation* in which those constraints are removed (each
1681
+ restated in the nearest `description`, which the model still reads) and
1682
+ every `oneOf` becomes `anyOf`. Every canonical-valid query document
1683
+ validates under the profile; the reverse is deliberately not guaranteed.
1684
+ Hand the profile to the decoder, keep validating locally against the
1685
+ canonical schema — the package [README](../README.md#generating-queries-with-llms)
1686
+ walks the full pipeline.
1687
+
1213
1688
  Because query documents are plain JSON, they also travel well through the
1214
1689
  rest of an LLM toolchain: function-call arguments, retrieval filters, and
1215
1690
  audit logs all speak JSON already, and a generated query can be validated,
@@ -1219,3 +1694,190 @@ The same constrained-decoding model applies to complete recursive
1219
1694
  stylesheets; see
1220
1695
  [JSLT-FORMAT Appendix B](./JSLT-FORMAT.md#appendix-b-llm-structured-output-non-normative)
1221
1696
  and its mechanically query-derived schema twins.
1697
+
1698
+ ## Appendix C. The normalized form (normative)
1699
+
1700
+ The engine compiles in two stages: stage 1 **normalizes** a query
1701
+ document into a frozen abstract syntax tree (the grammar of §§3–9
1702
+ resolved — object partitioning, phrase classification, string forms,
1703
+ scope resolution, cardinality analysis); stage 2 specializes that tree
1704
+ into closures. Stage 2 is an optimisation artifact and changes freely.
1705
+ **Stage 1 is the language resolved, and this appendix publishes it as a
1706
+ contract**: `analyzeQuery` (the package's `./query` subpath) returns the
1707
+ normalized tree so a consumer — a translator, a planner, an analyzer —
1708
+ can walk *the engine's own reading* of a document instead of inventing a
1709
+ second one.
1710
+
1711
+ ### C.1 The analysis entry point
1712
+
1713
+ ```js
1714
+ analyzeQuery(doc, options) -> {
1715
+ astVersion, // integer; see the compatibility policy (C.7)
1716
+ root, // the frozen node tree (C.3)
1717
+ externals, // [{ name, slot }] in order of first appearance (§9)
1718
+ frameSize, // total frame slots the tree addresses (C.5)
1719
+ dependencies, // { externals, operators, functions, collations }
1720
+ limits, // the normalized limits record, or null
1721
+ }
1722
+ ```
1723
+
1724
+ `analyzeQuery` accepts the same options as `compileJsonQuery` and
1725
+ applies the same JQ0xxx rejections, with one deliberate difference:
1726
+ **schema literals do not require `options.compileTypeTest`**. Where
1727
+ compilation without the hook is `JQ0008`, analysis normalizes the schema
1728
+ literal to its `raw` node carrying the frozen schema with **no compiled
1729
+ predicate** (`test` is `null`), so a consumer can analyse a document it
1730
+ could not execute. When the hook IS supplied, analysis compiles the
1731
+ predicate exactly as compilation would (and can therefore still raise
1732
+ `JQ0009`). Everything else — `$call`/`$collation` registry resolution,
1733
+ closed-world externals, limits validation — behaves identically in both
1734
+ modes.
1735
+
1736
+ A caller wanting both pays for one normalization:
1737
+ `compileJsonQuery(doc, { analysis: true })` exposes the same record at
1738
+ `query.analysis` on the compiled query.
1739
+
1740
+ `NODE_KINDS` (same subpath) is the frozen, sorted list of the twelve
1741
+ node kinds of C.3; `AST_VERSION` is the current version integer.
1742
+
1743
+ ### C.2 The cardinality lattice
1744
+
1745
+ Every node carries `card`, a static **upper approximation** of its
1746
+ runtime sequence length:
1747
+
1748
+ | value | constant | meaning |
1749
+ |---|---|---|
1750
+ | 0 | `CARD_ZERO` | statically the empty sequence |
1751
+ | 1 | `CARD_ONE` | always exactly one item |
1752
+ | 2 | `CARD_OPT` | zero or one item |
1753
+ | 3 | `CARD_MANY` | any number of items (the top) |
1754
+
1755
+ Two combinators are part of the contract: `joinCard(a, b)` is the least
1756
+ upper bound (the cardinality of "one branch or the other", `$if`), and
1757
+ `sumCard(a, b)` is concatenation (`$seq`): `ZERO` is its identity and
1758
+ any two non-`ZERO` contributions give `MANY`. Path nodes additionally
1759
+ carry `singular` (C.3), the RFC 9535 singular-query judgement of their
1760
+ segment list.
1761
+
1762
+ ### C.3 The twelve node kinds
1763
+
1764
+ Every node is a plain frozen object carrying at least
1765
+ `{ kind, card, docPath }`. The complete field sets:
1766
+
1767
+ | kind | fields beyond `kind`/`card`/`docPath` |
1768
+ |---|---|
1769
+ | `literal` | `value` — a frozen JSON value (scalars, `$const` payloads, non-`$` strings) |
1770
+ | `var` | `slot`, `external` (boolean), `name` (`'$'` for the input document, slot 0) |
1771
+ | `path` | `name` (root variable), `rootSlot`, `external`, `rootCard`, `segments` (the frozen RFC 9535 segment list of `parseJSONPath`), `singular` |
1772
+ | `object` | `entries` — `[{ name, expr }]`, the map constructor of Rule 1 |
1773
+ | `map` | `pairs` — `[{ key, value }]`, the general `$map` constructor |
1774
+ | `array` | `elements` — `[node]`, the Rule 3 array constructor |
1775
+ | `raw` | `value` — a frozen verbatim JSON value (operator `raw`/`name`/`schema` argument positions); schema positions also carry `test` — the compiled predicate, or `null` under analysis without a hook |
1776
+ | `op` | `name`, `args` — `[node]`; `$range` under `limits` also carries `limits`; host extension operators also carry `entry` (the host's registry entry, an opaque host value) |
1777
+ | `call` | `name`, `fn` (the registered host function — an opaque host value), `args` |
1778
+ | `let` | `bindings` — `[{ name, slot, expr }]`, `ret` (the degenerate `{$let, $return}` phrase) |
1779
+ | `quant` | `some` (boolean), `bindings` — `[{ name, slot, expr }]`, `satisfies` |
1780
+ | `flwor` | see C.4 |
1781
+
1782
+ **Host-valued members.** `raw.test`, `call.fn`, `op.entry` and an
1783
+ orderby spec's `collation` are the four places the tree carries host
1784
+ functions rather than JSON. A consumer that serializes or diffs the tree
1785
+ MUST treat them as opaque presence/absence facts; everything else in the
1786
+ tree is plain JSON.
1787
+
1788
+ ### C.4 The `flwor` node
1789
+
1790
+ The full phrase (§6) normalizes to one node with the clauses in their
1791
+ fixed semantic order regardless of JSON key order:
1792
+
1793
+ - `fold` — `null` or `{ name, slot, expr, docPath }` (§6.9); the initial
1794
+ value is normalized in the ENCLOSING scope.
1795
+ - `forBindings` — `[{ name, slot, expr, atSlot, allowingEmpty, window }]`;
1796
+ `atSlot` is `-1` without `$at`; `window` is `null` or
1797
+ `{ sliding, size, step }` (§6.10).
1798
+ - `letBindings` — `[{ name, slot, expr }]`.
1799
+ - `asChecks` — `null` or `[{ name, slot, isLet, schema, test, docPath }]`
1800
+ (§6.8); `test` is `null` under analysis without a hook.
1801
+ - `where` — `null` or a node.
1802
+ - `groupby` — `null` or `{ keys: [{ name, slot, expr, docPath }],
1803
+ docPath, accSlots }`; `accSlots` is the frozen list of pre-group
1804
+ binding slots that later clauses actually read (barrier liveness — an
1805
+ over-approximation-free artifact of `collectReadSlots`).
1806
+ - `orderby` — `null` or `{ specs: [{ key, desc, emptyGreatest,
1807
+ collation, collationName, docPath }], docPath, liveSlots }`;
1808
+ `liveSlots` is the analogous snapshot liveness list at the sort
1809
+ barrier.
1810
+ - `count` — `null` or `{ name, slot }`.
1811
+ - `ret` — the `$return` node.
1812
+ - `limits` — the normalized limits record, or `null`.
1813
+
1814
+ ### C.5 Slots, frames, and external resolution
1815
+
1816
+ A compiled query evaluates against one frame array. **Slot 0 is the
1817
+ input document.** Every binding site — `$for` names, `$at` names, `$let`
1818
+ names, `$fold`'s accumulator, `$groupby` key names, `$count` — and every
1819
+ external parameter is allocated the next slot from a single counter, in
1820
+ normalization order; nested phrases keep allocating in the same frame.
1821
+ `frameSize` is the final counter (one more than the highest slot; under
1822
+ `limits.steps` one extra slot holds the step counter). Scoping is
1823
+ lexical: a name resolves to the innermost binding; a free name is an
1824
+ **external parameter**, allocated a slot at its first appearance (use is
1825
+ the declaration, §9) and reported in `externals` in that order. Under a
1826
+ closed-world compilation (`options.externals`) a free name outside the
1827
+ declared set is `JQ0005` at its own reference site. An external's `card`
1828
+ is `CARD_ONE` (the caller binds one JSON value).
1829
+
1830
+ ### C.6 `docPath` and the freezing guarantee
1831
+
1832
+ Every node's `docPath` is an RFC 6901 JSON Pointer into the query
1833
+ document as written: `''` is the document root (or `/$expr`-prefixed
1834
+ under the version envelope, §4), object member names are
1835
+ pointer-escaped, operator argument positions append the operator key and
1836
+ the array index (`/$where/$eq/0`). It is the same pointer surface the
1837
+ `JQ0xxx` errors carry.
1838
+
1839
+ The tree is **deeply frozen at every level** — nodes, binding records,
1840
+ segment lists, captured values. That is a guarantee, not an
1841
+ implementation detail: a consumer may hold, share and index the tree
1842
+ without defensive copies, and MUST NOT mutate it (annotation passes
1843
+ return new trees; see `annotateTypes`). Captured document fragments
1844
+ (`literal`/`raw` values, schemas) are deep-frozen COPIES — the caller's
1845
+ objects are never frozen.
1846
+
1847
+ ### C.7 The compatibility policy
1848
+
1849
+ `AST_VERSION` (currently **1**) is bumped when a node kind is added or
1850
+ removed, a published field is removed or retyped, or an invariant of
1851
+ this appendix changes.
1852
+
1853
+ - Adding a **new optional field** to a node is NOT a version bump;
1854
+ consumers must tolerate unknown fields.
1855
+ - Adding a **node kind** IS a version bump: an exhaustive consumer
1856
+ dispatching on `kind` must fail loudly on a kind it does not know,
1857
+ and the version tells it why.
1858
+ - Explicitly **not** promised: the compiled closures, the operator
1859
+ registry's internals (`compile` bodies), evaluation order beyond what
1860
+ §§2–9 already require, and the two liveness lists' exact contents
1861
+ beyond "the slots later clauses read".
1862
+
1863
+ ### C.8 Type annotation (optional pass)
1864
+
1865
+ The tree carries cardinality but no value types. `annotateTypes`
1866
+ (same subpath) is a separate, optional pass:
1867
+
1868
+ ```js
1869
+ annotateTypes(analysis, { typeOf }) -> analysis'
1870
+ ```
1871
+
1872
+ It returns a NEW analysis whose tree mirrors the input with a frozen
1873
+ `type` tag on every node — `{ type: 'unknown' | 'null' | 'boolean' |
1874
+ 'number' | 'integer' | 'string' | 'array' | 'object', optional:
1875
+ boolean }` — never mutating the input and never running during
1876
+ compilation. `typeOf(pathNode)` is the caller's answer for path nodes
1877
+ (a store schema, a model — whatever the caller knows); returning
1878
+ `null`/`undefined` means unknown. Literals and constructors type
1879
+ themselves; quantifiers are boolean; operators propagate through the
1880
+ registry's declared `resultType` families (comparison, arithmetic,
1881
+ string, aggregate — everything undeclared yields `unknown`).
1882
+ **`unknown` is always a safe answer; a wrong tag is a defect.** General
1883
+ inference beyond these rules is out of scope here.