ata-validator 1.24.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 1.25.0 - 2026-09-17
6
+
7
+ ### Fixed
8
+
9
+ - Emitted standalone modules compiled their regex patterns on every call. The compiler stores each pattern both as a closure variable and as an inline `const _reN = __ataSafeRe(...)` in the stored source; in process the inline line never runs (the closure binds the compiled matcher once), but every emitter placed it inside the emitted function, where it shadowed the module-scope binding and rebuilt the matcher per call. A schema with one `pattern` validated at 202 thousand ops/s as a standalone module while the in-process path ran at millions. Patterns are now declared once at load scope in all four emitters (`toStandalone`, `toStandaloneModule`, `bundleStandalone`, `bundleCompact`); the same document on the same schema now measures 27.0 million ops/s, 134x. `bundleCompact` needed care: its deduplication keyed on body text alone, and with the pattern no longer in the body, two schemas differing only in their pattern would have merged, so the key now includes the factory-scope declarations.
10
+
11
+ ### Added
12
+
13
+ - Provably local `unevaluated*` compiles real error detail on the generated-code path, and therefore in standalone modules. When every occurrence of `unevaluatedProperties`/`unevaluatedItems` sits on a node with no `$ref` and no `patternProperties`, and every in-place applicator (`allOf`, `anyOf`, `oneOf`, `if`/`then`/`else`) contributes only property names the node's own `properties` already declares, the keyword is exactly `additionalProperties`/`items` in disguise and both the error and combined generators now emit it as such, keeping the keyword's own identity: one error per stray key with `params.unevaluatedProperty` (ATA7003), one error with `params.limit` for extra items. `toStandaloneModule` with error detail requested no longer degrades on these schemas; a config schema of the common `if/then/else` plus root `unevaluatedProperties: false` shape ships an AOT module with the same per-field errors the runtime reports. Schemas where the annotations cannot be proven local (`unevaluated*` next to a `$ref`, cousin-visible shapes) still decline, loudly, as before. Verified by the error-shape differential (2,816 cases), the engine differential (18,126 cases) and the official suite on all three dialects.
14
+ - `toStandaloneModule(schema, { positions: true })` also exports `validateJSON(text)`: parse, validate, and on failure attach a `dataFrame` (`byteOffset`, `length`, `line`, `col`, `text`) to every error by walking the original text once. The mapping goes through `instancePath`, so a key that appears in several sections frames each error on its own occurrence, which a first-occurrence string search does not. A syntax error is the single ATA9001 error with a frame on the document. The walker is the runtime's own `buildDataPositionMap`, embedded verbatim so the two cannot drift, and it costs about 5 KB in the module (1.7 KB gzipped), which is why it is opt-in.
15
+ - `toStandaloneModule(schema, { parse: true })`'s `parse(data)` now fills declared `default` values for absent optional properties (object and array defaults are fresh per call) and is emitted for composed schemas under the same proof the `unevaluated*` error generators use: in-place applicators are admitted when every property name they mention is already declared in the node's own `properties`. The common config shape, `if`/`then`/`else` over declared fields with root `unevaluatedProperties: false`, now gets a `parse` that enforces the conditional, rejects undeclared keys, and returns the ready object. Two default shapes decline because they are where `parse` and the runtime would disagree: a required property with a default, and a default the property's own schema rejects.
16
+ - Every emitted module exports `schemaHash`, a 16 character content hash of the schema it was compiled from, over canonical JSON so key order does not matter, computed from the schema as the caller wrote it so dialect normalization does not break the comparison. `schemaHash(schema)` from `ata-validator/build` computes the same value; a build detects a stale artifact by comparing the two instead of embedding its own fingerprint.
17
+ - The agreement between compiled modules and the runtime is now a stated contract, in `docs/STABILITY.md` and the API reference: same version, same verdict on every document, enforced by the differential test in every CI run, with the emitters declining or degrading loudly where they cannot prove equivalence.
18
+ - The compat shim exports `attachDataFrames(errors, text)`: the same one-pass mapping for Ajv-shaped errors, for callers that keep the source text around. Not an Ajv API; it exists because hand-written line finders based on a string search get repeated keys wrong.
19
+
5
20
  ## 1.24.0 - 2026-09-16
6
21
 
7
22
  ### Added
package/bin/ata.js CHANGED
@@ -161,7 +161,13 @@ function cmdCompile(args) {
161
161
  process.exit(1);
162
162
  }
163
163
  const input = args._[0];
164
- const format = args.opts.format || 'esm';
164
+ // With no --format, the output filename already says what the caller wants:
165
+ // -o validator.cjs used to emit an ES module into a .cjs file, which Node
166
+ // then refused to load.
167
+ const extFormat = args.opts.output && /\.cjs$/.test(args.opts.output) ? 'cjs'
168
+ : args.opts.output && /\.mjs$/.test(args.opts.output) ? 'esm'
169
+ : null;
170
+ const format = args.opts.format || extFormat || 'esm';
165
171
  if (format !== 'esm' && format !== 'cjs') {
166
172
  process.stderr.write(`error: --format must be esm or cjs (got "${format}")\n`);
167
173
  process.exit(1);
package/build.d.ts CHANGED
@@ -89,6 +89,18 @@ export interface BundleStandaloneOptions {
89
89
  }
90
90
 
91
91
  export interface ToStandaloneModuleOptions {
92
+ /**
93
+ * Run the authoring-time schema checks before emitting: unknown keywords
94
+ * (with a spelling suggestion), inert keywords, unsatisfiable required
95
+ * names, unresolvable local $refs. Throws with every finding.
96
+ */
97
+ strictSchema?: boolean;
98
+ /**
99
+ * Called when the module ships degraded: error detail was requested but the
100
+ * error generator declined this schema, so failures report the single
101
+ * ATA9000 abort-early error while the verdict stays exact.
102
+ */
103
+ onWarning?: (message: string) => void;
92
104
  format?: 'cjs' | 'esm';
93
105
  abortEarly?: boolean;
94
106
  source?: boolean;
@@ -99,12 +111,27 @@ export interface ToStandaloneModuleOptions {
99
111
  formatMode?: 'embed' | 'inject';
100
112
  /**
101
113
  * Also export `parse(data)`: validate, then return a copy of the input
102
- * holding only the properties the schema declares. Off by default because
103
- * it adds to the emitted module's size. Emitted only where the copy is
104
- * provably exact; a schema using `$ref`, a composition, `patternProperties`,
105
- * an `additionalProperties` schema or an array of objects gets no `parse`.
114
+ * holding only the properties the schema declares, with declared `default`
115
+ * values filled in for absent optional properties (object and array
116
+ * defaults are fresh per call). Off by default because it adds to the
117
+ * emitted module's size. Emitted only where the copy is provably exact:
118
+ * `$ref` and `patternProperties` decline; in-place applicators (`allOf`,
119
+ * `anyOf`, `oneOf`, `if`/`then`/`else`, and `unevaluatedProperties: false`)
120
+ * are admitted when every property name they mention is already declared
121
+ * in the node's own `properties`. A required property with a default, or
122
+ * a default its own schema rejects, also declines, because those are the
123
+ * two shapes where parse() and the runtime would disagree.
106
124
  */
107
125
  parse?: boolean;
126
+ /**
127
+ * Also export `validateJSON(text)`: parse the JSON text, validate, and on
128
+ * failure attach a `dataFrame` ({ byteOffset, length, line, col, text })
129
+ * to every error by walking the original text once, so errors point at the
130
+ * right occurrence even when the same key appears in several sections.
131
+ * Off by default because the embedded position walker adds to the emitted
132
+ * module's size.
133
+ */
134
+ positions?: boolean;
108
135
  }
109
136
 
110
137
  /** Bundle multiple schemas into one self-contained module (no ata-validator runtime). */
@@ -113,6 +140,16 @@ export function bundleStandalone(schemas: unknown[], options?: BundleStandaloneO
113
140
  /** Like {@link bundleStandalone} but deduplicates shared bodies for smaller output. */
114
141
  export function bundleCompact(schemas: unknown[], options?: BundleStandaloneOptions): string;
115
142
 
143
+ /**
144
+ * Stable content hash of a schema, 16 hex characters, over a canonical JSON
145
+ * form (keys sorted at every level). Every module from
146
+ * {@link toStandaloneModule} exports its own `schemaHash`; comparing that
147
+ * against `schemaHash(currentSchema)` tells a build the module is stale.
148
+ * An integrity aid, not a security boundary.
149
+ */
150
+ export function schemaHash(schema: unknown): string;
151
+
116
152
  /** Emit a self-contained `validate`/`isValid` module string for a single
117
- * schema, plus `parse` when {@link ToStandaloneModuleOptions.parse} is set. */
153
+ * schema, plus `parse` when {@link ToStandaloneModuleOptions.parse} is set
154
+ * and `validateJSON` when {@link ToStandaloneModuleOptions.positions} is. */
118
155
  export function toStandaloneModule(schema: unknown, options?: ToStandaloneModuleOptions): string | null;
package/compat.d.ts CHANGED
@@ -25,6 +25,14 @@ declare class Ata {
25
25
  removeKeyword(keyword: string): this;
26
26
 
27
27
  errorsText(errors?: Ata.ErrorObject[] | null, options?: { separator?: string; dataVar?: string }): string;
28
+
29
+ /**
30
+ * Attach a `dataFrame` ({ byteOffset, length, line, col, text }) to each
31
+ * error by mapping its `instancePath` into the JSON text the data was
32
+ * parsed from. One walk of the text; correct when the same key appears in
33
+ * several sections. Not an Ajv API. Mutates and returns `errors`.
34
+ */
35
+ static attachDataFrames(errors: Ata.ErrorObject[] | null | undefined, text: string | Buffer): Ata.ErrorObject[] | null | undefined;
28
36
  }
29
37
 
30
38
  declare namespace Ata {
package/compat.js CHANGED
@@ -409,6 +409,26 @@ function metaValidator(id) {
409
409
  return v;
410
410
  }
411
411
 
412
+ // Maps each error's instancePath to its position in the JSON text the data
413
+ // was parsed from and attaches it as `dataFrame` ({ byteOffset, length,
414
+ // line, col, text }). One walk of the text, and correct when the same key
415
+ // appears in several sections, which a first-occurrence string search is
416
+ // not. Not an Ajv API: exported for callers that keep the source text
417
+ // around and want file positions on Ajv-shaped errors.
418
+ Ata.attachDataFrames = function attachDataFrames(errors, text) {
419
+ if (!errors || !errors.length || text == null) return errors;
420
+ const { buildDataPositionMap } = require('./lib/data-positions');
421
+ let map;
422
+ try { map = buildDataPositionMap(text); } catch { return errors; }
423
+ for (const e of errors) {
424
+ if (!e || e.dataFrame) continue;
425
+ const ptr = e.instancePath != null ? e.instancePath : (e.dataPath || '');
426
+ const p = map[ptr];
427
+ if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
428
+ }
429
+ return errors;
430
+ };
431
+
412
432
  module.exports = Ata;
413
433
  module.exports.default = Ata;
414
434
  module.exports.Ata = Ata;
package/index.d.ts CHANGED
@@ -395,9 +395,20 @@ export interface ValidatorOptions {
395
395
  keywords?: Record<string, KeywordDefinition | KeywordValidate>;
396
396
  /**
397
397
  * When true, validation errors include `parentSchema` (the schema object
398
- * that produced the error). Matches ajv's `verbose: true`.
398
+ * that produced the error), `schema` (the failing keyword's own value) and
399
+ * `data` (the value the error points at). Matches ajv's `verbose: true`.
399
400
  */
400
401
  verbose?: boolean;
402
+ /**
403
+ * Authoring-time schema checks, run at construction on the schema as
404
+ * written: unknown keywords (with a spelling suggestion), keywords the
405
+ * node's own `type` makes inert, `required` names nothing can satisfy, and
406
+ * local `$ref`s that do not resolve. `true` throws with every finding;
407
+ * `'log'` warns through `logger` (or the console) and continues.
408
+ */
409
+ strictSchema?: boolean | 'log';
410
+ /** Receives `strictSchema: 'log'` warnings; `false` silences them. */
411
+ logger?: { warn(...args: unknown[]): void } | false;
401
412
  /**
402
413
  * When true, validate() returns a shared frozen result on the first failure
403
414
  * instead of collecting full error details. Smaller hot-path allocation.
package/lib/aot-build.js CHANGED
@@ -270,10 +270,22 @@ function bundleCompact(schemas, opts) {
270
270
  }
271
271
 
272
272
  function toStandaloneModule(schema, opts) {
273
+ // The same authoring-time checks `ata compile --strict-schema` runs, for
274
+ // callers driving the build API directly: a typo in a schema headed for a
275
+ // compiled module is the quietest place a typo can live.
276
+ if (opts && opts.strictSchema) {
277
+ const { checkSchemaStrict } = require('./strict-check');
278
+ const problems = checkSchemaStrict(schema, {});
279
+ if (problems.length > 0) {
280
+ throw new Error(problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n'));
281
+ }
282
+ }
273
283
  const v = schema instanceof Validator ? schema : new Validator(schema, opts);
274
284
  return aot.toStandaloneModule(v, opts);
275
285
  }
276
286
 
287
+ const { schemaHash } = require('./schema-hash');
288
+
277
289
  module.exports = {
278
290
  build,
279
291
  expandGlobs,
@@ -283,4 +295,5 @@ module.exports = {
283
295
  bundleStandalone,
284
296
  bundleCompact,
285
297
  toStandaloneModule,
298
+ schemaHash,
286
299
  };
package/lib/aot-impl.js CHANGED
@@ -13,7 +13,9 @@
13
13
  // bundleCompact(Validator, schemas, opts)
14
14
  // loadBundle(Validator, mods, schemas, opts)
15
15
 
16
- const { compileToJSCodegenWithErrors } = require('./js-compiler');
16
+ const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions } = require('./js-compiler');
17
+ const { buildDataPositionMap } = require('./data-positions');
18
+ const { schemaHash } = require('./schema-hash');
17
19
  const SAFE_REGEX_SOURCE = require('./safe-regex-source');
18
20
 
19
21
  // Embedded verbatim in standalone modules so the output file has no runtime
@@ -54,6 +56,35 @@ function safeRePrelude(...fns) {
54
56
  return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
55
57
  }
56
58
 
59
+ // Serialize the closure variables a compiled function referenced (regexes,
60
+ // sets, lookup tables, preprocessor functions) into declarations for load
61
+ // scope. Regex lines are no longer inlined into _source, so every emitter
62
+ // must declare them exactly once outside the emitted function; inlining them
63
+ // in the body recompiled the pattern on every call.
64
+ function closureDeclLines(jsFn, declKW) {
65
+ const kw = declKW || 'const';
66
+ const lines = [];
67
+ if (!jsFn || !jsFn._closures || jsFn._closures.length === 0) return lines;
68
+ for (const { name, val } of jsFn._closures) {
69
+ if (Array.isArray(val)) { lines.push(`${kw} ${name} = ${JSON.stringify(val)};`); continue; }
70
+ if (val && val.__ataSafe) {
71
+ lines.push(`${kw} ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
72
+ } else if (val instanceof RegExp) {
73
+ const flags = val.flags;
74
+ lines.push(`${kw} ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
75
+ } else if (val instanceof Set) {
76
+ lines.push(`${kw} ${name} = new Set(${JSON.stringify([...val])});`);
77
+ } else if (typeof val === 'function') {
78
+ // new Function('_ppv', body) — extract body from toString()
79
+ const str = val.toString();
80
+ const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
81
+ const body = m ? m[1].trim() : str;
82
+ lines.push(`${kw} ${name} = function(_ppv) { ${body} };`);
83
+ }
84
+ }
85
+ return lines;
86
+ }
87
+
57
88
  // A validator can enforce more than its schema says. `withKeywords` from
58
89
  // @ata-project/keywords wraps an instance's entry points, and a wrapper
59
90
  // declares that by setting `_externalChecks`. The emitters below build their
@@ -87,11 +118,12 @@ function toStandalone(validator) {
87
118
  );
88
119
  const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
89
120
 
121
+ const closureSrc = closureDeclLines(jsFn).join('\n');
90
122
  return `// Auto-generated by ata-validator — do not edit
91
123
  'use strict';
92
124
  ${_CP_LEN_SOURCE}
93
125
  ${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
94
- const boolFn = function(d) {
126
+ ${closureSrc ? closureSrc + '\n' : ''}const boolFn = function(d) {
95
127
  ${src}
96
128
  };
97
129
  const hybridFactory = function(R, E) {
@@ -171,17 +203,40 @@ function emitClone(node, access, depth) {
171
203
  if (!node || typeof node !== 'object') return null;
172
204
  if (depth > 12) return null;
173
205
  // Only plain object nodes are cloneable, and only when the schema names
174
- // every property it keeps. A union type, a $ref, or any composition means
175
- // the set of allowed keys is not this node's to decide.
176
- if (node.$ref || node.allOf || node.anyOf || node.oneOf || node.not ||
177
- node.if || node.patternProperties || node.unevaluatedProperties ||
206
+ // every property it keeps. A $ref means the set of allowed keys is not
207
+ // this node's to decide. In-place applicators (allOf, anyOf, oneOf,
208
+ // if/then/else) are admitted under the same proof the unevaluated* error
209
+ // generators use: every property name they contribute must already be
210
+ // declared in this node's own `properties`, so they constrain values but
211
+ // never widen the key set. `not` never widens it. `unevaluatedProperties`
212
+ // is admitted only as `false` under that proof, where it is exactly
213
+ // `additionalProperties: false`.
214
+ if (node.$ref || node.patternProperties ||
178
215
  node.additionalProperties === true ||
179
216
  (node.additionalProperties && typeof node.additionalProperties === 'object')) {
180
217
  return null;
181
218
  }
219
+ if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== false) return null;
182
220
  if (node.type !== 'object' || !node.properties) return null;
183
221
  const keys = Object.keys(node.properties);
184
222
  if (keys.length === 0) return null;
223
+ if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
224
+ node.unevaluatedProperties === false) {
225
+ if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined ||
226
+ node.dependentSchemas !== undefined || node.dependencies !== undefined) return null;
227
+ const names = new Set();
228
+ const state = { prefix: 0 };
229
+ for (const k of ['allOf', 'anyOf', 'oneOf']) {
230
+ if (node[k] !== undefined) {
231
+ if (!Array.isArray(node[k])) return null;
232
+ for (const b of node[k]) if (!unevalContributions(b, 'props', names, state, 0)) return null;
233
+ }
234
+ }
235
+ for (const k of ['if', 'then', 'else']) {
236
+ if (node[k] !== undefined && !unevalContributions(node[k], 'props', names, state, 0)) return null;
237
+ }
238
+ for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
239
+ }
185
240
  const required = new Set(Array.isArray(node.required) ? node.required : []);
186
241
 
187
242
  // A property is copyable only when the emitter can name everything that
@@ -196,7 +251,22 @@ function emitClone(node, access, depth) {
196
251
  for (const key of keys) {
197
252
  const prop = node.properties[key];
198
253
  if (!prop || typeof prop !== 'object') return null;
199
- if (prop.$ref || prop.allOf || prop.anyOf || prop.oneOf || prop.not || prop.if) return null;
254
+ if (prop.$ref) return null;
255
+ // A property that is required and carries a default is the one place the
256
+ // runtime and a raw-input validation disagree: the runtime fills the
257
+ // default before checking `required`, so `{}` validates there and would
258
+ // throw here. Declining keeps parse() exactly as strict as validate().
259
+ if (required.has(key) && prop.default !== undefined) return null;
260
+ // A default the property's own schema rejects would make parse() hand
261
+ // back a document validate() refuses: the runtime fills defaults before
262
+ // validating, so it catches the bad default, and a raw-input check here
263
+ // never sees it. Probe the default at emit time and decline when it does
264
+ // not hold, or when the subschema cannot be compiled to check it.
265
+ if (prop.default !== undefined) {
266
+ let probe = null;
267
+ try { probe = compileToJSCodegen(prop, null, null); } catch (_) { probe = null; }
268
+ if (!probe || !probe(prop.default)) return null;
269
+ }
200
270
  const read = `${access}[${JSON.stringify(key)}]`;
201
271
  let value;
202
272
  const types = Array.isArray(prop.type) ? prop.type : [prop.type];
@@ -230,7 +300,7 @@ function emitClone(node, access, depth) {
230
300
  return null;
231
301
  }
232
302
  if (required.has(key)) fixed.push(`${JSON.stringify(key)}: ${value}`);
233
- else conditional.push({ key, value });
303
+ else conditional.push({ key, value, dflt: prop.default });
234
304
  }
235
305
  if (fixed.length === 0 && conditional.length === 0) return null;
236
306
 
@@ -240,7 +310,12 @@ function emitClone(node, access, depth) {
240
310
  // gains a key the input did not have.
241
311
  const tmp = `_c${depth}`;
242
312
  const adds = conditional
243
- .map(({ key, value }) => `if (${JSON.stringify(key)} in ${access}) ${tmp}[${JSON.stringify(key)}] = ${value};`)
313
+ .map(({ key, value, dflt }) => {
314
+ const set = `if (${JSON.stringify(key)} in ${access}) ${tmp}[${JSON.stringify(key)}] = ${value};`;
315
+ // The default is emitted as a literal inside the call, so an object or
316
+ // array default is a fresh value on every parse, never shared state.
317
+ return dflt === undefined ? set : `${set} else ${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
318
+ })
244
319
  .join(' ');
245
320
  return `(function(){ const ${tmp} = ${literal}; ${adds} return ${tmp}; })()`;
246
321
  }
@@ -294,29 +369,8 @@ function toStandaloneModule(validator, opts) {
294
369
 
295
370
  // Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
296
371
  let closureDecls = '';
297
- if (jsFn._closures && jsFn._closures.length > 0) {
298
- const lines = [];
299
- for (const { name, val } of jsFn._closures) {
300
- if (Array.isArray(val)) {
301
- lines.push(`const ${name} = ${JSON.stringify(val)};`);
302
- continue;
303
- }
304
- if (val && val.__ataSafe) {
305
- lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
306
- } else if (val instanceof RegExp) {
307
- const flags = val.flags;
308
- lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
309
- } else if (val instanceof Set) {
310
- lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
311
- } else if (typeof val === 'function') {
312
- // new Function('_ppv', body) — extract body from toString()
313
- const str = val.toString();
314
- // Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
315
- const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
316
- const body = m ? m[1].trim() : str;
317
- lines.push(`const ${name} = function(_ppv) { ${body} };`);
318
- }
319
- }
372
+ {
373
+ const lines = closureDeclLines(jsFn);
320
374
  if (lines.length) closureDecls = lines.join('\n') + '\n';
321
375
  }
322
376
 
@@ -381,8 +435,48 @@ function toStandaloneModule(validator, opts) {
381
435
  ? `function _ataParse(data) {\n if (!_fn(data)) { const e = new Error('validation failed'); e.name = 'AtaValidationError'; throw e; }\n return ${cloneExpr};\n}\n`
382
436
  : '';
383
437
 
438
+ // validateJSON(): parse the text, validate, and on failure attach a
439
+ // dataFrame (byte offset, line, col, source line) to every error by
440
+ // walking the original text once. Off by default for the same reason as
441
+ // parse(): it costs bytes in every emitted module. Ask for it with
442
+ // { positions: true }. The walker is the runtime's buildDataPositionMap,
443
+ // embedded verbatim via toString so the two cannot drift; that is why the
444
+ // function in lib/data-positions.js must stay self-contained.
445
+ const positionsCore = !(opts && opts.positions) ? '' : `const _ataPosMap = ${buildDataPositionMap.toString()};
446
+ function _ataFrame(p) { return { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text }; }
447
+ function validateJSON(text) {
448
+ const s = String(text);
449
+ let data;
450
+ try { data = JSON.parse(s); } catch (e) {
451
+ const nl = s.indexOf(String.fromCharCode(10));
452
+ let frame = { byteOffset: 0, length: s.length, line: 1, col: 1, text: nl === -1 ? s : s.slice(0, nl) };
453
+ try { const m = _ataPosMap(s); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
454
+ return { valid: false, errors: [{ code: 'ATA9001', message: 'invalid JSON document', keyword: '__parse__', path: '', instancePath: '', dataFrame: frame }] };
455
+ }
456
+ const r = validate(data);
457
+ if (r.valid || !r.errors || !r.errors.length) return r;
458
+ let map;
459
+ try { map = _ataPosMap(s); } catch (_) { return r; }
460
+ const errors = r.errors.map((e) => {
461
+ const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
462
+ const p = map[ptr];
463
+ return p ? Object.assign({}, e, { dataFrame: _ataFrame(p) }) : e;
464
+ });
465
+ return { valid: false, errors };
466
+ }
467
+ `;
468
+
384
469
  const baseNames = cloneExpr ? 'validate, isValid' : 'validate, isValid';
385
- const names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
470
+ // The content hash of the schema this module was compiled from, over the
471
+ // schema as the caller wrote it (before dialect normalization), so a build
472
+ // can compare it against schemaHash(currentSchema) and know the module is
473
+ // stale without embedding its own fingerprint.
474
+ const hashSrc = schemaHash(validator._rawSchema !== undefined ? validator._rawSchema : validator._schemaObj);
475
+ const hashDecl = `const schemaHash = ${JSON.stringify(hashSrc)};\n`;
476
+
477
+ let names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
478
+ names += ', schemaHash';
479
+ if (positionsCore) names += ', validateJSON';
386
480
  const parseAlias = cloneExpr ? ', _ataParse as parse' : '';
387
481
  const parseProp = cloneExpr ? ', parse: _ataParse' : '';
388
482
  const exports = format === 'esm'
@@ -411,7 +505,7 @@ ${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(
411
505
  };
412
506
  ${errCore}function isValid(data) { return _fn(data); }
413
507
  function validate(data) { ${validBody}; }
414
- ${parseCore}${exports}`;
508
+ ${parseCore}${positionsCore}${hashDecl}${exports}`;
415
509
  }
416
510
 
417
511
  // Bundle multiple validators into a single JS file for fast startup.
@@ -517,6 +611,8 @@ function bundleStandalone(Validator, schemas, opts) {
517
611
  ? (jsFn._preambleGuard || '') + (kept.length ? kept.join('\n ') + '\n ' : '')
518
612
  : (jsFn._preambleSource || '');
519
613
  if (preambleSrc) preamble = preamble ? `${preamble}\n${preambleSrc}` : preambleSrc;
614
+ const closureSrc = closureDeclLines(jsFn, 'var').join('\n');
615
+ if (closureSrc) preamble = preamble ? `${preamble}\n${closureSrc}\n` : closureSrc + '\n';
520
616
  if (opts && opts.verbose) {
521
617
  // Embed the schema and a small resolver so errors carry parentSchema.
522
618
  const schemaLit = JSON.stringify(v._schemaObj);
@@ -573,6 +669,7 @@ function bundleCompact(Validator, schemas, opts) {
573
669
  guard: jsFn._preambleGuard || '',
574
670
  parts: jsFn._preambleParts || null,
575
671
  preamble: jsFn._preambleSource || '',
672
+ closures: closureDeclLines(jsFn, 'var').join('\n'),
576
673
  hybridBody: jsFn._hybridSource,
577
674
  err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
578
675
  fmt: jsFn._formatClosures || null,
@@ -602,7 +699,11 @@ function bundleCompact(Validator, schemas, opts) {
602
699
  const preamble = kept !== null
603
700
  ? e.guard + (kept.length ? kept.join('\n ') + '\n ' : '')
604
701
  : e.preamble;
605
- e.hybrid = preamble ? `${preamble}\n${e.hybridBody}` : e.hybridBody;
702
+ // Factory-scope code: guard state, hoisted branch helpers, and the
703
+ // closure declarations (regexes above all), matching where the
704
+ // in-process compiler places them. Keeping them per call recompiled
705
+ // every pattern on every validation.
706
+ e.factory = (preamble ? preamble + '\n' : '') + (e.closures ? e.closures + '\n' : '');
606
707
  if (e.err) for (const h of sharedDecls) e.err = e.err.split(h).join('');
607
708
  }
608
709
 
@@ -614,11 +715,15 @@ function bundleCompact(Validator, schemas, opts) {
614
715
 
615
716
  const indices = entries.map((e) => {
616
717
  if (!e) return [-1, -1];
617
- let hi = bodyMap.get(e.hybrid);
718
+ // The dedupe key must include the factory code: two schemas can emit an
719
+ // identical body that references _re1 with different patterns, and the
720
+ // pattern now lives only in the factory-scope declaration.
721
+ const key = e.factory + '\u0000' + e.hybridBody;
722
+ let hi = bodyMap.get(key);
618
723
  if (hi === undefined) {
619
724
  hi = bodies.length;
620
- bodies.push(e.hybrid);
621
- bodyMap.set(e.hybrid, hi);
725
+ bodies.push({ factory: e.factory, body: e.hybridBody });
726
+ bodyMap.set(key, hi);
622
727
  }
623
728
  let ei = -1;
624
729
  if (e.err) {
@@ -661,7 +766,7 @@ function bundleCompact(Validator, schemas, opts) {
661
766
  // Shared hybrid factories
662
767
  out += `${declKW} H=[\n`;
663
768
  out += bodies
664
- .map((b) => `function(R,E){return function(d){${b}}}`)
769
+ .map((b) => `function(R,E){${b.factory}return function(d){${b.body}}}`)
665
770
  .join(',\n');
666
771
  out += '\n];\n';
667
772
 
@@ -6,10 +6,13 @@
6
6
  * AND abortEarly is off. Zero cost on the valid path.
7
7
  */
8
8
 
9
- const { escapePtr } = require('./source-positions');
10
-
9
+ // Self-contained on purpose: `toStandaloneModule({ positions: true })` embeds
10
+ // this function verbatim via Function.prototype.toString, so it may not close
11
+ // over anything in this module and may not assume Node globals. The inlined
12
+ // escapePtr below duplicates lib/source-positions.js for that reason.
11
13
  function buildDataPositionMap (input) {
12
- const text = Buffer.isBuffer(input) ? input.toString('utf8') : String(input);
14
+ const escapePtr = (s) => s.replace(/~/g, '~0').replace(/\//g, '~1');
15
+ const text = (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) ? input.toString('utf8') : String(input);
13
16
  const map = Object.create(null);
14
17
  const lines = text.split('\n');
15
18
  const lineStart = new Array(lines.length + 1);
@@ -1610,8 +1610,14 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1610
1610
  boolFn._hybridFactory = (R, E) => hybridFactory(...closureValues, R, E)
1611
1611
  } catch {}
1612
1612
 
1613
- // Store source for standalone compilation (includes regex inline for file output)
1614
- const helperStr = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
1613
+ // Store source for standalone compilation. Regex declarations are NOT
1614
+ // inlined into the body: they are carried by _closures and the emitters
1615
+ // declare them once at load scope. Inlining them here put a
1616
+ // `const _reN=__ataSafeRe(...)` inside the emitted function, which
1617
+ // shadowed the module-scope binding and recompiled the pattern on every
1618
+ // call, 25x on a schema with one pattern.
1619
+ const emitHelpers = ctx.helperCode.filter((c) => !/^const _re\d+=(?:__ataSafeRe|new RegExp)\(/.test(c))
1620
+ const helperStr = emitHelpers.length ? emitHelpers.join('\n ') + '\n ' : ''
1615
1621
  boolFn._source = helperStr + body
1616
1622
  boolFn._preambleSource = preambleStr
1617
1623
  boolFn._preambleGuard = guardStr
@@ -3219,6 +3225,110 @@ function childPathDynExpr(parentExpr, indexExpr) {
3219
3225
  return `${parentExpr}+'/'+${indexExpr}`
3220
3226
  }
3221
3227
 
3228
+ // unevaluated* differs from additionalProperties/items only through
3229
+ // annotations contributed by in-place applicators at the same schema object.
3230
+ // When every applicator subschema is provably free of the annotating keywords
3231
+ // for that kind, the keyword is exactly its plain counterpart, and the error
3232
+ // generators can emit real detail for it instead of declining. `required`
3233
+ // inside a then-branch annotates nothing, which is precisely the shape a
3234
+ // config schema has; a then-branch carrying `properties` does, and stays
3235
+ // declined. `not` never keeps its annotations, so it is skipped. A reference
3236
+ // or dependency keyword makes the node ineligible outright: resolving what
3237
+ // they might contribute is not this function's job.
3238
+ // Collects what an in-place applicator subschema can contribute: property
3239
+ // names into `names`, and the deepest tuple prefix it may evaluate into
3240
+ // `state.prefix`. Returns false when the contribution cannot be bounded
3241
+ // (patternProperties, additionalProperties, a reference, item schemas, and so
3242
+ // on), which makes the node ineligible outright.
3243
+ function unevalContributions (sub, kind, names, state, depth) {
3244
+ if (depth > 24) return false
3245
+ if (sub === true || sub === false) return true
3246
+ if (typeof sub !== 'object' || sub === null) return false
3247
+ if (kind === 'props') {
3248
+ if (sub.patternProperties !== undefined || sub.additionalProperties !== undefined || sub.unevaluatedProperties !== undefined) return false
3249
+ if (sub.properties !== undefined) {
3250
+ if (typeof sub.properties !== 'object' || sub.properties === null) return false
3251
+ for (const k of Object.keys(sub.properties)) names.add(k)
3252
+ }
3253
+ } else {
3254
+ if (sub.items !== undefined || sub.additionalItems !== undefined || sub.contains !== undefined || sub.unevaluatedItems !== undefined) return false
3255
+ if (sub.prefixItems !== undefined) {
3256
+ if (!Array.isArray(sub.prefixItems)) return false
3257
+ if (sub.prefixItems.length > state.prefix) state.prefix = sub.prefixItems.length
3258
+ }
3259
+ }
3260
+ if (sub.$ref !== undefined || sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined) return false
3261
+ if (sub.dependentSchemas !== undefined || sub.dependencies !== undefined) return false
3262
+ for (const k of ['allOf', 'anyOf', 'oneOf']) {
3263
+ if (sub[k] !== undefined) {
3264
+ if (!Array.isArray(sub[k])) return false
3265
+ for (const b of sub[k]) if (!unevalContributions(b, kind, names, state, depth + 1)) return false
3266
+ }
3267
+ }
3268
+ for (const k of ['if', 'then', 'else']) {
3269
+ if (sub[k] !== undefined && !unevalContributions(sub[k], kind, names, state, depth + 1)) return false
3270
+ }
3271
+ return true
3272
+ }
3273
+
3274
+ // A contribution that names only what the node's own adjacent keywords
3275
+ // already evaluate unconditionally changes nothing: a key in the node's own
3276
+ // `properties` is evaluated whether or not the branch that also names it
3277
+ // passes, and a branch prefix no longer than the node's own is covered
3278
+ // position for position. Anything beyond those bounds is conditional
3279
+ // evaluation the plain counterpart cannot express, and the node stays with
3280
+ // the interpreted engine.
3281
+ function unevalLocalOk (node, key) {
3282
+ if (node[key] !== false) return false
3283
+ if (node.$ref !== undefined || node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return false
3284
+ if (node.dependentSchemas !== undefined || node.dependencies !== undefined) return false
3285
+ const kind = key === 'unevaluatedProperties' ? 'props' : 'items'
3286
+ if (kind === 'props' && node.patternProperties !== undefined) return false
3287
+ if (kind === 'items' && node.contains !== undefined) return false
3288
+ const names = new Set()
3289
+ const state = { prefix: 0 }
3290
+ for (const k of ['allOf', 'anyOf', 'oneOf']) {
3291
+ if (node[k] !== undefined) {
3292
+ if (!Array.isArray(node[k])) return false
3293
+ for (const b of node[k]) if (!unevalContributions(b, kind, names, state, 0)) return false
3294
+ }
3295
+ }
3296
+ for (const k of ['if', 'then', 'else']) {
3297
+ if (node[k] !== undefined && !unevalContributions(node[k], kind, names, state, 0)) return false
3298
+ }
3299
+ if (kind === 'props') {
3300
+ const own = node.properties && typeof node.properties === 'object' ? node.properties : {}
3301
+ for (const n of names) if (!Object.prototype.hasOwnProperty.call(own, n)) return false
3302
+ } else {
3303
+ const ownPrefix = Array.isArray(node.prefixItems) ? node.prefixItems.length
3304
+ : (Array.isArray(node.items) ? node.items.length : 0)
3305
+ if (node.items !== undefined && !Array.isArray(node.items)) { /* all evaluated; any prefix is covered */ }
3306
+ else if (state.prefix > ownPrefix) return false
3307
+ }
3308
+ return true
3309
+ }
3310
+
3311
+ // Every occurrence in the document must qualify or the generator declines the
3312
+ // whole schema, exactly as the string scan it replaces did. The walk is
3313
+ // generic and may visit data positions (an enum value, say); judging those is
3314
+ // harmless, since a data node that fails the test only makes the answer more
3315
+ // conservative and the generators never read data positions as schemas.
3316
+ function unevalAllProvablyLocal (root) {
3317
+ const seen = new Set()
3318
+ const stack = [root]
3319
+ while (stack.length) {
3320
+ const n = stack.pop()
3321
+ if (n === null || typeof n !== 'object' || seen.has(n)) continue
3322
+ seen.add(n)
3323
+ if (!Array.isArray(n)) {
3324
+ if (n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
3325
+ if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
3326
+ }
3327
+ for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
3328
+ }
3329
+ return true
3330
+ }
3331
+
3222
3332
  // Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
3223
3333
  // Returns a JS expression string or null if pattern is too complex
3224
3334
  function fastPrefixCheck(pattern, keyVar) {
@@ -3267,10 +3377,12 @@ function genCharCodeSwitch(keys, v) {
3267
3377
  // Returns a function: (data, allErrors) => { valid, errors }
3268
3378
  // Valid path is still fast — only error path does extra work.
3269
3379
  function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
3270
- // Bail on unevaluated keywords — error codegen doesn't support them yet
3380
+ // unevaluated* is generated only where it is provably its plain counterpart
3381
+ // (see unevalLocalOk); any occurrence that is not keeps the whole schema on
3382
+ // the interpreted engine's error path, exactly as before.
3271
3383
  if (typeof schema === 'object' && schema !== null) {
3272
3384
  const s = JSON.stringify(schema)
3273
- if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
3385
+ if ((s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) && !unevalAllProvablyLocal(schema)) return null
3274
3386
  }
3275
3387
  if (typeof schema === 'boolean') {
3276
3388
  return schema
@@ -3409,7 +3521,11 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3409
3521
  })
3410
3522
  }
3411
3523
  return fn
3412
- } catch {
3524
+ } catch (e) {
3525
+ // Declining here is safe (the interpreter takes over) but silent, which
3526
+ // is how every past regression in this file hid. The env hook makes the
3527
+ // decline visible without changing behavior.
3528
+ if (process.env.ATA_DEBUG_ERRGEN) console.error('error codegen declined:', e.message)
3413
3529
  return null
3414
3530
  }
3415
3531
  }
@@ -3774,6 +3890,52 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3774
3890
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
3775
3891
  }
3776
3892
 
3893
+ // unevaluatedProperties: false, on a node where it is provably its plain
3894
+ // counterpart (the bail above guaranteed that for every occurrence). With
3895
+ // additionalProperties present everything is evaluated and there is nothing
3896
+ // to emit. The error keeps the keyword's own identity: the interpreted
3897
+ // engine reports these as unevaluatedProperties with the key named, and the
3898
+ // two engines are pinned to identical output.
3899
+ if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) {
3900
+ const allowedU = Object.keys(schema.properties || {}).map(k => `${JSON.stringify(k)}`).join(',')
3901
+ const ui = ctx.varCounter++
3902
+ const upSp = `${schemaPrefix}/unevaluatedProperties`
3903
+ const upLit = buildErrorLiteral({ keyword: 'unevaluatedProperties', schemaPath: upSp, sourceMap: ctx.sourceMap })
3904
+ const patChecksU = []
3905
+ for (const pat of Object.keys(schema.patternProperties || {})) {
3906
+ const pattern = JSON.stringify(pat)
3907
+ if (!ctx.regExpMap.has(pattern)) {
3908
+ const ri = ctx.varCounter++
3909
+ ctx.regExpMap.set(pattern, ri)
3910
+ if (patternIsSafe(pat)) {
3911
+ ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
3912
+ ctx.usesSafeRe = true
3913
+ } else {
3914
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
3915
+ }
3916
+ }
3917
+ patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
3918
+ }
3919
+ const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
3920
+ const innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
3921
+ lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
3922
+ }
3923
+
3924
+ // unevaluatedItems: false under the same rule. The interpreted engine
3925
+ // reports one error for the array with the first unevaluated position as
3926
+ // the limit; with adjacent `items` everything is evaluated. The draft-07
3927
+ // array form of items acts as the prefix.
3928
+ if (schema.unevaluatedItems === false) {
3929
+ const itemsIsPrefix = Array.isArray(schema.items)
3930
+ const allEval = schema.items !== undefined && !itemsIsPrefix
3931
+ if (!allEval) {
3932
+ const plen = itemsIsPrefix ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
3933
+ const uiSp = `${schemaPrefix}/unevaluatedItems`
3934
+ const uiLit = buildErrorLiteral({ keyword: 'unevaluatedItems', schemaPath: uiSp, sourceMap: ctx.sourceMap })
3935
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${plen}){_e.push({code:'${uiLit.codeStr}',keyword:'unevaluatedItems',instancePath:${pathExpr||'""'},schemaPath:'${uiSp}'${ordinalField(ctx, `${uiSp}`)},params:{limit:${plen}},message:'must NOT have more than ${plen} items',docUrl:'${uiLit.docUrl}'${uiLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
3936
+ }
3937
+ }
3938
+
3777
3939
  // dependentRequired
3778
3940
  if (schema.dependentRequired) {
3779
3941
  const drSp = `${schemaPrefix}/dependentRequired`
@@ -4004,10 +4166,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4004
4166
  // Avoids double-pass (jsFn → false → errFn runs same checks again).
4005
4167
  // Uses type-aware optimizations: after type check passes, skip guards.
4006
4168
  function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
4007
- // Bail on unevaluated keywords — combined codegen doesn't support them yet
4169
+ // Same provably-local rule as the error generator above.
4008
4170
  if (typeof schema === 'object' && schema !== null) {
4009
4171
  const s = JSON.stringify(schema)
4010
- if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
4172
+ if ((s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) && !unevalAllProvablyLocal(schema)) return null
4011
4173
  // Bail on self-referencing schemas — combined codegen doesn't support recursion
4012
4174
  if (s.includes('"$ref":"#"')) return null
4013
4175
  // Defs on a $ref cycle compile as named functions in the boolean and
@@ -4403,6 +4565,39 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4403
4565
  }
4404
4566
  }
4405
4567
 
4568
+ // unevaluatedProperties: false where it is provably additionalProperties
4569
+ // (the entry bail guaranteed that), keeping the keyword's own identity so
4570
+ // the two engines report the same error.
4571
+ if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined && !schema.patternProperties) {
4572
+ const propKeysU = Object.keys(schema.properties || {})
4573
+ const ui = ctx.varCounter++
4574
+ const failU = fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:_k${ui}[_i]}`, "'must NOT have unevaluated properties'")
4575
+ let innerU
4576
+ if (propKeysU.length === 0) {
4577
+ innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){${failU}}`
4578
+ } else if (propKeysU.length <= 8) {
4579
+ const checksU = propKeysU.map(k => `_k${ui}[_i]!==${JSON.stringify(k)}`).join('&&')
4580
+ innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(${checksU}){${failU}}`
4581
+ } else {
4582
+ const allowedU = propKeysU.map(k => JSON.stringify(k)).join(',')
4583
+ innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++)if(!_a${ui}.has(_k${ui}[_i])){${failU}}`
4584
+ }
4585
+ lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
4586
+ }
4587
+
4588
+ // unevaluatedItems: false under the same rule: one error for the array,
4589
+ // the first uncovered position as the limit. Adjacent schema-form `items`
4590
+ // evaluates everything; the draft-07 array form acts as the prefix.
4591
+ if (schema.unevaluatedItems === false) {
4592
+ const itemsIsPrefixC = Array.isArray(schema.items)
4593
+ const allEvalC = schema.items !== undefined && !itemsIsPrefixC
4594
+ if (!allEvalC) {
4595
+ const plenC = itemsIsPrefixC ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
4596
+ const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${plenC}}`, `'must NOT have more than ${plenC} items'`)
4597
+ lines.push(`if(Array.isArray(${v})&&${v}.length>${plenC}){${failI}}`)
4598
+ }
4599
+ }
4600
+
4406
4601
  // dependentRequired
4407
4602
  if (schema.dependentRequired) {
4408
4603
  for (const [key, deps] of Object.entries(schema.dependentRequired)) {
@@ -4807,4 +5002,4 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
4807
5002
  // not → contributes nothing (spec: annotations from not are discarded)
4808
5003
  }
4809
5004
 
4810
- module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, AJV_MESSAGES }
5005
+ module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, AJV_MESSAGES }
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ // A stable content hash for a schema, so a compiled artifact can say which
4
+ // schema it came from and a build can detect staleness by comparing hashes
5
+ // instead of embedding its own. The hash is over a canonical JSON form
6
+ // (object keys sorted at every level), so the same schema hashes the same
7
+ // however its keys were ordered, and it is FNV-1a with two independent
8
+ // bases, 16 hex characters. This is an integrity aid, not a security
9
+ // boundary: it detects drift, it does not authenticate anything.
10
+
11
+ function canonical (value) {
12
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
13
+ if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']';
14
+ const keys = Object.keys(value).sort();
15
+ const parts = [];
16
+ for (const k of keys) {
17
+ if (value[k] === undefined) continue;
18
+ parts.push(JSON.stringify(k) + ':' + canonical(value[k]));
19
+ }
20
+ return '{' + parts.join(',') + '}';
21
+ }
22
+
23
+ function fnv1a (str, seed) {
24
+ let h = seed >>> 0;
25
+ for (let i = 0; i < str.length; i++) {
26
+ h ^= str.charCodeAt(i);
27
+ h = Math.imul(h, 0x01000193) >>> 0;
28
+ }
29
+ return h >>> 0;
30
+ }
31
+
32
+ function schemaHash (schema) {
33
+ const text = canonical(schema);
34
+ const a = fnv1a(text, 0x811c9dc5);
35
+ const b = fnv1a(text, 0xcbf29ce4);
36
+ return a.toString(16).padStart(8, '0') + b.toString(16).padStart(8, '0');
37
+ }
38
+
39
+ module.exports = { schemaHash };
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.24.0';
10
+ module.exports = '1.25.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -51,7 +51,7 @@
51
51
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
52
52
  "build": "cmake-js build --target ata",
53
53
  "rebuild": "cmake-js rebuild --target ata",
54
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_uri_helper_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
54
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_scanner_differential.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_uri_helper_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_file_positions.js && node tests/test_schema_hash.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
55
55
  "bench:size": "node benchmark/bench_aot_size.mjs",
56
56
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
57
57
  "test:compat": "node tests/test_compat.js",
@@ -122,13 +122,13 @@
122
122
  "LICENSE"
123
123
  ],
124
124
  "optionalDependencies": {
125
- "@ata-validator/native-darwin-arm64": "1.24.0",
126
- "@ata-validator/native-darwin-x64": "1.24.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.24.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.24.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.24.0",
130
- "@ata-validator/native-linux-x64-musl": "1.24.0",
131
- "@ata-validator/native-win32-x64": "1.24.0"
125
+ "@ata-validator/native-darwin-arm64": "1.25.0",
126
+ "@ata-validator/native-darwin-x64": "1.25.0",
127
+ "@ata-validator/native-linux-arm64-gnu": "1.25.0",
128
+ "@ata-validator/native-linux-arm64-musl": "1.25.0",
129
+ "@ata-validator/native-linux-x64-gnu": "1.25.0",
130
+ "@ata-validator/native-linux-x64-musl": "1.25.0",
131
+ "@ata-validator/native-win32-x64": "1.25.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"