ata-validator 1.29.0 → 1.30.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/index.js CHANGED
@@ -500,7 +500,7 @@ Object.defineProperty(RichRejection.prototype, 'errors', {
500
500
  // The position map, resolved now that an error is actually being read.
501
501
  let positions = null;
502
502
  if (enrich && raw.length && this._rawInput != null) {
503
- positions = self._pos().get(this._rawInput);
503
+ positions = self._pos().targeted(this._rawInput, wantedPointersFor(raw));
504
504
  if (positions) self._posCache.reset();
505
505
  }
506
506
  // One options object for the whole list, not one per error.
@@ -571,7 +571,7 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
571
571
  // to raw errors, and detecting on it left every collapsed oneOf/anyOf error
572
572
  // unenriched on the text path.
573
573
  if (!raw[0] || !raw[0].docUrl) {
574
- const positions = self._pos().get(jsonStr);
574
+ const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
575
575
  // Declaration order, as validate() applies it; the text path used to
576
576
  // enrich in emission order.
577
577
  const ordered = raw.length > 1 ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
@@ -599,7 +599,7 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
599
599
  // them is worth another walk of the document: resolving the map to discover
600
600
  // there was nothing to fill cost a second full walk on every rejection.
601
601
  if (raw.some((e) => e && !e.dataFrame)) {
602
- const positions = self._pos().get(jsonStr);
602
+ const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw.filter((e) => e && !e.dataFrame)));
603
603
  if (positions) {
604
604
  for (const e of raw) {
605
605
  if (e && !e.dataFrame) {
@@ -623,6 +623,31 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
623
623
  },
624
624
  });
625
625
 
626
+ // The pointers a set of errors will ask the position map about. lib/enrich-error
627
+ // looks up the error's own path, and for an additional or unevaluated property
628
+ // the child pointer named in `params`, unescaped, which is the form it asks for.
629
+ // The escaped form goes in too: including a pointer that is never read costs
630
+ // nothing, and the cache answers anything outside this set from the full map
631
+ // rather than reporting no position.
632
+ function wantedPointersFor (errors) {
633
+ const wanted = new Set();
634
+ for (const e of errors) {
635
+ if (!e) continue;
636
+ const path = e.path != null ? e.path : (e.instancePath || '');
637
+ wanted.add(path);
638
+ if (e.instancePath != null && e.instancePath !== path) wanted.add(e.instancePath);
639
+ const params = e.params;
640
+ const named = params && (params.additionalProperty || params.unevaluatedProperty);
641
+ if (typeof named === 'string') {
642
+ wanted.add(path + '/' + named);
643
+ if (named.indexOf('~') !== -1 || named.indexOf('/') !== -1) {
644
+ wanted.add(path + '/' + named.replace(/~/g, '~0').replace(/\//g, '~1'));
645
+ }
646
+ }
647
+ }
648
+ return wanted;
649
+ }
650
+
626
651
  // A raw error without the `_o` ordering key, for the legacy error shape.
627
652
  function stripOrdinal(e) {
628
653
  if (e === null || typeof e !== 'object' || e._o === undefined) return e;
@@ -1450,10 +1475,32 @@ class Validator {
1450
1475
  // The factory bakes the error function in as an argument and never
1451
1476
  // calls it for a document that passes, so a resolver here costs the
1452
1477
  // accepted path nothing and keeps the compile off the first call.
1453
- const hybridFn = jsFn._hybridFactory(VALID_RESULT, errPreferCombined);
1478
+ // Until the first rejection this is the hybrid: the verdict function,
1479
+ // with the error resolver baked in and never called for a document that
1480
+ // passes. That keeps the combined function's compile off the first call,
1481
+ // which is two thirds of what a first call costs.
1482
+ //
1483
+ // From the first rejection on, the combined function answers directly.
1484
+ // It decides and collects in one pass, so the verdict pass in front of it
1485
+ // was validating the document a second time: 134.1 microseconds against
1486
+ // its 45.3 on a 1000-user array. Swapping rather than starting there keeps
1487
+ // the lazy compile, and swapping at all is only free because the combined
1488
+ // function now costs what the verdict function costs on accepted
1489
+ // documents (1.02x on that array, 0.99x on a small body) since
1490
+ // additionalProperties stopped materialising its key array. The
1491
+ // indirection this needs measured inside the noise at both sizes.
1492
+ let impl = null;
1493
+ const onReject = (data) => {
1494
+ const combined = combinedIfSafe();
1495
+ if (combined) { impl = combined; return combined(data); }
1496
+ return errOnly(data);
1497
+ };
1498
+ const hybridFn = jsFn._hybridFactory(VALID_RESULT, onReject);
1499
+ impl = hybridFn;
1500
+ const run = (data) => impl(data);
1454
1501
  this.validate = preprocess
1455
- ? (data) => { preprocess(data); return hybridFn(data); }
1456
- : hybridFn;
1502
+ ? (data) => { preprocess(data); return run(data); }
1503
+ : run;
1457
1504
  } else {
1458
1505
  // No hybrid factory, so the assembly needs the function itself rather
1459
1506
  // than a reference it can call later: build it now.
@@ -1530,7 +1577,11 @@ class Validator {
1530
1577
  : jsonValidateInner;
1531
1578
  this.validateJSON = useSimdjsonForLarge && native && !preprocess
1532
1579
  ? (jsonStr) => {
1533
- if (jsonStr.length >= SIMDJSON_THRESHOLD) {
1580
+ // `_skipNativeFast` is set by the scanner short-circuit below when it
1581
+ // has already decided the document is invalid. The encode and the
1582
+ // native call would run only to return false, and the error path
1583
+ // underneath does not need them.
1584
+ if (jsonStr.length >= SIMDJSON_THRESHOLD && this._skipNativeFast !== true) {
1534
1585
  this._ensureNative();
1535
1586
  const buf = Buffer.from(jsonStr);
1536
1587
  if (native.rawFastValidate(this._fastSlot, buf))
@@ -1979,8 +2030,23 @@ class Validator {
1979
2030
  };
1980
2031
  return self.isValidJSON(jsonStr);
1981
2032
  };
1982
- if (options.abortEarly) {
2033
+ // validateJSON gets the same short-circuit isValidJSON has. The verdict is
2034
+ // a property of the text, and the scanner reads the text once and
2035
+ // allocates nothing; above the simdjson threshold the path underneath
2036
+ // encoded the whole document to a Buffer and called the native validator
2037
+ // instead, which measured 340 microseconds against the scanner's 191 on a
2038
+ // 149 KB config, the same against the published addon as against a local
2039
+ // build.
2040
+ //
2041
+ // An accepted document stops at the scanner. A rejected one still has to
2042
+ // produce errors, so it goes on to the path below, which is the
2043
+ // rich-errors wrapper and everything under it; the flag only tells that
2044
+ // path to skip an encode and a native call that would return false. Doing
2045
+ // it the other way, returning errors from the inner function directly,
2046
+ // would hand back errors that never passed through enrichment.
2047
+ {
1983
2048
  const validateByParsing = this.validateJSON;
2049
+ const abortEarly = !!options.abortEarly;
1984
2050
  this.validateJSON = (jsonStr) => {
1985
2051
  const scan = self._ensureScanner();
1986
2052
  if (scan === undefined) return validateByParsing(jsonStr);
@@ -1989,7 +2055,15 @@ class Validator {
1989
2055
  if (typeof text === 'string') {
1990
2056
  const r = scan(text);
1991
2057
  if (r === 1) return VALID_RESULT;
1992
- if (r === 0) return ABORT_EARLY_RESULT;
2058
+ if (r === 0) {
2059
+ if (abortEarly) return ABORT_EARLY_RESULT;
2060
+ self._skipNativeFast = true;
2061
+ try {
2062
+ return validateByParsing(text);
2063
+ } finally {
2064
+ self._skipNativeFast = false;
2065
+ }
2066
+ }
1993
2067
  }
1994
2068
  return validateByParsing(text);
1995
2069
  };
package/lib/aot-impl.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // loadBundle(Validator, mods, schemas, opts)
15
15
 
16
16
  const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions } = require('./js-compiler');
17
- const { buildDataPositionMap } = require('./data-positions');
17
+ const { buildTargetedPositionMap } = require('./data-positions');
18
18
  const { schemaHash } = require('./schema-hash');
19
19
  const ATA_VERSION = require('./version');
20
20
  const SAFE_REGEX_SOURCE = require('./safe-regex-source');
@@ -595,10 +595,15 @@ function toStandaloneModule(validator, opts) {
595
595
  // dataFrame (byte offset, line, col, source line) to every error by
596
596
  // walking the original text once. Off by default for the same reason as
597
597
  // parse(): it costs bytes in every emitted module. Ask for it with
598
- // { positions: true }. The walker is the runtime's buildDataPositionMap,
598
+ // { positions: true }. The walker is the runtime's buildTargetedPositionMap,
599
599
  // embedded verbatim via toString so the two cannot drift; that is why the
600
- // function in lib/data-positions.js must stay self-contained.
601
- const positionsCore = !(opts && opts.positions) ? '' : `const _ataPosMap = ${buildDataPositionMap.toString()};
600
+ // function in lib/data-positions.js must stay self-contained. It is given the
601
+ // pointers the errors name, which is the only thing this module looks up, so a
602
+ // subtree that cannot hold one is scanned to its end and never walked: framing
603
+ // an error went from 8.02x a JSON.parse of the document to 1.72x, and 3.11x in
604
+ // the worst case of a pointer at the very end. It is also 125 characters
605
+ // smaller than the full-map builder it replaces.
606
+ const positionsCore = !(opts && opts.positions) ? '' : `const _ataPosMap = ${buildTargetedPositionMap.toString()};
602
607
  function _ataFrame(p) { return { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text }; }
603
608
  function validateJSON(text) {
604
609
  const s = String(text);
@@ -606,13 +611,15 @@ function validateJSON(text) {
606
611
  try { data = JSON.parse(s); } catch (e) {
607
612
  const nl = s.indexOf(String.fromCharCode(10));
608
613
  let frame = { byteOffset: 0, length: s.length, line: 1, col: 1, text: nl === -1 ? s : s.slice(0, nl) };
609
- try { const m = _ataPosMap(s); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
614
+ try { const m = _ataPosMap(s, new Set([''])); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
610
615
  return { valid: false, errors: [{ code: 'ATA9001', message: 'invalid JSON document', keyword: '__parse__', path: '', instancePath: '', dataFrame: frame }] };
611
616
  }
612
617
  const r = validate(data);
613
618
  if (r.valid || !r.errors || !r.errors.length) return r;
614
619
  let map;
615
- try { map = _ataPosMap(s); } catch (_) { return r; }
620
+ const _want = new Set();
621
+ for (const e of r.errors) _want.add(e.instancePath != null ? e.instancePath : (e.path || ''));
622
+ try { map = _ataPosMap(s, _want); } catch (_) { return r; }
616
623
  const errors = r.errors.map((e) => {
617
624
  const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
618
625
  const p = map[ptr];
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { buildDataPositionMap } = require('./data-positions');
3
+ const { buildDataPositionMap, buildTargetedPositionMap } = require('./data-positions');
4
4
 
5
5
  /**
6
6
  * Memoize the position map for the duration of a single validate() call.
@@ -12,7 +12,50 @@ function createCache () {
12
12
  const wm = new WeakMap();
13
13
  const sm = new Map(); // strings can't go in WeakMap; clear after each validate
14
14
  let lastInput = null;
15
- return {
15
+ const cache = {
16
+ /**
17
+ * The entries for a known set of pointers, which is what a rejection needs:
18
+ * recording one per node in the document was the dominant cost of reading an
19
+ * error's frame.
20
+ *
21
+ * The caller derives `wanted` from the errors, and a derivation that misses a
22
+ * pointer must not cost a frame, so the result answers any pointer outside
23
+ * the set by building the full map once and reading from that. A wrong
24
+ * derivation is then slow rather than silently short of frames, which is the
25
+ * only acceptable direction for this trade: a missing caret is the failure
26
+ * nobody reports.
27
+ */
28
+ targeted (input, wanted) {
29
+ if (input == null) return null;
30
+ if (!wanted || wanted.size === 0) return null;
31
+ const already = typeof input === 'string'
32
+ ? sm.get(input)
33
+ : (Buffer.isBuffer(input) ? wm.get(input) : undefined);
34
+ if (already) return already;
35
+ let filtered;
36
+ try {
37
+ filtered = buildTargetedPositionMap(input, wanted);
38
+ } catch {
39
+ return null;
40
+ }
41
+ let full;
42
+ const fallback = (key) => {
43
+ if (wanted.has(key)) return undefined; // asked for, absent from the document
44
+ if (full === undefined) full = cache.get(input) || null;
45
+ return full ? full[key] : undefined;
46
+ };
47
+ return new Proxy(filtered, {
48
+ get (target, key) {
49
+ if (typeof key !== 'string') return target[key];
50
+ if (key in target) return target[key];
51
+ return fallback(key);
52
+ },
53
+ has (target, key) {
54
+ if (typeof key !== 'string') return key in target;
55
+ return (key in target) || fallback(key) !== undefined;
56
+ },
57
+ });
58
+ },
16
59
  get (input) {
17
60
  if (input == null) return null;
18
61
  if (typeof input === 'string') {
@@ -38,6 +81,7 @@ function createCache () {
38
81
  lastInput = null;
39
82
  },
40
83
  };
84
+ return cache;
41
85
  }
42
86
 
43
87
  module.exports = { createCache };
@@ -168,4 +168,203 @@ function buildDataPositionMap (input) {
168
168
  return map;
169
169
  }
170
170
 
171
- module.exports = { buildDataPositionMap };
171
+ /**
172
+ * The same map, for a known set of pointers.
173
+ *
174
+ * A rejection names two or three pointers and nothing reads any other entry, so
175
+ * recording one per node in the document is nearly all waste: on a 149 KB config
176
+ * the full map was 1330 microseconds of a 2457 microsecond error read. Here a
177
+ * subtree that cannot contain a wanted pointer is scanned to its end and never
178
+ * walked, and the walk stops once every wanted pointer has been recorded.
179
+ * Measured against the full map: 11.5x on pointers near the start of the
180
+ * document, and 3.7x in the worst case for the technique, a single pointer at
181
+ * the very end.
182
+ *
183
+ * Deliberately a second function rather than an option on the first. That one is
184
+ * embedded verbatim into standalone modules via Function.prototype.toString, and
185
+ * adding per-node checks to it cost 8% on the full-map path for callers that want
186
+ * every entry. The price is two walks of the same grammar, which
187
+ * `tests/test_targeted_positions.js` holds to identical output.
188
+ *
189
+ * Entries are identical to the full map's for every pointer in `wanted`. A
190
+ * pointer absent from the document is absent here too, exactly as it would be.
191
+ */
192
+ function buildTargetedPositionMap (input, wanted) {
193
+ const text = (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) ? input.toString('utf8') : String(input);
194
+ const map = Object.create(null);
195
+ const n = text.length;
196
+
197
+ // Every prefix of every wanted pointer: the containers worth descending into.
198
+ const interest = new Set(['']);
199
+ for (const w of wanted) {
200
+ let at = 0;
201
+ for (;;) {
202
+ const next = w.indexOf('/', at + 1);
203
+ if (next === -1) break;
204
+ interest.add(w.slice(0, next));
205
+ at = next;
206
+ }
207
+ interest.add(w);
208
+ }
209
+ let remaining = wanted.size;
210
+
211
+ const ptrs = [];
212
+ const starts = [];
213
+ const lens = [];
214
+ const keyStarts = [];
215
+ const keyLens = [];
216
+
217
+ let i = 0;
218
+
219
+ function skipWs () {
220
+ while (i < n) {
221
+ const ch = text.charCodeAt(i);
222
+ if (ch === 0x20 || ch === 0x09 || ch === 0x0a || ch === 0x0d) i++; else break;
223
+ }
224
+ }
225
+
226
+ function readString (decode) {
227
+ const start = i;
228
+ let simple = text.charCodeAt(i) === 0x22;
229
+ i++;
230
+ let closed = false;
231
+ while (i < n) {
232
+ const ch = text.charCodeAt(i);
233
+ if (ch === 0x5c) { simple = false; i += 2; continue; }
234
+ if (ch === 0x22) { i++; closed = true; break; }
235
+ if (ch < 0x20) simple = false;
236
+ i++;
237
+ }
238
+ if (!closed) throw new Error('unterminated string at offset ' + start);
239
+ if (simple) return decode ? text.slice(start + 1, i - 1) : null;
240
+ const decoded = JSON.parse(text.slice(start, i));
241
+ return decode ? decoded : null;
242
+ }
243
+
244
+ // Past one value, recording nothing inside it. Strings are consumed through
245
+ // readString so a brace inside one cannot move the depth.
246
+ function skipValue () {
247
+ skipWs();
248
+ if (i >= n) return;
249
+ const ch = text.charCodeAt(i);
250
+ if (ch === 0x22) { readString(false); return; }
251
+ if (ch === 0x7b || ch === 0x5b) {
252
+ let depth = 0;
253
+ while (i < n) {
254
+ const c = text.charCodeAt(i);
255
+ if (c === 0x22) { readString(false); continue; }
256
+ if (c === 0x7b || c === 0x5b) { depth++; i++; continue; }
257
+ if (c === 0x7d || c === 0x5d) { depth--; i++; if (depth <= 0) return; continue; }
258
+ i++;
259
+ }
260
+ return;
261
+ }
262
+ while (i < n) {
263
+ const c = text.charCodeAt(i);
264
+ if (c === 0x2c || c === 0x7d || c === 0x5d || c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) break;
265
+ i++;
266
+ }
267
+ }
268
+
269
+ function walk (ptr, keyStart, keyLen) {
270
+ skipWs();
271
+ if (i >= n) return;
272
+ const start = i;
273
+ const isWanted = wanted.has(ptr);
274
+
275
+ if (!interest.has(ptr)) {
276
+ skipValue();
277
+ if (isWanted) { ptrs.push(ptr); starts.push(start); lens.push(i - start); keyStarts.push(keyStart); keyLens.push(keyLen); remaining--; }
278
+ return;
279
+ }
280
+
281
+ const ch = text.charCodeAt(i);
282
+ if (ch === 0x7b) {
283
+ i++;
284
+ for (;;) {
285
+ skipWs();
286
+ if (i >= n) break;
287
+ if (text.charCodeAt(i) === 0x7d) { i++; break; }
288
+ if (text.charCodeAt(i) === 0x2c) { i++; continue; }
289
+ skipWs();
290
+ const memberKeyStart = i;
291
+ const key = readString(true);
292
+ const memberKeyLen = i - memberKeyStart;
293
+ skipWs();
294
+ if (text.charCodeAt(i) !== 0x3a) throw new Error('expected ":" at offset ' + i);
295
+ i++;
296
+ const seg = (key.indexOf('~') === -1 && key.indexOf('/') === -1)
297
+ ? key
298
+ : key.replace(/~/g, '~0').replace(/\//g, '~1');
299
+ walk(ptr + '/' + seg, memberKeyStart, memberKeyLen);
300
+ if (remaining === 0) return;
301
+ }
302
+ } else if (ch === 0x5b) {
303
+ i++;
304
+ let idx = 0;
305
+ for (;;) {
306
+ skipWs();
307
+ if (i >= n) break;
308
+ if (text.charCodeAt(i) === 0x5d) { i++; break; }
309
+ if (text.charCodeAt(i) === 0x2c) { i++; continue; }
310
+ const before = i;
311
+ walk(ptr + '/' + idx, -1, 0);
312
+ if (remaining === 0) return;
313
+ if (i === before) break;
314
+ idx++;
315
+ }
316
+ } else if (ch === 0x22) {
317
+ readString(false);
318
+ } else {
319
+ while (i < n) {
320
+ const c = text.charCodeAt(i);
321
+ if (c === 0x2c || c === 0x7d || c === 0x5d || c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) break;
322
+ i++;
323
+ }
324
+ }
325
+
326
+ if (isWanted) { ptrs.push(ptr); starts.push(start); lens.push(i - start); keyStarts.push(keyStart); keyLens.push(keyLen); remaining--; }
327
+ }
328
+
329
+ if (text.charCodeAt(0) === 0xfeff) i = 1;
330
+ walk('', -1, 0);
331
+
332
+ if (ptrs.length === 0) return map;
333
+
334
+ const lines = text.split('\n');
335
+ const lineStart = new Array(lines.length + 1);
336
+ lineStart[0] = 0;
337
+ for (let k = 0; k < lines.length; k++) lineStart[k + 1] = lineStart[k] + lines[k].length + 1;
338
+
339
+ function lineOf (off) {
340
+ let lo = 0, hi = lineStart.length - 1;
341
+ while (lo < hi) {
342
+ const mid = (lo + hi + 1) >> 1;
343
+ if (lineStart[mid] <= off) lo = mid; else hi = mid - 1;
344
+ }
345
+ return lo;
346
+ }
347
+
348
+ for (let k = 0; k < ptrs.length; k++) {
349
+ const off = starts[k];
350
+ const line = lineOf(off);
351
+ const entry = {
352
+ byteOffset: off,
353
+ length: lens[k],
354
+ line: line + 1,
355
+ col: off - lineStart[line] + 1,
356
+ text: lines[line] || '',
357
+ };
358
+ if (keyStarts[k] >= 0) {
359
+ const keyLine = lineOf(keyStarts[k]);
360
+ entry.keyOffset = keyStarts[k];
361
+ entry.keyLength = keyLens[k];
362
+ entry.keyLine = keyLine + 1;
363
+ entry.keyCol = keyStarts[k] - lineStart[keyLine] + 1;
364
+ }
365
+ map[ptrs[k]] = entry;
366
+ }
367
+ return map;
368
+ }
369
+
370
+ module.exports = { buildDataPositionMap, buildTargetedPositionMap };
package/lib/formats.js CHANGED
@@ -202,12 +202,16 @@ function ipv6 (s) {
202
202
  // Labels of letters, digits and hyphens, each 1 to 63 characters, none
203
203
  // starting or ending with a hyphen, joined by single dots, 253 characters at
204
204
  // most. Same answers as the expression it replaces, read once.
205
- function hostname (s) {
206
- const n = s.length;
205
+ // `from` and `to` let a caller check a hostname inside a larger string, which is
206
+ // how the email check reads its domain half without slicing it out.
207
+ function hostname (s, from, to) {
208
+ const start = from === undefined ? 0 : from;
209
+ const end = to === undefined ? s.length : to;
210
+ const n = end - start;
207
211
  if (n === 0 || n > 253) return false;
208
212
  let labelLength = 0;
209
213
  let previous = 46; // a dot, so a leading hyphen is refused like a leading dot
210
- for (let i = 0; i < n; i++) {
214
+ for (let i = start; i < end; i++) {
211
215
  const c = s.charCodeAt(i);
212
216
  if (c === 46) {
213
217
  if (labelLength === 0 || previous === 45) return false;
@@ -686,15 +690,28 @@ function dateTimeSource (v, isStr) {
686
690
  `if(_dtu!==1439)return false}`);
687
691
  }
688
692
 
689
- function hostnameSource (v, isStr) {
690
- return guard(v, isStr, `const _n=${v}.length;if(_n===0||_n>253)return false;let _ll=0,_pv=46;` +
691
- `for(let _i=0;_i<_n;_i++){const _c=${v}.charCodeAt(_i);` +
693
+ // The scan, over a range of the string given as source expressions. The email
694
+ // check reads its domain half through this, without slicing it out, which was an
695
+ // allocation on every call.
696
+ //
697
+ // A separate function rather than optional parameters on hostnameSource: the code
698
+ // generator calls every entry of its format table as `fn(v, isStr, ctx)`, so a
699
+ // third parameter here would silently receive a context object and emit
700
+ // `_st=[object Object]`. That declines the whole compile, which is how it was
701
+ // found.
702
+ function hostnameRangeSource (v, st, en) {
703
+ return `const _st=${st},_en=${en},_n=_en-_st;if(_n===0||_n>253)return false;let _ll=0,_pv=46;` +
704
+ `for(let _i=_st;_i<_en;_i++){const _c=${v}.charCodeAt(_i);` +
692
705
  `if(_c===46){if(_ll===0||_pv===45)return false;_ll=0;_pv=_c;continue}` +
693
706
  `const _an=(_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90);` +
694
707
  `if(!_an&&_c!==45)return false;` +
695
708
  `if(_c===45&&_pv===46)return false;` +
696
709
  `if(++_ll>63)return false;_pv=_c}` +
697
- `if(_ll===0||_pv===45)return false`);
710
+ `if(_ll===0||_pv===45)return false`;
711
+ }
712
+
713
+ function hostnameSource (v, isStr) {
714
+ return guard(v, isStr, hostnameRangeSource(v, '0', `${v}.length`));
698
715
  }
699
716
 
700
717
  function uuidSource (v, isStr) {
@@ -856,12 +873,16 @@ function uriTemplateSource (v, isStr) {
856
873
  `if(_c===125)return false;` +
857
874
  `if(_c!==123&&(_c<=32||_c===127))return false;` +
858
875
  `if(_c!==123){_i++;continue}` +
859
- `const _e=${v}.indexOf('}',_i+1);` +
860
- `if(_e===-1)return false;` +
861
- `const _x=${v}.slice(_i+1,_e);` +
876
+ // `_ute`, not `_e`: this source is spliced into a generated function whose
877
+ // error accumulator is `_e`, and a `const _e` here shadowed it, so
878
+ // `(_e||(_e=[])).push(...)` ran against a number and threw. The verdict was
879
+ // right, so the suite never saw it; only a caller reading `.errors` did.
880
+ `const _ute=${v}.indexOf('}',_i+1);` +
881
+ `if(_ute===-1)return false;` +
882
+ `const _x=${v}.slice(_i+1,_ute);` +
862
883
  `if(_x===''||_x.indexOf('{')!==-1)return false;` +
863
884
  `if(!${URI_TEMPLATE_EXPR.toString()}.test(_x))return false;` +
864
- `_i=_e+1` +
885
+ `_i=_ute+1` +
865
886
  `}`;
866
887
  return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
867
888
  }
@@ -874,35 +895,69 @@ function uriTemplateSource (v, isStr) {
874
895
  // the dot that separates atoms. Written as code ranges rather than a regular
875
896
  // expression because this lands inside every emitted module that validates an
876
897
  // email, and the expression cost more bytes there than the whole check.
877
- function isEmailLocalChar (c) {
878
- return (c >= 48 && c <= 57) || (c >= 97 && c <= 122) || (c >= 65 && c <= 90) ||
879
- c === 46 || c === 33 || (c >= 35 && c <= 39) || c === 42 || c === 43 ||
880
- c === 45 || c === 47 || c === 61 || c === 63 || (c >= 94 && c <= 96) ||
881
- (c >= 123 && c <= 126);
882
- }
883
- const EMAIL_LOCAL_SRC = (v) => `((${v}>=48&&${v}<=57)||(${v}>=97&&${v}<=122)||(${v}>=65&&${v}<=90)||${v}===46||${v}===33||(${v}>=35&&${v}<=39)||${v}===42||${v}===43||${v}===45||${v}===47||${v}===61||${v}===63||(${v}>=94&&${v}<=96)||(${v}>=123&&${v}<=126))`;
898
+ // The emitted form keeps the comparison chain: a standalone module pays for the
899
+ // bytes of a 128-entry table in every file that validates an email, and the
900
+ // runtime pays for the chain on every call. Different budgets, same answers,
901
+ // which `tests/test_format_engine_parity.js` holds.
902
+ // Stated as the complement: of the printable range, only twelve characters are
903
+ // not atext or the dot, so excluding them is 54 source bytes shorter than listing
904
+ // what is allowed, and a standalone module carries this expression. Checked
905
+ // against the allowed-list form over every code point from 0 to 65536 in
906
+ // tests/test_formats_single_pass.js.
907
+ const EMAIL_LOCAL_SRC = (v) => `(${v}>32&&${v}<127&&${v}!==34&&${v}!==40&&${v}!==41&&${v}!==44&&${v}!==58&&${v}!==59&&${v}!==60&&${v}!==62&&${v}!==64&&${v}!==91&&${v}!==92&&${v}!==93)`;
908
+ // The same table treatment the rest of this file uses for character classes, and
909
+ // no slicing: the two halves are read in place by offset. The version that split
910
+ // the string at the '@' and tested each local character against a chain of ten
911
+ // comparisons cost 3.2x the hostname check it contains, and that one function was
912
+ // the only measured place ata validated slower than the default validator.
913
+ const EMAIL_LOCAL = new Uint8Array(128);
914
+ for (let i = 48; i <= 57; i++) EMAIL_LOCAL[i] = 1; // digits
915
+ for (let i = 97; i <= 122; i++) EMAIL_LOCAL[i] = 1; // a-z
916
+ for (let i = 65; i <= 90; i++) EMAIL_LOCAL[i] = 1; // A-Z
917
+ for (let i = 35; i <= 39; i++) EMAIL_LOCAL[i] = 1; // # $ % & '
918
+ for (let i = 94; i <= 96; i++) EMAIL_LOCAL[i] = 1; // ^ _ `
919
+ for (let i = 123; i <= 126; i++) EMAIL_LOCAL[i] = 1; // { | } ~
920
+ for (const c of [46, 33, 42, 43, 45, 47, 61, 63]) EMAIL_LOCAL[c] = 1; // . ! * + - / = ?
921
+
922
+ // One forward pass over the string. '@' is not a local-part character, so in an
923
+ // unquoted address the first one is the separator, and the local part can be
924
+ // checked on the way to finding it. The version that called lastIndexOf first
925
+ // read the string backwards before reading it forwards twice.
884
926
  function email(s) {
885
- const at = s.lastIndexOf('@');
886
- if (at <= 0 || at === s.length - 1) return false;
887
- const local = s.slice(0, at);
888
- const domain = s.slice(at + 1);
889
- if (local.length > 64) return false;
890
- if (local.charCodeAt(0) === 34) {
891
- // quoted string: must close, and the quote may only end the local part
892
- if (local.charCodeAt(local.length - 1) !== 34 || local.length < 2) return false;
927
+ const n = s.length;
928
+ if (n < 3) return false;
929
+ let at;
930
+ if (s.charCodeAt(0) === 34) {
931
+ // A quoted local part may contain '@', so it needs the last one. Rare enough
932
+ // to keep the extra scan rather than complicate the common path.
933
+ at = s.lastIndexOf('@');
934
+ if (at < 2 || at === n - 1 || at > 64) return false;
935
+ if (s.charCodeAt(at - 1) !== 34) return false;
893
936
  } else {
894
- if (local.charCodeAt(0) === 46 || local.charCodeAt(local.length - 1) === 46) return false;
895
- if (local.indexOf('..') !== -1) return false;
896
- for (let i = 0; i < local.length; i++) if (!isEmailLocalChar(local.charCodeAt(i))) return false;
937
+ if (s.charCodeAt(0) === 46) return false; // a leading dot is not a mailbox
938
+ at = -1;
939
+ let afterDot = false;
940
+ for (let i = 0; i < n; i++) {
941
+ const c = s.charCodeAt(i);
942
+ if (c === 64) { at = i; break; }
943
+ if (c > 127 || EMAIL_LOCAL[c] === 0) return false;
944
+ if (c === 46) {
945
+ if (afterDot) return false; // a doubled dot is not one either
946
+ afterDot = true;
947
+ } else {
948
+ afterDot = false;
949
+ }
950
+ }
951
+ if (at <= 0 || at === n - 1 || at > 64) return false;
952
+ if (s.charCodeAt(at - 1) === 46) return false;
897
953
  }
898
- if (domain.charCodeAt(0) === 91) { // '['
899
- if (domain.charCodeAt(domain.length - 1) !== 93 || domain.length <= 2) return false;
900
- const inner = domain.slice(1, -1);
954
+ if (s.charCodeAt(at + 1) === 91) { // '['
955
+ if (s.charCodeAt(n - 1) !== 93 || n - at - 1 <= 2) return false;
901
956
  // IPv6 literals are tagged; anything else in brackets is an IPv4 address.
902
- if (inner.startsWith('IPv6:')) return ipv6(inner.slice(5));
903
- return ipv4(inner);
957
+ if (s.startsWith('IPv6:', at + 2)) return ipv6(s.slice(at + 7, n - 1));
958
+ return ipv4(s.slice(at + 2, n - 1));
904
959
  }
905
- return hostname(domain);
960
+ return hostname(s, at + 1, n);
906
961
  }
907
962
  // Emitted rather than bound as a closure: a standalone module embeds format
908
963
  // functions by their source text, so anything reaching a free variable would
@@ -910,23 +965,36 @@ function email(s) {
910
965
  // one hostname implementation for both halves of this file.
911
966
  function emailSource (v, isStr) {
912
967
  const inner =
913
- `const _at=${v}.lastIndexOf('@');` +
914
- `if(_at<=0||_at===${v}.length-1)return false;` +
915
- `const _lp=${v}.slice(0,_at);const _dm=${v}.slice(_at+1);` +
916
- `if(_lp.length>64)return false;` +
917
- `if(_lp.charCodeAt(0)===34){if(_lp.length<2||_lp.charCodeAt(_lp.length-1)!==34)return false}` +
918
- `else{` +
919
- `if(_lp.charCodeAt(0)===46||_lp.charCodeAt(_lp.length-1)===46)return false;` +
920
- `if(_lp.indexOf('..')!==-1)return false;` +
921
- `for(let _i=0;_i<_lp.length;_i++){const _lc=_lp.charCodeAt(_i);if(!${EMAIL_LOCAL_SRC('_lc')})return false}` +
968
+ // One forward pass, and neither half sliced out. '@' is not a local-part
969
+ // character, so in an unquoted address the first one is the separator and the
970
+ // local part is checked on the way to it. A quoted local part may contain
971
+ // '@' and keeps the backward scan, which is the rare shape.
972
+ // The bounds tests are merged into one condition per branch, and there is no
973
+ // separate length check: an empty local part fails `_at<=0` and an empty
974
+ // domain fails `_at===_n0-1`, so a short string is already refused. This
975
+ // lands in every emitted module that validates an email, and 25 bytes of it
976
+ // put the CLI smoke build over its --max-size once.
977
+ `const _n0=${v}.length;let _at;` +
978
+ `if(${v}.charCodeAt(0)===34){` +
979
+ `_at=${v}.lastIndexOf('@');` +
980
+ `if(_at<2||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)!==34)return false` +
981
+ `}else{` +
982
+ `if(${v}.charCodeAt(0)===46)return false;` +
983
+ `_at=-1;let _pd=false;` +
984
+ `for(let _i=0;_i<_n0;_i++){const _lc=${v}.charCodeAt(_i);if(_lc===64){_at=_i;break}if(!${EMAIL_LOCAL_SRC('_lc')})return false;if(_lc===46){if(_pd)return false;_pd=true}else _pd=false}` +
985
+ `if(_at<=0||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)===46)return false` +
922
986
  `}` +
923
- `if(_dm.charCodeAt(0)===91){` +
987
+ // An address literal is rare enough to pay for its own slice inside the
988
+ // branch that needs it; the hostname path, which is every real address,
989
+ // reads the domain in place.
990
+ `if(${v}.charCodeAt(_at+1)===91){` +
991
+ `const _dm=${v}.slice(_at+1);` +
924
992
  `if(_dm.charCodeAt(_dm.length-1)!==93||_dm.length<=2)return false;` +
925
993
  `const _in=_dm.slice(1,-1);` +
926
994
  `if(_in.startsWith('IPv6:')){const _i6=_in.slice(5);${ipv6Source('_i6', true)}}` +
927
995
  `else{${ipv4Source('_in', true)}}` +
928
996
  `}` +
929
- `else{${hostnameSource('_dm', true)}}`;
997
+ `else{${hostnameRangeSource(v, '_at+1', `${v}.length`)}}`;
930
998
  return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`;
931
999
  }
932
1000
 
@@ -3889,7 +3889,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3889
3889
  patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
3890
3890
  }
3891
3891
  const isAdditional = patChecks.length ? `!_a${ci}.has(_k${ci}[_i])&&!(${patChecks.join('||')})` : `!_a${ci}.has(_k${ci}[_i])`
3892
- const inner = `const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}'${ordinalField(ctx, `${apSp}`)},params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
3892
+ // Same shape as the combined generator: a for-in says whether anything is
3893
+ // extra without allocating, and the keys are materialised only to name one.
3894
+ // This function walks every node of a document that failed anywhere, so on a
3895
+ // large array the clean items were each paying for a key array.
3896
+ const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
3897
+ const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
3898
+ const detail = `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}'${ordinalField(ctx, `${apSp}`)},params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
3899
+ const inner = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
3893
3900
  lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
3894
3901
  }
3895
3902
 
@@ -4554,18 +4561,28 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4554
4561
  if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
4555
4562
  const propKeys = Object.keys(schema.properties)
4556
4563
  const ci = ctx.varCounter++
4564
+ // The error has to name the offending property, so the keys have to be
4565
+ // materialised to report one. They do not have to be materialised to find
4566
+ // out whether there is one: a for-in answers that with no allocation, and
4567
+ // an accepted document is the common case. The verdict generator pays a
4568
+ // bare key count here, and this kept the combined function 2.8x that on a
4569
+ // valid document, which is what stopped it from replacing the
4570
+ // verdict-then-revalidate pair on the error path.
4571
+ const detail = (test) =>
4572
+ `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${test}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}`
4573
+ let body
4557
4574
  if (propKeys.length <= 8) {
4558
4575
  // Direct chain: no Set allocation for small schemas
4576
+ const gate = propKeys.map(k => `_f${ci}!==${JSON.stringify(k)}`).join('&&')
4559
4577
  const checks = propKeys.map(k => `_k${ci}[_i]!==${JSON.stringify(k)}`).join('&&')
4560
- lines.push(isObj
4561
- ? `{const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
4562
- : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${checks}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
4578
+ body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gate}){_x${ci}=1;break}}if(_x${ci}){${detail(checks)}}`
4563
4579
  } else {
4564
4580
  const allowed = propKeys.map(k => JSON.stringify(k)).join(',')
4565
- lines.push(isObj
4566
- ? `{const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`
4567
- : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){const _k${ci}=Object.keys(${v});const _a${ci}=new Set([${allowed}]);for(let _i=0;_i<_k${ci}.length;_i++)if(!_a${ci}.has(_k${ci}[_i])){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}}`)
4581
+ body = `const _a${ci}=new Set([${allowed}]);let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
4568
4582
  }
4583
+ lines.push(isObj
4584
+ ? `{${body}}`
4585
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body}}`)
4569
4586
  }
4570
4587
 
4571
4588
  // unevaluatedProperties: false where it is provably additionalProperties
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.29.0';
10
+ module.exports = '1.30.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.29.0",
3
+ "version": "1.30.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_engine_option.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_json_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_defaults_own_keys.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_position_map_cost.js && node tests/test_aot_staleness.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",
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_validate_json_scanner_cost.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_engine_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.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_additional_props_combined_cost.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_single_pass_errors.js && node tests/test_lazy_json_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_defaults_own_keys.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_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.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",
@@ -121,13 +121,13 @@
121
121
  "LICENSE"
122
122
  ],
123
123
  "optionalDependencies": {
124
- "@ata-validator/native-darwin-arm64": "1.29.0",
125
- "@ata-validator/native-darwin-x64": "1.29.0",
126
- "@ata-validator/native-linux-arm64-gnu": "1.29.0",
127
- "@ata-validator/native-linux-arm64-musl": "1.29.0",
128
- "@ata-validator/native-linux-x64-gnu": "1.29.0",
129
- "@ata-validator/native-linux-x64-musl": "1.29.0",
130
- "@ata-validator/native-win32-x64": "1.29.0"
124
+ "@ata-validator/native-darwin-arm64": "1.30.0",
125
+ "@ata-validator/native-darwin-x64": "1.30.0",
126
+ "@ata-validator/native-linux-arm64-gnu": "1.30.0",
127
+ "@ata-validator/native-linux-arm64-musl": "1.30.0",
128
+ "@ata-validator/native-linux-x64-gnu": "1.30.0",
129
+ "@ata-validator/native-linux-x64-musl": "1.30.0",
130
+ "@ata-validator/native-win32-x64": "1.30.0"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "yaml": "^2.0.0"