ata-validator 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
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.8.2 - 2026-08-28
6
+
7
+ ### Fixed
8
+
9
+ - A `pattern` of the form `^[class]+$`, `^[class]{m,n}$` or `^[class]{n}$` with n above 16 could be violated without `validate()` saying so. The boolean program compiles those patterns to an inline character loop wrapped in an arrow function, and the rewrite that turns the boolean program into the error-reporting one replaced the `return false` and `return true` inside that arrow with `return E(d)` and `return R`. Both are objects, so the arrow started returning a truthy value on mismatch, the enclosing `!(cond && obj)` went false, and the error program accepted what the boolean program had rejected.
10
+
11
+ What that looked like from outside depended on the path. Without preprocessing, `validate()` still rejected, because the boolean verdict runs first, but the error it produced was a generic `schema validation failed` with no keyword, no `params.pattern` and no path. With preprocessing, which means any schema carrying a `default` or a validator built with `coerceTypes` or `removeAdditional`, the error program is installed as `validate()` directly and the result was `valid: true`. `Validator.bundleStandalone` and `bundleCompact` embed the same program and had the same silent accept. `ata compile` output is built from the boolean program and was not affected. The rewrite has been wrong since 0.6.0; the official suite never exercised these pattern shapes, so nothing caught it.
12
+
13
+ The rewrite now leaves the body of an arrow function alone, the same way it already left `function` bodies alone. `tests/test_hybrid_agreement.js` compares the rewritten program with the boolean it came from on every affected shape, at the root, in a property, in array items, through `bundleStandalone` and through a compiled module, and checks the two preprocessing cases directly. `tests/test_codegen_entrypoint_agreement.js` now compares the rewritten program as a fifth participant across the whole official suite. `tests/test_ajv_errors.js`, which had been failing on exactly this for as long as the bug existed, is now part of `npm test`.
14
+
15
+ ### Changed
16
+
17
+ - Ranking an error into schema declaration order no longer re-derives the rank on every error of every failing document. It was splitting the schema path with two regular expressions per segment and then scanning `Object.keys(node)` at each level, which was 8.5% of the error path in a profile, for an answer that depends only on the schema and the path. The rank is now cached per path under its root, each node's keys are indexed once, and escape handling is skipped for segments with no tilde. That function drops from 8.5% to 1.4% of the error path, and the error path as a whole gets 3.5% faster, median of eleven interleaved runs. `docs/performance-notes.md` records the larger gap this was measured against; it is not closed by this change.
18
+
5
19
  ## 1.8.1 - 2026-08-27
6
20
 
7
21
  ### Changed
package/index.js CHANGED
@@ -373,12 +373,64 @@ function resolveSchemaByPath(rootSchema, schemaPath) {
373
373
  // order AJV emits and what schema authors read top to bottom. Segments that
374
374
  // cannot be resolved (cross-schema refs, normalized keys) end the walk; the
375
375
  // stable sort then keeps such errors in engine emission order.
376
+ // The rank of a `schemaPath` under a given root is fixed: the schema does not
377
+ // change between validations, so neither does the answer. It was recomputed for
378
+ // every error of every failing document, and computing it is not cheap. Two
379
+ // caches, both keyed on things that do not change:
380
+ //
381
+ // rootSchema -> schemaPath -> rank, so a path is walked once ever
382
+ // node -> key -> its index, so the walk stops calling Object.keys and
383
+ // scanning the result for a string
384
+ //
385
+ // A failing route sees the same handful of schemaPaths over and over, which is
386
+ // what makes the first one worth having.
387
+ const _rankCache = new WeakMap();
388
+ const _keyIndexCache = new WeakMap();
389
+
390
+ function keyIndex(node, seg) {
391
+ let index = _keyIndexCache.get(node);
392
+ if (index === undefined) {
393
+ index = new Map();
394
+ const keys = Object.keys(node);
395
+ for (let i = 0; i < keys.length; i++) index.set(keys[i], i);
396
+ _keyIndexCache.set(node, index);
397
+ }
398
+ const at = index.get(seg);
399
+ return at === undefined ? -1 : at;
400
+ }
401
+
402
+ // `~1` and `~0` are the only escapes a JSON pointer has, and almost no schema
403
+ // key contains a tilde. Looking for one is far cheaper than two regex passes
404
+ // over every segment of every path.
405
+ function unescapePointerSegment(seg) {
406
+ return seg.indexOf('~') < 0 ? seg : seg.replace(/~1/g, '/').replace(/~0/g, '~');
407
+ }
408
+
376
409
  function schemaOrderRank(rootSchema, schemaPath) {
377
410
  if (!schemaPath || typeof schemaPath !== 'string' || !schemaPath.startsWith('#')) return null;
378
- const parts = schemaPath.slice(1).split('/').filter(Boolean).map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
411
+ if (rootSchema === null || typeof rootSchema !== 'object') return null;
412
+
413
+ let byPath = _rankCache.get(rootSchema);
414
+ if (byPath === undefined) { byPath = new Map(); _rankCache.set(rootSchema, byPath); }
415
+ const hit = byPath.get(schemaPath);
416
+ if (hit !== undefined) return hit;
417
+
418
+ const rank = _computeRank(rootSchema, schemaPath);
419
+ byPath.set(schemaPath, rank);
420
+ return rank;
421
+ }
422
+
423
+ function _computeRank(rootSchema, schemaPath) {
379
424
  const rank = [];
380
425
  let node = rootSchema;
381
- for (const seg of parts) {
426
+ let start = 1;
427
+ while (start <= schemaPath.length) {
428
+ let end = schemaPath.indexOf('/', start);
429
+ if (end < 0) end = schemaPath.length;
430
+ if (end === start) { start = end + 1; continue; } // what filter(Boolean) dropped
431
+ const seg = unescapePointerSegment(schemaPath.slice(start, end));
432
+ start = end + 1;
433
+
382
434
  if (node == null || typeof node !== 'object') break;
383
435
  if (Array.isArray(node)) {
384
436
  const idx = Number(seg);
@@ -386,7 +438,7 @@ function schemaOrderRank(rootSchema, schemaPath) {
386
438
  rank.push(idx);
387
439
  node = node[idx];
388
440
  } else {
389
- const idx = Object.keys(node).indexOf(seg);
441
+ const idx = keyIndex(node, seg);
390
442
  if (idx < 0) break;
391
443
  rank.push(idx);
392
444
  node = node[seg];
@@ -1492,9 +1492,17 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1492
1492
  function replaceTopLevel(code) {
1493
1493
  let fnDepth = 0, result = '', i = 0
1494
1494
  while (i < code.length) {
1495
- if (code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))) {
1496
- // Found a nested function — skip to opening brace, track all braces inside
1497
- let j = i + 8
1495
+ // A nested closure keeps its own returns. Both spellings count: a
1496
+ // `function` keyword and an arrow with a block body. The inline pattern
1497
+ // compiler emits `(()=>{...return false...return true})()` and rewriting
1498
+ // those returns turned the arrow's boolean into an object, which made
1499
+ // `!(cond && obj)` false and let the hybrid accept what the boolean
1500
+ // program rejected.
1501
+ const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))
1502
+ const isArrowBlock = code.startsWith('=>{', i)
1503
+ if (isFunctionKw || isArrowBlock) {
1504
+ // Skip to the opening brace, then track all braces inside the body.
1505
+ let j = isArrowBlock ? i + 2 : i + 8
1498
1506
  while (j < code.length && code[j] !== '{') j++
1499
1507
  result += code.slice(i, j + 1)
1500
1508
  i = j + 1
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.8.1';
10
+ module.exports = '1.8.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -44,7 +44,7 @@
44
44
  "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",
45
45
  "build": "cmake-js build --target ata",
46
46
  "rebuild": "cmake-js rebuild --target ata",
47
- "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_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_diagnostic.js && node tests/test_format_engine_parity.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_pattern_properties_errors.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_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.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_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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",
47
+ "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_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_diagnostic.js && node tests/test_format_engine_parity.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_ajv_errors.js && node tests/test_pattern_properties_errors.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_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.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_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.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",
48
48
  "bench:size": "node benchmark/bench_aot_size.mjs",
49
49
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
50
50
  "test:compat": "node tests/test_compat.js",
@@ -112,13 +112,13 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.8.1",
116
- "@ata-validator/native-darwin-x64": "1.8.1",
117
- "@ata-validator/native-linux-x64-gnu": "1.8.1",
118
- "@ata-validator/native-linux-arm64-gnu": "1.8.1",
119
- "@ata-validator/native-linux-x64-musl": "1.8.1",
120
- "@ata-validator/native-linux-arm64-musl": "1.8.1",
121
- "@ata-validator/native-win32-x64": "1.8.1"
115
+ "@ata-validator/native-darwin-arm64": "1.8.2",
116
+ "@ata-validator/native-darwin-x64": "1.8.2",
117
+ "@ata-validator/native-linux-x64-gnu": "1.8.2",
118
+ "@ata-validator/native-linux-arm64-gnu": "1.8.2",
119
+ "@ata-validator/native-linux-x64-musl": "1.8.2",
120
+ "@ata-validator/native-linux-arm64-musl": "1.8.2",
121
+ "@ata-validator/native-win32-x64": "1.8.2"
122
122
  },
123
123
  "peerDependencies": {
124
124
  "yaml": "^2.0.0"