ata-validator 1.2.2 → 1.4.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,25 @@
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.4.0 - 2026-08-04
6
+
7
+ ### Fixed
8
+
9
+ - Where `new Function` is unavailable, validation now runs on the interpreted engine instead of degrading quietly. Cloudflare Workers, Deno Deploy and pages under a strict Content-Security-Policy refuse code generation; the closure-based path does not call `new Function` itself, so it survived the refusal and went on to handle schemas it gets wrong. Against the full suite with code generation blocked, ata scored 1188 of 1290 with 30 schemas failing to build. It now scores 1286 of 1290 with none, which is the same as the compiled path within one case. Draft 7 is 910 of 922. `tests/test_no_eval.js` runs the whole suite with `eval` and `new Function` blocked so this is checked rather than assumed.
10
+
11
+ ## 1.3.0 - 2026-08-02
12
+
13
+ ### Added
14
+
15
+ - `useDefaults` and `assertFormat` validator options, both defaulting to `true` so existing behavior is unchanged. `useDefaults: false` stops missing properties being filled in from their `default` before validation, which matters because a `default` that does not satisfy its own subschema currently makes an otherwise valid instance fail. `assertFormat: false` treats `format` as an annotation rather than an assertion, the Draft 2020-12 reading when the format-assertion vocabulary is not in use. With both off, ata passes every case in the suite's `format` and `default` files.
16
+
17
+ ### Fixed
18
+
19
+ - A `$ref` that could not be resolved validated everything instead of failing. The JS compiler emitted no check at all for an unresolved reference, so a typo in a `$ref`, or a reference into a schema that was never registered, silently turned off every constraint behind it rather than reporting an error. Both compiler paths now decline to compile such a schema and it validates on the interpreted engine, which reports `ATA5001` naming the reference it could not resolve.
20
+ - References that resolve relative to an enclosing base URI are no longer compiled by the JS paths, which match registry entries by exact key or path suffix and have no notion of a base. Nested `$id` scopes, relative references, and documents registered under a URI different from the `$id` they declare now route to the interpreted engine, which tracks the base properly. Schemas that reference a flat registry of ids, the common `$ref: 'shared#'` shape, still take the compiled fast path.
21
+ - A schema supplied through the `schemas` option as a URI-keyed record was registered only under the `$id` it declared, so references to the URI it was registered under could not resolve. It is now addressable by both.
22
+ - Official draft 2020-12 remote-reference suite: 30 of 31 cases, up from 21. Pure-JS configuration on the full suite: 1189 of 1190, up from 1188. The native configuration stays at 1190 of 1190.
23
+
5
24
  ## 1.2.2 - 2026-07-26
6
25
 
7
26
  ### Fixed
package/README.md CHANGED
@@ -20,7 +20,7 @@ The `ata-validator` package itself is pure JavaScript. The native accelerator (s
20
20
  npm install ata-validator --omit=optional
21
21
  ```
22
22
 
23
- or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup passes 99.8% of the official draft 2020-12 test suite. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
23
+ or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1285 of 1290 Draft 2020-12 cases; the two miss a different `$dynamicRef` scope corner each. Only the buffer and parallel APIs (`isValid` on raw buffers, `countValid`, `batchIsValid`, `validateAndParse`) need the native engine and say so with a clear error.
24
24
 
25
25
  In your code:
26
26
 
@@ -466,14 +466,17 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
466
466
 
467
467
  ### Known limitations
468
468
 
469
- The Draft 2020-12 pass rate (1188 of 1190 applicable cases, 99.8%, in the pure-JS configuration; 1190 of 1190 with the native accelerator installed, which covers the two remaining `$dynamicRef` scope corners) excludes areas that are deliberate scope decisions for 1.x:
469
+ Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1285 of 1290 cases, 99.6%. Draft 7 gives 911 of 922, 98.8%. `npm run test:suite` reproduces both and lists the remaining failures by name.
470
+
471
+ Areas that remain deliberate scope decisions for 1.x:
470
472
 
471
473
  - **Remote `$ref` over the network** is not fetched. Register cross-schema refs explicitly with `schemas: [...]` or `addSchema()`.
472
474
  - **`$vocabulary`** is not implemented; custom vocabularies are ignored rather than enforced.
473
475
  - **`contentEncoding` / `contentMediaType` / `contentSchema`** are annotation-only, as the spec permits, and are not validated.
474
476
  - **`minLength`/`maxLength`** count UTF-16 code units, not grapheme clusters.
475
- - **Infinite-loop detection** suite cases are skipped; circular `$ref` chains are cut off by a recursion depth guard instead.
476
- - **`default`** optional suite tests are excluded; ata applies `default` values to validated data, where the spec treats `default` as a non-enforcing annotation.
477
+ - **Infinite-loop detection** relies on a recursion depth guard that cuts off circular `$ref` chains.
478
+ - **`default`** values are applied to validated data by default, where the spec treats `default` as a non-enforcing annotation. Pass `useDefaults: false` for the specification behavior.
479
+ - **`format`** is asserted by default rather than treated as an annotation. Pass `assertFormat: false` for the specification behavior.
477
480
 
478
481
  If one of these blocks you, open an issue; scope decisions get revisited with real use cases.
479
482
 
package/index.d.ts CHANGED
@@ -317,6 +317,19 @@ export type ValidateAndParseResult<T = unknown> =
317
317
  export interface ValidatorOptions {
318
318
  coerceTypes?: boolean;
319
319
  removeAdditional?: boolean;
320
+ /**
321
+ * When true (default), missing properties are filled in from their `default`
322
+ * before validation. Set to false to leave the instance untouched, which is
323
+ * what the specification describes: `default` is an annotation and never
324
+ * affects whether an instance is valid.
325
+ */
326
+ useDefaults?: boolean;
327
+ /**
328
+ * When true (default), `format` is asserted and a value that does not match
329
+ * its format fails. Set to false to treat `format` as an annotation, the
330
+ * Draft 2020-12 behavior when the format-assertion vocabulary is not in use.
331
+ */
332
+ assertFormat?: boolean;
320
333
  schemas?: Record<string, object> | object[];
321
334
  /**
322
335
  * Custom format checkers. Keys are format names referenced from `format` in
package/index.js CHANGED
@@ -9,7 +9,7 @@ const {
9
9
  compileToJSCodegenWithErrors,
10
10
  compileToJSCombined,
11
11
  } = require("./lib/js-compiler");
12
- const { normalizeDraft7, normalizeNullable } = require("./lib/draft7");
12
+ const { normalizeDraft7, normalizeNullable, stripFormatAssertions } = require("./lib/draft7");
13
13
  const { classify } = require("./lib/shape-classifier");
14
14
  const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
15
15
 
@@ -250,11 +250,13 @@ function buildPreprocessCodegen(schema, options) {
250
250
  }
251
251
 
252
252
  // defaults: inline per property
253
- for (const [key, prop] of Object.entries(props)) {
254
- if (prop && typeof prop === 'object' && prop.default !== undefined) {
255
- const k = JSON.stringify(key);
256
- const def = JSON.stringify(prop.default);
257
- lines.push(`if(!(${k} in d))d[${k}]=${def}`);
253
+ if (options.useDefaults !== false) {
254
+ for (const [key, prop] of Object.entries(props)) {
255
+ if (prop && typeof prop === 'object' && prop.default !== undefined) {
256
+ const k = JSON.stringify(key);
257
+ const def = JSON.stringify(prop.default);
258
+ lines.push(`if(!(${k} in d))d[${k}]=${def}`);
259
+ }
258
260
  }
259
261
  }
260
262
 
@@ -269,6 +271,21 @@ function buildPreprocessCodegen(schema, options) {
269
271
  }
270
272
  }
271
273
 
274
+ // Cloudflare Workers, Deno Deploy and pages under a strict Content-Security-
275
+ // Policy refuse `new Function`. Probed once, lazily, because the answer cannot
276
+ // change within a realm and the probe itself is a code generation attempt.
277
+ let _codegenAvailable = null;
278
+ function codegenAvailable() {
279
+ if (_codegenAvailable === null) {
280
+ try {
281
+ _codegenAvailable = new Function('return 1')() === 1;
282
+ } catch {
283
+ _codegenAvailable = false;
284
+ }
285
+ }
286
+ return _codegenAvailable;
287
+ }
288
+
272
289
  // Schema compilation cache: same schema string -> reuse compiled functions
273
290
  const _compileCache = new Map();
274
291
 
@@ -435,7 +452,11 @@ function buildSchemaMap(schemas) {
435
452
  } else {
436
453
  for (const [key, s] of Object.entries(schemas)) {
437
454
  const normalized = _normalizeCallerSchema(s)
438
- map.set(normalized.$id || key, normalized)
455
+ // A retrieved document is addressable both by the URI it was registered
456
+ // under and by the $id it declares. Registering only the $id makes
457
+ // references to the retrieval URI unresolvable.
458
+ map.set(key, normalized)
459
+ if (normalized.$id && normalized.$id !== key) map.set(normalized.$id, normalized)
439
460
  }
440
461
  }
441
462
  return map
@@ -537,10 +558,18 @@ class Validator {
537
558
  // When schema is a string, JSON.parse already produces a fresh object.
538
559
  // When schema is an object, normalization runs on a clone so the caller's
539
560
  // object is never touched.
540
- const schemaObj = typeof schema === "string"
561
+ let schemaObj = typeof schema === "string"
541
562
  ? JSON.parse(schema)
542
563
  : _normalizeCallerSchema(schema);
543
564
 
565
+ // assertFormat: false makes `format` annotation-only. Strip it on a clone
566
+ // so the caller's schema keeps the keyword.
567
+ if (options.assertFormat === false) {
568
+ schemaObj = stripFormatAssertions(
569
+ schemaObj === schema ? _deepCloneWithSymbols(schemaObj) : schemaObj,
570
+ );
571
+ }
572
+
544
573
  this._schemaStr = null; // lazy: computed on first use
545
574
  this._schemaObj = schemaObj;
546
575
  this._options = options;
@@ -695,7 +724,13 @@ class Validator {
695
724
  const cached = this._userFormats ? null : _compileCache.get(mapKey);
696
725
  let jsFn, jsCombinedFn, jsErrFn, _isCodegen = false;
697
726
  var _forceNapi = typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI;
698
- if (cached && !_forceNapi) {
727
+ // Where source cannot be turned into a function, neither JS path is usable.
728
+ // The closure path does not call `new Function` itself, so it survives the
729
+ // block and would quietly handle schemas it gets wrong; the interpreted
730
+ // engine is both eval-free and more correct, so go straight there.
731
+ if (!codegenAvailable()) {
732
+ jsFn = null; jsCombinedFn = null; jsErrFn = null;
733
+ } else if (cached && !_forceNapi) {
699
734
  jsFn = cached.jsFn;
700
735
  jsCombinedFn = cached.combined;
701
736
  jsErrFn = cached.errFn;
@@ -722,7 +757,7 @@ class Validator {
722
757
  const preprocessSchema = resolveSchemaForPreprocess(schemaObj, this._schemaMap);
723
758
  let preprocess = buildPreprocessCodegen(preprocessSchema, options);
724
759
  if (!preprocess) {
725
- const applyDefaults = buildDefaultsApplier(preprocessSchema);
760
+ const applyDefaults = options.useDefaults === false ? null : buildDefaultsApplier(preprocessSchema);
726
761
  const applyCoerce = options.coerceTypes ? buildCoercer(preprocessSchema) : null;
727
762
  const applyRemove = options.removeAdditional
728
763
  ? buildRemover(preprocessSchema)
package/lib/draft7.js CHANGED
@@ -126,4 +126,46 @@ function _normalizeNullable(schema) {
126
126
  }
127
127
  }
128
128
 
129
- module.exports = { isDraft7, normalizeDraft7, normalizeNullable }
129
+ // Draft 2020-12 treats `format` as an annotation unless the assertion
130
+ // vocabulary is in use. Removing the keyword is exactly that reading, and it
131
+ // keeps every engine in agreement without each one growing its own switch.
132
+ // Walks schema-bearing keywords only, so a property or definition named
133
+ // "format" is untouched.
134
+ function stripFormatAssertions(schema) {
135
+ if (typeof schema !== 'object' || schema === null) return schema
136
+ _stripFormat(schema, new Set())
137
+ return schema
138
+ }
139
+
140
+ function _stripFormat(schema, seen) {
141
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return
142
+ if (seen.has(schema)) return
143
+ seen.add(schema)
144
+
145
+ if (typeof schema.format === 'string') delete schema.format
146
+
147
+ const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
148
+ for (const key of objSubs) {
149
+ if (schema[key] && typeof schema[key] === 'object' && !Array.isArray(schema[key])) {
150
+ for (const v of Object.values(schema[key])) _stripFormat(v, seen)
151
+ }
152
+ }
153
+ const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
154
+ for (const key of arrSubs) {
155
+ if (Array.isArray(schema[key])) {
156
+ for (const s of schema[key]) _stripFormat(s, seen)
157
+ }
158
+ }
159
+ const singleSubs = ['items', 'additionalItems', 'contains', 'not', 'if', 'then', 'else',
160
+ 'additionalProperties', 'propertyNames', 'unevaluatedItems',
161
+ 'unevaluatedProperties', 'contentSchema']
162
+ for (const key of singleSubs) {
163
+ if (Array.isArray(schema[key])) {
164
+ for (const s of schema[key]) _stripFormat(s, seen)
165
+ } else {
166
+ _stripFormat(schema[key], seen)
167
+ }
168
+ }
169
+ }
170
+
171
+ module.exports = { isDraft7, normalizeDraft7, normalizeNullable, stripFormatAssertions }
@@ -107,6 +107,8 @@ function compileToJS(schema, defs, schemaMap) {
107
107
  if (!hasDynamic && hasNestedIdScope(schema)) return null
108
108
  }
109
109
 
110
+ if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
111
+
110
112
  // Collect $defs early so sub-schemas can resolve $ref
111
113
  const rootDefs = defs || collectDefs(schema)
112
114
 
@@ -611,27 +613,23 @@ function resolveRef(ref, defs, schemaMap) {
611
613
  if (entry) return (d) => { const fn = entry.fn; return fn ? fn(d) : true }
612
614
  }
613
615
  }
616
+ // A referenced schema that cannot itself be compiled must propagate the bail.
617
+ // Substituting an always-true function here would make the reference vacuous
618
+ // and silently drop every constraint behind it.
614
619
  // 2. Cross-schema ref (exact match)
615
620
  if (schemaMap && schemaMap.has(ref)) {
616
- const resolved = schemaMap.get(ref)
617
- const fn = compileToJS(resolved, null, schemaMap)
618
- return fn || (() => true)
621
+ return compileToJS(schemaMap.get(ref), null, schemaMap)
619
622
  }
620
623
  // 3. Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
621
624
  if (schemaMap && ref.includes('#')) {
622
625
  const r = resolveCrossSchemaRef(ref, schemaMap)
623
- if (r) {
624
- const fn = compileToJS(r.schema, null, schemaMap)
625
- return fn || (() => true)
626
- }
626
+ if (r) return compileToJS(r.schema, null, schemaMap)
627
627
  }
628
628
  // 4. Cross-schema ref (relative URI resolution, no fragment)
629
629
  if (schemaMap && !ref.includes('://') && !ref.startsWith('#')) {
630
630
  for (const [id] of schemaMap) {
631
631
  if (id.endsWith('/' + ref)) {
632
- const resolved = schemaMap.get(id)
633
- const fn = compileToJS(resolved, null, schemaMap)
634
- return fn || (() => true)
632
+ return compileToJS(schemaMap.get(id), null, schemaMap)
635
633
  }
636
634
  }
637
635
  }
@@ -933,6 +931,169 @@ function hasAdditionalPropertiesSchema(schema) {
933
931
  return false
934
932
  }
935
933
 
934
+ // Schema-bearing keywords, so the walk below never mistakes a property named
935
+ // "$ref" for an actual reference.
936
+ const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
937
+ const SUBSCHEMA_LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
938
+ const SUBSCHEMA_SINGLES = ['items', 'additionalItems', 'contains', 'not', 'if', 'then', 'else',
939
+ 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema']
940
+
941
+ // genCode emits nothing for a $ref it cannot resolve, which makes the reference
942
+ // silently vacuous — a typo in a $ref would turn validation off rather than
943
+ // fail. Codegen bails instead, so the schema routes to the interpreted engine,
944
+ // which reports the unresolved reference as an error. Mirrors genCode's own
945
+ // resolution rules; anything genCode cannot resolve is treated as unresolvable.
946
+ function refResolves(ref, rootDefs, anchors, schemaMap) {
947
+ if (ref === '#') return true
948
+ const local = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
949
+ if (local) return !!(rootDefs && rootDefs[local[1]])
950
+ if (ref.startsWith('#') && !ref.startsWith('#/')) {
951
+ const entry = rootDefs && rootDefs[ref]
952
+ return !!((entry && entry.raw) || (anchors && anchors[ref]))
953
+ }
954
+ if (ref.startsWith('#')) return false
955
+ if (!schemaMap) return false
956
+ if (schemaMap.has(ref)) return true
957
+ if (!ref.includes('://')) {
958
+ for (const [id] of schemaMap) if (id.endsWith('/' + ref)) return true
959
+ }
960
+ if (ref.includes('#')) return !!resolveCrossSchemaRef(ref, schemaMap)
961
+ return false
962
+ }
963
+
964
+ // Resolve a $ref that crosses into another document, mirroring genCode's rules.
965
+ function crossDocTarget(ref, schemaMap) {
966
+ if (!schemaMap || ref.startsWith('#')) return null
967
+ if (schemaMap.has(ref)) return schemaMap.get(ref)
968
+ if (!ref.includes('://')) {
969
+ for (const [id, s] of schemaMap) if (id.endsWith('/' + ref)) return s
970
+ }
971
+ if (ref.includes('#')) {
972
+ const r = resolveCrossSchemaRef(ref, schemaMap)
973
+ if (r) return r.schema
974
+ }
975
+ return null
976
+ }
977
+
978
+ // True when a subtree contains a fragment-only reference ("#", "#/$defs/x",
979
+ // "#anchor"). Such a reference is relative to the document that holds it.
980
+ function subtreeHasLocalRef(node, seen) {
981
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) return false
982
+ if (seen.has(node)) return false
983
+ seen.add(node)
984
+ if (typeof node.$ref === 'string' && node.$ref.startsWith('#')) return true
985
+ for (const key of SUBSCHEMA_MAPS) {
986
+ const group = node[key]
987
+ if (group && typeof group === 'object' && !Array.isArray(group)) {
988
+ for (const sub of Object.values(group)) if (subtreeHasLocalRef(sub, seen)) return true
989
+ }
990
+ }
991
+ for (const key of SUBSCHEMA_LISTS) {
992
+ if (Array.isArray(node[key])) {
993
+ for (const sub of node[key]) if (subtreeHasLocalRef(sub, seen)) return true
994
+ }
995
+ }
996
+ for (const key of SUBSCHEMA_SINGLES) {
997
+ if (Array.isArray(node[key])) {
998
+ for (const sub of node[key]) if (subtreeHasLocalRef(sub, seen)) return true
999
+ } else if (subtreeHasLocalRef(node[key], seen)) return true
1000
+ }
1001
+ return false
1002
+ }
1003
+
1004
+ // Neither JS path tracks the base URI a reference is resolved against: they
1005
+ // match registry keys by exact string or by path suffix. That is fine for a
1006
+ // flat registry of absolute ids, but wrong as soon as resolution depends on an
1007
+ // enclosing scope. These shapes go to the interpreted engine, which resolves
1008
+ // against a real base.
1009
+ function needsBaseTracking(schema, schemaMap, seen) {
1010
+ if (!schemaMap || schemaMap.size === 0) return false
1011
+ // A nested $id opens a new scope that later relative references resolve
1012
+ // against.
1013
+ if (hasNestedIdScope(schema)) return true
1014
+ return refsAreScopeSensitive(schema, schemaMap, seen)
1015
+ }
1016
+
1017
+ function refsAreScopeSensitive(node, schemaMap, seen) {
1018
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) return false
1019
+ if (seen.has(node)) return false
1020
+ seen.add(node)
1021
+
1022
+ const ref = node.$ref
1023
+ if (typeof ref === 'string' && !ref.startsWith('#')) {
1024
+ const key = ref.split('#')[0]
1025
+ // A key the registry holds verbatim needs no base to resolve — this is the
1026
+ // flat-registry case ($ref: 'shared#'). Anything else that is not absolute
1027
+ // only resolves by walking up from an enclosing base.
1028
+ if (!schemaMap.has(key) && !ref.includes('://')) return true
1029
+ const target = crossDocTarget(ref, schemaMap)
1030
+ if (target && typeof target === 'object') {
1031
+ // Registered under a key that differs from the $id it declares, so its
1032
+ // own references resolve against a different base than its retrieval URI.
1033
+ if (typeof target.$id === 'string' && target.$id !== key) return true
1034
+ if (subtreeHasLocalRef(target, new Set()) && refsAreScopeSensitive(target, schemaMap, seen)) return true
1035
+ }
1036
+ }
1037
+
1038
+ for (const key of SUBSCHEMA_MAPS) {
1039
+ const group = node[key]
1040
+ if (group && typeof group === 'object' && !Array.isArray(group)) {
1041
+ for (const sub of Object.values(group)) if (refsAreScopeSensitive(sub, schemaMap, seen)) return true
1042
+ }
1043
+ }
1044
+ for (const key of SUBSCHEMA_LISTS) {
1045
+ if (Array.isArray(node[key])) {
1046
+ for (const sub of node[key]) if (refsAreScopeSensitive(sub, schemaMap, seen)) return true
1047
+ }
1048
+ }
1049
+ for (const key of SUBSCHEMA_SINGLES) {
1050
+ if (Array.isArray(node[key])) {
1051
+ for (const sub of node[key]) if (refsAreScopeSensitive(sub, schemaMap, seen)) return true
1052
+ } else if (refsAreScopeSensitive(node[key], schemaMap, seen)) return true
1053
+ }
1054
+ return false
1055
+ }
1056
+
1057
+ function hasUnresolvableRef(node, rootDefs, anchors, schemaMap, seen) {
1058
+ if (typeof node !== 'object' || node === null || Array.isArray(node)) return false
1059
+ if (seen.has(node)) return false
1060
+ seen.add(node)
1061
+
1062
+ if (typeof node.$ref === 'string') {
1063
+ if (!refResolves(node.$ref, rootDefs, anchors, schemaMap)) return true
1064
+ // Following a reference into another document changes the base URI, which
1065
+ // codegen does not track: it would resolve the target's own fragment-only
1066
+ // references against the entry schema instead. Bail so the interpreted
1067
+ // engine, which does track the base, handles it.
1068
+ const target = crossDocTarget(node.$ref, schemaMap)
1069
+ if (target && subtreeHasLocalRef(target, new Set())) return true
1070
+ }
1071
+
1072
+ for (const key of SUBSCHEMA_MAPS) {
1073
+ const group = node[key]
1074
+ if (group && typeof group === 'object' && !Array.isArray(group)) {
1075
+ for (const sub of Object.values(group)) {
1076
+ if (hasUnresolvableRef(sub, rootDefs, anchors, schemaMap, seen)) return true
1077
+ }
1078
+ }
1079
+ }
1080
+ for (const key of SUBSCHEMA_LISTS) {
1081
+ if (Array.isArray(node[key])) {
1082
+ for (const sub of node[key]) {
1083
+ if (hasUnresolvableRef(sub, rootDefs, anchors, schemaMap, seen)) return true
1084
+ }
1085
+ }
1086
+ }
1087
+ for (const key of SUBSCHEMA_SINGLES) {
1088
+ if (Array.isArray(node[key])) {
1089
+ for (const sub of node[key]) {
1090
+ if (hasUnresolvableRef(sub, rootDefs, anchors, schemaMap, seen)) return true
1091
+ }
1092
+ } else if (hasUnresolvableRef(node[key], rootDefs, anchors, schemaMap, seen)) return true
1093
+ }
1094
+ return false
1095
+ }
1096
+
936
1097
  // --- Codegen mode: generates a single Function (NOT CSP-safe) ---
937
1098
  // This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
938
1099
  function compileToJSCodegen(schema, schemaMap, userFormats) {
@@ -998,6 +1159,9 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
998
1159
  }
999
1160
  }
1000
1161
 
1162
+ if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
1163
+ if (needsBaseTracking(schema, schemaMap, new Set())) return null
1164
+
1001
1165
  const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
1002
1166
  const lines = []
1003
1167
  genCode(schema, 'd', lines, ctx)
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.2.2';
10
+ module.exports = '1.4.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
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,9 +44,9 @@
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_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_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_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.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_order.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_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_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_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_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.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_order.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_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
- "test:suite": "node tests/run_suite.js",
49
+ "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7",
50
50
  "test:compat": "node tests/test_compat.js",
51
51
  "test:standard-schema": "node tests/test_standard_schema.js",
52
52
  "test:browser": "node tests/test_browser.js",
@@ -112,12 +112,12 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.2.2",
116
- "@ata-validator/native-linux-x64-gnu": "1.2.2",
117
- "@ata-validator/native-linux-arm64-gnu": "1.2.2",
118
- "@ata-validator/native-linux-x64-musl": "1.2.2",
119
- "@ata-validator/native-linux-arm64-musl": "1.2.2",
120
- "@ata-validator/native-win32-x64": "1.2.2"
115
+ "@ata-validator/native-darwin-arm64": "1.4.0",
116
+ "@ata-validator/native-linux-x64-gnu": "1.4.0",
117
+ "@ata-validator/native-linux-arm64-gnu": "1.4.0",
118
+ "@ata-validator/native-linux-x64-musl": "1.4.0",
119
+ "@ata-validator/native-linux-arm64-musl": "1.4.0",
120
+ "@ata-validator/native-win32-x64": "1.4.0"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "yaml": "^2.0.0"