@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
@@ -4,16 +4,72 @@
4
4
  // an RFC 6901 JSON Pointer into the *query document* locating the offending
5
5
  // construct.
6
6
 
7
+ import { CodedError } from '@jarenjs/core/errors';
8
+
9
+ /**
10
+ * The runtime code table (the `CSV_CODES` shape): one entry per code
11
+ * the engine can raise, proven in sync with QUERY-FORMAT.md §10's
12
+ * normative tables by a test — the table cannot silently drift from
13
+ * the spec.
14
+ */
15
+ export const QUERY_CODES = Object.freeze({
16
+ JQ0001: 'object mixes $-prefixed and plain keys',
17
+ JQ0002: 'unknown operator or $-key outside the vocabulary',
18
+ JQ0003: 'known phrase with bad arity, value shape, or key combination',
19
+ JQ0004: 'string starting $ is not a valid path or escape',
20
+ JQ0005: 'variable reference neither bound nor a declared external',
21
+ JQ0006: 'version envelope with unknown or non-string $query',
22
+ JQ0007: 'duplicate variable binding within one phrase',
23
+ JQ0008: 'schema operator in a query compiled without a type-test compiler',
24
+ JQ0009: 'schema literal rejected by the type-test compiler',
25
+ JQ0010: '$call/$collation naming no registered function/collation',
26
+ JQ0011: 'expression nesting deeper than limits.depth',
27
+ JQ2001: 'runtime type error',
28
+ JQ2002: '$idiv/$mod by zero',
29
+ JQ2003: 'EBV of a multi-item sequence',
30
+ JQ2004: '$map key expression not a single string',
31
+ JQ2005: 'incomparable $orderby/$sort keys',
32
+ JQ2006: 'reference to an unbound external parameter',
33
+ JQ2007: 'resource guard: an operator result exceeding an implementation limit',
34
+ JQ2008: 'schema assertion failure',
35
+ JQ2009: 'an execution limit exceeded',
36
+ JQ2010: 'a registered $call function threw',
37
+ JQ2011: 'the input document is undefined',
38
+ });
39
+
40
+ /**
41
+ * Shared constructor body for the two query error classes: `cause`
42
+ * retains what host code threw, BY VALUE — set via an own property
43
+ * even for `undefined`, so presence is testable (the base's `hasOwn`
44
+ * options form, passed through unchanged).
45
+ */
46
+ class JsonQueryError extends CodedError {
47
+ /**
48
+ * @param {string} name - The public class name for `error.name`
49
+ * @param {string} code
50
+ * @param {string} reason
51
+ * @param {string} docPath
52
+ * @param {{ cause?: unknown }} [options]
53
+ */
54
+ constructor(name, code, reason, docPath, options) {
55
+ super(name, code, reason, docPath, options);
56
+ }
57
+ }
58
+
7
59
  /**
8
60
  * Error thrown when a query document is rejected at compile time
9
61
  * (`JQ0xxx` codes, QUERY-FORMAT.md section 10.2).
10
62
  */
11
- export class JsonQueryCompileError extends Error {
12
- constructor(code, message, docPath) {
13
- super(`${code}: ${message} at ${docPath}`);
14
- this.name = 'JsonQueryCompileError';
15
- this.code = code;
16
- this.docPath = docPath;
63
+ export class JsonQueryCompileError extends JsonQueryError {
64
+ /**
65
+ * @param {string} code
66
+ * @param {string} reason
67
+ * @param {string} docPath
68
+ * @param {{ cause?: unknown }} [options] - `cause` retains what a
69
+ * host hook (e.g. `compileTypeTest`) threw
70
+ */
71
+ constructor(code, reason, docPath, options) {
72
+ super('JsonQueryCompileError', code, reason, docPath, options);
17
73
  }
18
74
  }
19
75
 
@@ -21,12 +77,16 @@ export class JsonQueryCompileError extends Error {
21
77
  * Error thrown when evaluating a compiled query fails
22
78
  * (`JQ2xxx` codes, QUERY-FORMAT.md section 10.3).
23
79
  */
24
- export class JsonQueryRuntimeError extends Error {
25
- constructor(code, message, docPath) {
26
- super(`${code}: ${message} at ${docPath}`);
27
- this.name = 'JsonQueryRuntimeError';
28
- this.code = code;
29
- this.docPath = docPath;
80
+ export class JsonQueryRuntimeError extends JsonQueryError {
81
+ /**
82
+ * @param {string} code
83
+ * @param {string} reason
84
+ * @param {string} docPath
85
+ * @param {{ cause?: unknown }} [options] - `cause` retains what host
86
+ * code threw
87
+ */
88
+ constructor(code, reason, docPath, options) {
89
+ super('JsonQueryRuntimeError', code, reason, docPath, options);
30
90
  }
31
91
  }
32
92
 
@@ -13,20 +13,154 @@
13
13
  // plain JSON out (empty sequence -> undefined, singleton -> the item,
14
14
  // longer sequence -> array of items).
15
15
 
16
- import { normalizeQuery, deepFreezeCopy } from './normalize.js';
17
- import { compileNode, UNBOUND } from './compile.js';
16
+ import { createBoundedCache, createWeakCache } from '@jarenjs/core/cache';
17
+ import { normalizeQuery, deepFreezeCopy, NODE_KINDS } from './normalize.js';
18
+ import { compileQueryRoot, UNBOUND } from './compile.js';
18
19
  import { EMPTY, Seq, ebv } from './runtime.js';
20
+ import { JsonQueryRuntimeError } from './errors.js';
19
21
 
20
- export { JsonQueryCompileError, JsonQueryRuntimeError } from './errors.js';
22
+ export { JsonQueryCompileError, JsonQueryRuntimeError, QUERY_CODES } from './errors.js';
23
+ export { NODE_KINDS };
24
+ export { annotateTypes, TYPE_TAGS } from './types.js';
21
25
 
22
26
  const hasOwn = Object.hasOwn;
23
27
 
28
+ /**
29
+ * The enforced query limits (QUERY-FORMAT.md section 8.12). These are
30
+ * deterministic result/phrase-OUTPUT caps, not general resource
31
+ * budgets: they bound what a phrase or the query hands onward, never
32
+ * the memory, work, fan-out or recursion spent producing it (a group
33
+ * or order barrier may accumulate arbitrarily many items behind a
34
+ * small final result). Untrusted queries need worker isolation, not
35
+ * these limits.
36
+ * @typedef {Object} JsonQueryLimits
37
+ * @property {number} [sequenceItems] - Caps every FLWOR phrase
38
+ * materialization and tightens `$range`'s resource guard; exceeding
39
+ * it is `JQ2009` (`$range` keeps its historical `JQ2007`).
40
+ * @property {number} [resultItems] - Caps the final result at the
41
+ * query boundary (`JQ2009`); checked after evaluation, and
42
+ * deliberately bypassed by `first`/`exists`/`ebv`.
43
+ * @property {number} [steps] - Caps expression-node evaluations
44
+ * (`JQ2009`). This is the one limit that bounds *work* rather than
45
+ * output, so it is also the one that costs: setting it compiles a
46
+ * counter check into every node, roughly halving throughput. A step
47
+ * is one node evaluation, not one primitive operation - a single
48
+ * node that loops internally (`$range` materialization, a general
49
+ * comparison's cross product) counts once.
50
+ * @property {number} [depth] - Caps expression nesting, enforced at
51
+ * compile time (`JQ0011`). The language has no recursion, so the
52
+ * compiled closure tree's evaluation depth IS the document's static
53
+ * nesting: checking it once is exact and costs nothing to evaluate.
54
+ */
55
+
56
+ /**
57
+ * Compile options for {@link compileJsonQuery}.
58
+ * @typedef {Object} JsonQueryOptions
59
+ * @property {(schemaJson: any, docPath: string) => ((value: any) => boolean)} [compileTypeTest]
60
+ * Hook compiling a JSON Schema literal into a boolean item
61
+ * predicate, called once per schema literal at query compile time
62
+ * (QUERY-FORMAT.md section 8.11). `@jarenjs/validate/query` exports
63
+ * `createTypeTestCompiler()` producing one; any conforming
64
+ * implementation works - this package never imports the validator.
65
+ * Without a hook, the schema operators `$valid`/`$assert`/`$as` are
66
+ * compile error JQ0008.
67
+ * @property {object} [extensions] - Package-internal operator
68
+ * extension point, the operator analogue of `compileTypeTest` (used
69
+ * by the JSLT layer; not a public contract). A plain object of
70
+ * `name -> entry` following the operator registry contract; see
71
+ * normalizeQuery in normalize.js for the full shape. The published
72
+ * format vocabulary is unchanged: without extensions, documents
73
+ * using such operators fail JQ0002.
74
+ * @property {Record<string, (...args: any[]) => any>} [functions]
75
+ * Registry of named trusted pure host functions for `$call`
76
+ * (QUERY-FORMAT.md section 8.12); an unregistered or empty name is
77
+ * rejected at compile time (JQ0010 / TypeError).
78
+ * @property {Record<string, (a: string, b: string) => number>} [collations]
79
+ * Registry of named pure compare functions for `$orderby`'s
80
+ * `$collation` member (QUERY-FORMAT.md section 6.6).
81
+ * @property {Record<string, import('../path.js').JSONPathFunction>} [pathFunctions]
82
+ * Registry of custom JSONPath function extensions (RFC 9535 section
83
+ * 2.4), available inside the filters of every path string the
84
+ * document contains. Deliberately separate from `functions`: that
85
+ * registry extends the query vocabulary through `$call`, this one
86
+ * extends the RFC 9535 grammar the path strings are written in.
87
+ * @property {JsonQueryLimits} [limits] - Enforced execution limits.
88
+ * @property {readonly string[]} [externals] - Closed-world compilation:
89
+ * the variable names (no `$` sigil) the document may leave free.
90
+ * Every other free variable is compile error `JQ0005` at its own
91
+ * reference site, so a query cannot silently acquire a parameter the
92
+ * host never meant to expose. `[]` declares none. Omitted, the open
93
+ * world of QUERY-FORMAT.md section 9 applies: use is the declaration.
94
+ * @property {boolean} [analysis] - On `compileJsonQuery`: additionally
95
+ * expose the normalized-form record (QUERY-FORMAT.md Appendix C.1) at
96
+ * `query.analysis`, from the same normalization. Compilation itself
97
+ * stays strict — schema hooks remain required.
98
+ */
99
+
100
+ /**
101
+ * The published normalized-form record (QUERY-FORMAT.md Appendix C.1):
102
+ * what `analyzeQuery` returns and `compileJsonQuery`'s `analysis`
103
+ * option exposes.
104
+ * @typedef {Object} JsonQueryAnalysis
105
+ * @property {number} astVersion - see the compatibility policy (C.7)
106
+ * @property {object} root - the frozen node tree (C.3)
107
+ * @property {readonly { name: string, slot: number }[]} externals
108
+ * @property {number} frameSize
109
+ * @property {Readonly<JsonQueryDependencies>} dependencies
110
+ * @property {object | null} limits
111
+ */
112
+
113
+ /**
114
+ * The frozen dependency record of a compiled query (saved-rule
115
+ * vetting): the external names it binds, the operators it uses, and
116
+ * the registered functions/collations it resolved.
117
+ * @typedef {Object} JsonQueryDependencies
118
+ * @property {readonly string[]} externals
119
+ * @property {readonly string[]} operators
120
+ * @property {readonly string[]} functions
121
+ * @property {readonly string[]} collations
122
+ */
123
+
124
+ /**
125
+ * A plain-JSON explanation of a compiled query: its dependencies plus
126
+ * the enforced limits. A fresh value each `explain()` call.
127
+ * @typedef {Object} JsonQueryExplanation
128
+ * @property {string[]} externals
129
+ * @property {string[]} operators
130
+ * @property {string[]} functions
131
+ * @property {string[]} collations
132
+ * @property {{ sequenceItems: number | null, resultItems: number | null, steps: number | null, depth: number | null } | null} limits
133
+ */
134
+
135
+ /**
136
+ * The compiled query returned by {@link compileJsonQuery}: the query
137
+ * function itself, carrying its helper methods and metadata.
138
+ * @typedef {((data: any, externals?: Record<string, any>) => any) & {
139
+ * first: (data: any, externals?: Record<string, any>) => any,
140
+ * exists: (data: any, externals?: Record<string, any>) => boolean,
141
+ * ebv: (data: any, externals?: Record<string, any>) => boolean,
142
+ * externals: readonly string[],
143
+ * doc: any,
144
+ * dependencies: Readonly<JsonQueryDependencies>,
145
+ * explain: () => JsonQueryExplanation,
146
+ * analysis?: Readonly<JsonQueryAnalysis>,
147
+ * }} CompiledJsonQuery
148
+ */
149
+
24
150
  /**
25
151
  * Compile a Jaren JSON Query document into a reusable query function.
26
152
  *
27
153
  * The returned function applies the query to a JSON value and returns the
28
154
  * result as plain JSON: `undefined` for the empty sequence, the item
29
155
  * itself for a singleton result, an array of items for a longer sequence.
156
+ *
157
+ * The `data` argument MUST be a JSON value (section 2.1). The engine does
158
+ * not deep-validate it - that would cost a full walk per call - so a
159
+ * non-JSON value inside the document simply flows through as an opaque
160
+ * item. The single exception is `undefined`, rejected with `JQ2011`
161
+ * because this API already spends `undefined` on the empty sequence.
162
+ * An external bound to `undefined` reads as unbound (`JQ2006` on use).
163
+ *
30
164
  * It also carries helper methods and metadata:
31
165
  *
32
166
  * - `query(data, externals?)` - the query result as described above
@@ -44,26 +178,15 @@ const hasOwn = Object.hasOwn;
44
178
  * external raises `JQ2006`.
45
179
  * - `query.doc` - a deeply frozen copy of the query document (the
46
180
  * caller's object is never frozen)
181
+ * - `query.dependencies` - what the query depends on (frozen JSON):
182
+ * external names, operators, registered functions and collations
183
+ * - `query.explain()` - a fresh plain-JSON explanation: the
184
+ * dependencies plus the enforced limits
47
185
  *
48
186
  * @param {any} doc - the query document (any JSON value; a bare RFC 9535
49
187
  * JSONPath string is the degenerate query)
50
- * @param {object} [options] - compile options
51
- * @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
52
- * [options.compileTypeTest] - hook compiling a JSON Schema literal into
53
- * a boolean item predicate, called once per schema literal at query
54
- * compile time (QUERY-FORMAT.md section 8.11). `@jarenjs/validate/query`
55
- * exports `createTypeTestCompiler()` producing one; any conforming
56
- * implementation works - this package never imports the validator.
57
- * Without a hook, the schema operators `$valid`/`$assert`/`$as` are
58
- * compile error JQ0008.
59
- * @param {object} [options.extensions] - package-internal operator
60
- * extension point, the operator analogue of `compileTypeTest` (used by
61
- * the JSLT layer; not a public contract). A plain object of
62
- * `name -> entry` following the operator registry contract; see
63
- * normalizeQuery in normalize.js for the full shape. The published
64
- * format vocabulary is unchanged: without extensions, documents using
65
- * such operators fail JQ0002.
66
- * @returns {function} the compiled query function
188
+ * @param {JsonQueryOptions} [options] - compile options
189
+ * @returns {CompiledJsonQuery} the compiled query function
67
190
  * @throws {JsonQueryCompileError} when the document violates the format
68
191
  * @example
69
192
  * const q = compileJsonQuery({
@@ -74,16 +197,40 @@ const hasOwn = Object.hasOwn;
74
197
  * q(data, { max: 10 }); // { title: 'Sayings of the Century', cheap: true }
75
198
  */
76
199
  export function compileJsonQuery(doc, options = {}) {
77
- const { root, frameSize, externals } = normalizeQuery(doc, options);
78
- const get = compileNode(root);
200
+ // `analysis: true` additionally exposes the normalized-form record at
201
+ // `query.analysis` (QUERY-FORMAT.md Appendix C.1) from the SAME
202
+ // normalization — but compilation itself stays strict (the flag is
203
+ // stripped before normalizeQuery, so schema hooks stay required).
204
+ const wantAnalysis = options.analysis === true;
205
+ const normalized =
206
+ normalizeQuery(doc, wantAnalysis ? { ...options, analysis: false } : options);
207
+ const { root, frameSize, externals, limits, stepSlot, usedOps, usedFunctions, usedCollations } =
208
+ normalized;
209
+ const get = compileQueryRoot(root,
210
+ stepSlot < 0 ? null : { slot: stepSlot, limit: limits.steps });
79
211
  const extCount = externals.length;
212
+ const resultCap = limits !== null && limits.resultItems !== null ? limits.resultItems : 0;
80
213
 
81
214
  function evaluate(data, ext) {
215
+ // The data model is JSON (section 2.1), and the engine does not
216
+ // deep-validate its input - that would be an O(size) walk on every
217
+ // call. The one violation that must not pass silently is `undefined`,
218
+ // because this API already spends `undefined` on the empty sequence:
219
+ // `query(undefined)` would answer "empty" while `query.exists(...)`
220
+ // answered true. Every other non-JSON value flows through as an
221
+ // opaque item, which is the caller's contract to keep.
222
+ if (data === undefined)
223
+ throw new JsonQueryRuntimeError('JQ2011', 'the input document is undefined, which is not a JSON value', '');
82
224
  const frame = new Array(frameSize);
83
225
  frame[0] = data;
226
+ if (stepSlot >= 0)
227
+ frame[stepSlot] = 0;
84
228
  for (let i = 0; i < extCount; i++) {
85
229
  const e = externals[i];
86
- frame[e.slot] = ext != null && hasOwn(ext, e.name) ? ext[e.name] : UNBOUND;
230
+ // an external explicitly bound to `undefined` reads as unbound, so
231
+ // the reference raises JQ2006 instead of yielding a non-JSON item
232
+ const v = ext != null && hasOwn(ext, e.name) ? ext[e.name] : undefined;
233
+ frame[e.slot] = v === undefined ? UNBOUND : v;
87
234
  }
88
235
  return get(frame);
89
236
  }
@@ -92,7 +239,13 @@ export function compileJsonQuery(doc, options = {}) {
92
239
  const v = evaluate(data, ext);
93
240
  if (v === EMPTY)
94
241
  return undefined;
95
- return v instanceof Seq ? v.items : v;
242
+ if (v instanceof Seq) {
243
+ if (resultCap > 0 && v.items.length > resultCap)
244
+ throw new JsonQueryRuntimeError('JQ2009',
245
+ `the query result has ${v.items.length} items, more than limits.resultItems (${resultCap})`, '');
246
+ return v.items;
247
+ }
248
+ return v;
96
249
  };
97
250
  query.first = (data, ext) => {
98
251
  const v = evaluate(data, ext);
@@ -104,18 +257,107 @@ export function compileJsonQuery(doc, options = {}) {
104
257
  query.ebv = (data, ext) => ebv(evaluate(data, ext), '');
105
258
  query.externals = Object.freeze(externals.map((e) => e.name));
106
259
  query.doc = deepFreezeCopy(doc);
260
+ /**
261
+ * What the compiled query depends on (saved-rule vetting): the
262
+ * external names it binds, the operators it uses, and the registered
263
+ * functions/collations it resolved. All frozen JSON.
264
+ */
265
+ query.dependencies = Object.freeze({
266
+ externals: query.externals,
267
+ operators: Object.freeze([...usedOps].sort()),
268
+ functions: Object.freeze([...usedFunctions].sort()),
269
+ collations: Object.freeze([...usedCollations].sort()),
270
+ });
271
+ /**
272
+ * A plain-JSON explanation of the compiled query: its dependencies
273
+ * plus the enforced limits. A fresh value each call.
274
+ * @returns {JsonQueryExplanation}
275
+ */
276
+ query.explain = () => ({
277
+ externals: [...query.externals],
278
+ operators: [...query.dependencies.operators],
279
+ functions: [...query.dependencies.functions],
280
+ collations: [...query.dependencies.collations],
281
+ limits: limits === null ? null : {
282
+ sequenceItems: limits.sequenceItems,
283
+ resultItems: limits.resultItems,
284
+ steps: limits.steps,
285
+ depth: limits.depth,
286
+ },
287
+ });
288
+ if (wantAnalysis)
289
+ query.analysis = analysisRecord(normalized);
107
290
  return query;
108
291
  }
109
292
 
110
- const OBJECT_CACHE = new WeakMap();
111
- const STRING_CACHE = new Map();
112
- const STRING_CACHE_LIMIT = 512;
293
+ /**
294
+ * The version of the published normalized form (QUERY-FORMAT.md
295
+ * Appendix C.7): bumped when a node kind is added or removed, a
296
+ * published field is removed or retyped, or an appendix invariant
297
+ * changes. Adding an optional field is NOT a bump.
298
+ */
299
+ export const AST_VERSION = 1;
300
+
301
+ /**
302
+ * Build the frozen analysis record from a `normalizeQuery` result —
303
+ * shared by `analyzeQuery` and `compileJsonQuery`'s `analysis` option
304
+ * so both expose byte-identical shapes from one normalization.
305
+ * @param {ReturnType<typeof normalizeQuery>} normalized
306
+ */
307
+ function analysisRecord(normalized) {
308
+ const { root, frameSize, externals, limits, usedOps, usedFunctions, usedCollations } = normalized;
309
+ return Object.freeze({
310
+ astVersion: AST_VERSION,
311
+ root,
312
+ externals,
313
+ frameSize,
314
+ dependencies: Object.freeze({
315
+ externals: Object.freeze(externals.map((e) => e.name)),
316
+ operators: Object.freeze([...usedOps].sort()),
317
+ functions: Object.freeze([...usedFunctions].sort()),
318
+ collations: Object.freeze([...usedCollations].sort()),
319
+ }),
320
+ limits,
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Analyse a query document WITHOUT compiling it: the engine's own
326
+ * normalized reading of the document — the frozen node tree, the
327
+ * externals in first-appearance order, the frame size and the
328
+ * dependency sets — published as the versioned contract of
329
+ * QUERY-FORMAT.md Appendix C. Another package walks this instead of
330
+ * re-implementing the grammar, and an unknown `kind` in its dispatch is
331
+ * a loud failure instead of a silent divergence.
332
+ *
333
+ * Analysis applies every JQ0xxx rejection compilation would, with one
334
+ * difference (Appendix C.1): schema literals do not require
335
+ * `options.compileTypeTest` — without the hook they normalize to `raw`
336
+ * nodes whose `test` is `null`, so a document can be analysed by a
337
+ * consumer that could not execute it. With the hook supplied, analysis
338
+ * compiles the predicates exactly as compilation would.
339
+ * @param {any} doc - the query document (any JSON value)
340
+ * @param {JsonQueryOptions} [options] - the same options as
341
+ * `compileJsonQuery`
342
+ * @returns {{ astVersion: number, root: object, externals: readonly
343
+ * { name: string, slot: number }[], frameSize: number,
344
+ * dependencies: Readonly<JsonQueryDependencies>, limits: object | null }}
345
+ * @throws {JsonQueryCompileError} on any JQ0xxx condition (except
346
+ * JQ0008, which analysis does not raise)
347
+ */
348
+ export function analyzeQuery(doc, options = {}) {
349
+ return analysisRecord(normalizeQuery(doc, { ...options, analysis: true }));
350
+ }
351
+
352
+ const OBJECT_CACHE = createWeakCache();
353
+ const STRING_CACHE = createBoundedCache(512);
354
+ const compileUncached = (/** @type {any} */ doc) => compileJsonQuery(doc);
113
355
 
114
356
  /**
115
357
  * Apply a Jaren JSON Query document to a JSON value in one call.
116
- * Compiled queries are cached: object documents by identity (WeakMap),
117
- * string documents (the degenerate JSONPath case) by value (FIFO, 512
118
- * entries - the same pattern as `queryJSONPath`).
358
+ * Compiled queries are cached: object documents by identity (weak),
359
+ * string documents (the degenerate JSONPath case) by value (bounded
360
+ * LRU, 512 entries the shared `@jarenjs/core/cache` primitive).
119
361
  * @param {any} doc - the query document
120
362
  * @param {any} data - the JSON value to query
121
363
  * @param {object} [externals] - external parameter bindings (`{ name: value }`)
@@ -126,20 +368,10 @@ const STRING_CACHE_LIMIT = 512;
126
368
  export function queryJson(doc, data, externals) {
127
369
  let query;
128
370
  if (typeof doc === 'string') {
129
- query = STRING_CACHE.get(doc);
130
- if (query === undefined) {
131
- query = compileJsonQuery(doc);
132
- if (STRING_CACHE.size >= STRING_CACHE_LIMIT)
133
- STRING_CACHE.delete(STRING_CACHE.keys().next().value);
134
- STRING_CACHE.set(doc, query);
135
- }
371
+ query = STRING_CACHE.getOrCreate(doc, compileUncached);
136
372
  }
137
373
  else if (typeof doc === 'object' && doc !== null) {
138
- query = OBJECT_CACHE.get(doc);
139
- if (query === undefined) {
140
- query = compileJsonQuery(doc);
141
- OBJECT_CACHE.set(doc, query);
142
- }
374
+ query = OBJECT_CACHE.getOrCreate(doc, compileUncached);
143
375
  }
144
376
  else { // scalar documents are trivial literals; compiling is cheaper than caching
145
377
  query = compileJsonQuery(doc);