ata-validator 1.59.0 → 1.61.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.
@@ -19,6 +19,14 @@ const { normalizeKeywords, schemaUsesKeywords } = require('./keywords');
19
19
  // bundle built from lite.js never contains it. Until something registers, the
20
20
  // core behaves exactly as it does where `new Function` is blocked: every
21
21
  // schema runs on the interpreted engine, which passes the same test suite.
22
+ // Whether this process runs code compiled ahead of time, where generated
23
+ // code falls to an interpreter: Bun's flag for its compiler, or the
24
+ // environment saying so (an executable built before the flag existed).
25
+ function aotHost() {
26
+ if (env('ATA_ENGINE') === 'interpreter') return true;
27
+ const b = typeof Bun !== 'undefined' ? Bun : undefined;
28
+ return !!(b && b.isAOT === true);
29
+ }
22
30
  let _codegen = null;
23
31
  let compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined;
24
32
  function _registerCodegen(engine) {
@@ -37,6 +45,10 @@ let _aotLoader = null;
37
45
  // index.browser.mjs, null in every other entry, which then carry none of it.
38
46
  let _tiering = null;
39
47
  function _registerTiering(t) { _tiering = t; }
48
+ // Called with each validator the constructor finishes. The Node entry uses it
49
+ // to warm the engine once the event loop is free (see index.js).
50
+ let _onBuilt = null;
51
+ function _registerBuiltHook(f) { _onBuilt = f; }
40
52
  function _registerAot(load) {
41
53
  _aotLoader = load;
42
54
  }
@@ -358,64 +370,6 @@ function _jsonSyntaxRejection(e) {
358
370
 
359
371
 
360
372
 
361
- // Paths repeat across rejections of the same shape, so the parsed segment
362
- // list is cached per path string. Entries are frozen: the same array is
363
- // handed to every issue that names the path.
364
- //
365
- // The cache is bucketed by length and searched with ===, not keyed in a Map.
366
- // A path with an array index in it is concatenated afresh by every rejection,
367
- // and a Map has to hash each such string from scratch before it can look
368
- // anything up; that hashing was 30% of a Standard Schema rejection with
369
- // sixteen issues. Comparing against the few paths of the same length costs
370
- // less. Both levels are bounded so a stream of array indexes cannot grow it
371
- // without limit: a full bucket stops caching, too many lengths clear it.
372
- const _pathBuckets = new Map();
373
- const PATH_BUCKET_MAX = 32;
374
- const PATH_LENGTHS_MAX = 256;
375
- function parsePointerPath(path) {
376
- if (!path) return EMPTY_PATH;
377
- const n = path.length;
378
- let bucket = _pathBuckets.get(n);
379
- if (bucket !== undefined) {
380
- for (let i = 0; i < bucket.length; i += 2) if (bucket[i] === path) return bucket[i + 1];
381
- } else {
382
- if (_pathBuckets.size >= PATH_LENGTHS_MAX) _pathBuckets.clear();
383
- bucket = [];
384
- _pathBuckets.set(n, bucket);
385
- }
386
- const segs = Object.freeze(parsePointerPathUncached(path));
387
- if (bucket.length < PATH_BUCKET_MAX * 2) bucket.push(path, segs);
388
- return segs;
389
- }
390
- const EMPTY_PATH = Object.freeze([]);
391
-
392
- function parsePointerPathUncached(path) {
393
- // One pass, no intermediate arrays. Per Standard Schema V1 an array index
394
- // is emitted as a number and an object key as a string; a segment is an
395
- // index when it is all digits with no leading zero.
396
- const out = [];
397
- const n = path.length;
398
- let start = 1;
399
- for (let i = 1; i <= n; i++) {
400
- if (i !== n && path.charCodeAt(i) !== 47) continue;
401
- if (i > start) {
402
- let seg = path.slice(start, i);
403
- if (seg.indexOf('~') >= 0) seg = seg.replace(/~1/g, '/').replace(/~0/g, '~');
404
- const c0 = seg.charCodeAt(0);
405
- let numeric = c0 >= 48 && c0 <= 57 && (seg.length === 1 || c0 !== 48);
406
- if (numeric) {
407
- for (let k = 1; k < seg.length; k++) {
408
- const c = seg.charCodeAt(k);
409
- if (c < 48 || c > 57) { numeric = false; break; }
410
- }
411
- }
412
- out.push({ key: numeric ? Number(seg) : seg });
413
- }
414
- start = i + 1;
415
- }
416
- return out;
417
- }
418
-
419
373
  function createPaddedBuffer(jsonStr) {
420
374
  if (typeof Buffer === 'undefined') throw new Error('createPaddedBuffer requires Node.js Buffer');
421
375
  const jsonBuf = Buffer.from(jsonStr);
@@ -830,7 +784,13 @@ class Validator {
830
784
  if (options.engine !== undefined && options.engine !== 'auto' && options.engine !== 'interpreter') {
831
785
  throw new TypeError("engine must be 'auto' or 'interpreter', got " + JSON.stringify(options.engine));
832
786
  }
833
- this._interpretOnly = options.engine === 'interpreter';
787
+ // Under an ahead-of-time compiler the generated code is interpreted and
788
+ // the interpreted engine, plain JavaScript, is compiled: measured on
789
+ // Bun's compiler, 80 against 530 ns a document. So the default flips
790
+ // there: Bun.isAOT once Bun ships it, ATA_ENGINE=interpreter until then
791
+ // and for any other host. `engine: 'auto'` asked for by name keeps the
792
+ // generated code.
793
+ this._interpretOnly = options.engine === 'interpreter' || (options.engine === undefined && aotHost());
834
794
  this._initialized = false;
835
795
  this._nativeReady = false;
836
796
  this._compiled = null;
@@ -919,8 +879,10 @@ class Validator {
919
879
  this._verdictTail = null;
920
880
  this._validateTail = null;
921
881
  this._entryExt = null;
922
- // The browser build warms the engine once, when the page is idle (lib/tiering.js).
882
+ // The browser build warms the engine once, when the page is idle (lib/tiering.js);
883
+ // the Node entry does the same once the event loop is free (index.js).
923
884
  if (_tiering !== null) _tiering.built(this);
885
+ if (_onBuilt !== null) _onBuilt(this);
924
886
 
925
887
  // Public methods start as memoized accessors on the prototype; nothing is
926
888
  // allocated per instance until one is first read. See _defineLazyMethod
@@ -1008,7 +970,10 @@ class Validator {
1008
970
 
1009
971
  // Check cache first -- reuse compiled functions for same schema
1010
972
  const sm = this._schemaMap.size > 0 ? this._schemaMap : null;
1011
- const mapKey = compileCacheKey(this._schemaStr, this._schemaMap);
973
+ // The generated code takes the form of the target engine (js-compiler's
974
+ // setTargetEngine), so a function written for one engine is never handed
975
+ // to a compile asked for the other in the same process.
976
+ const mapKey = compileCacheKey(this._schemaStr, this._schemaMap) + '\u0000' + (_codegen && _codegen.jsCompiler && typeof _codegen.jsCompiler.targetEngine === 'function' ? _codegen.jsCompiler.targetEngine() : 'v8');
1012
977
  var _forceNapi = this._interpretOnly || env('ATA_FORCE_NAPI');
1013
978
  // Custom formats are JS functions: bypass the compile cache since they can
1014
979
  // differ between validators that share the same schema string. An
@@ -1838,23 +1803,9 @@ class Validator {
1838
1803
  }
1839
1804
  };
1840
1805
 
1841
- // Standard Schema V1
1806
+ // Standard Schema V1, the same object a Validator hands out.
1842
1807
  Object.defineProperty(v, "~standard", {
1843
- value: Object.freeze({
1844
- version: 1,
1845
- vendor: "ata-validator",
1846
- jsonSchema: standardJsonSchema(() => schemaObj),
1847
- validate(value) {
1848
- const result = v.validate(value);
1849
- if (result.valid) return { value };
1850
- return {
1851
- issues: result.errors.map((e) => ({
1852
- message: e.message,
1853
- path: parsePointerPath(e.instancePath),
1854
- })),
1855
- };
1856
- },
1857
- }),
1808
+ value: require('./standard-schema').standardSchemaFor(v, () => schemaObj, (self, value) => self.validate(value)),
1858
1809
  writable: false,
1859
1810
  enumerable: false,
1860
1811
  configurable: false,
@@ -1985,6 +1936,14 @@ function compile(schema, opts) {
1985
1936
  // JSONSchema type (see index.d.ts) to an inline schema object so TypeScript
1986
1937
  // gives autocomplete and value checking while authoring. Returns the schema
1987
1938
  // untouched so it can be passed straight to Validator, toStandaloneModule, etc.
1939
+ // `schemaOf<T>()`: the JSON Schema of a TypeScript type, written in by
1940
+ // @ata-project/unplugin at build time from the type and its JSDoc tags. It
1941
+ // has no run-time form: a call that reaches here was not replaced, and the
1942
+ // loud error is better than an empty schema that accepts everything.
1943
+ function schemaOf () {
1944
+ throw new Error('schemaOf<T>() is replaced at build time by @ata-project/unplugin (with ts-json-schema-generator installed); it has no run-time form. Write the JSON Schema, or build with the plugin.');
1945
+ }
1946
+
1988
1947
  function defineSchema (schema) {
1989
1948
  return schema;
1990
1949
  }
@@ -1994,74 +1953,19 @@ function defineSchema (schema) {
1994
1953
  // bound closure, stores it on the instance as an ordinary writable property
1995
1954
  // and returns it. The setter keeps the compile step's plain assignments
1996
1955
  // (`this.validate = fn`) working before the getter has ever run. Detached
1997
- // Standard JSON Schema, the `jsonSchema` member of `~standard`: what a consumer
1998
- // such as the MCP SDK reads to publish a schema, from `input({ target })`.
1999
- // ata validates the JSON Schema it was given and does not transform values, so
2000
- // input and output are the same document, handed back as a copy when the
2001
- // target is the schema's dialect: draft-07 when it declares draft-07, 2020-12
2002
- // when it declares 2020-12 or nothing, as ata reads it. Any other target
2003
- // throws, as the specification asks, rather than returning a document that
2004
- // would mean something else there.
2005
- function standardJsonSchema(getSchema) {
2006
- const convert = (options) => {
2007
- const target = options && options.target;
2008
- const schema = getSchema();
2009
- if (schema === true) return {};
2010
- if (schema === false) return { not: {} };
2011
- const declared = typeof schema.$schema === 'string' ? schema.$schema : '';
2012
- const dialect = declared === '' || declared.includes('draft/2020-12/schema') ? 'draft-2020-12'
2013
- : declared.includes('draft-07/schema') ? 'draft-07' : declared;
2014
- if (target !== dialect) {
2015
- throw new TypeError(`ata-validator cannot provide a ${dialect} schema as ${String(target)}; it does not convert between dialects`);
2016
- }
2017
- return JSON.parse(JSON.stringify(schema));
2018
- };
2019
- return Object.freeze({ input: convert, output: convert });
2020
- }
2021
-
2022
1956
  // use (`const f = v.validate`) keeps working because the closure binds the
2023
1957
  // instance.
2024
1958
  // Standard Schema V1. Built on first read, then pinned to the instance with
2025
1959
  // the same descriptor the constructor used to install eagerly.
1960
+ // Lazy on the prototype, pinned to the instance on first read; the object
1961
+ // comes from lib/standard-schema.js, shared with the wrapper around a module
1962
+ // written ahead of time, and loaded only when something reads `~standard`.
2026
1963
  Object.defineProperty(Validator.prototype, "~standard", {
2027
1964
  configurable: true,
1965
+ enumerable: false,
2028
1966
  get() {
2029
- const self = this;
2030
- const std = Object.freeze({
2031
- version: 1,
2032
- vendor: "ata-validator",
2033
- jsonSchema: standardJsonSchema(() => self._rawSchema),
2034
- validate(value) {
2035
- const result = self.validate(value);
2036
- if (result.valid) {
2037
- return { value };
2038
- }
2039
- // An issue carries a message and a path and nothing else, so the
2040
- // suggestion and source-frame work the rich error path does would be
2041
- // thrown away here. Take the raw, schema-ordered list when the result
2042
- // offers one; fall back to the public list otherwise.
2043
- const raw = typeof result._ataRaw === 'function' ? result._ataRaw() : result.errors;
2044
- const issues = new Array(raw.length);
2045
- for (let i = 0; i < raw.length; i++) {
2046
- const err = raw[i];
2047
- const path = err.instancePath != null ? err.instancePath : (err.path || '');
2048
- let message = err.message;
2049
- if (!message) {
2050
- // The native engine reports numeric codes without a message; the
2051
- // enrich pass knows how to word those. Rare, so required lazily.
2052
- message = require('./enrich-error').enrich(err, {}).message;
2053
- }
2054
- issues[i] = { message, path: parsePointerPath(path) };
2055
- }
2056
- return { issues };
2057
- },
2058
- });
2059
- Object.defineProperty(this, "~standard", {
2060
- value: std,
2061
- writable: false,
2062
- enumerable: false,
2063
- configurable: false,
2064
- });
1967
+ const std = require('./standard-schema').standardSchemaFor(this, (self) => self._rawSchema, (self, value) => self.validate(value));
1968
+ Object.defineProperty(this, "~standard", { value: std, writable: false, enumerable: false, configurable: false });
2065
1969
  return std;
2066
1970
  },
2067
1971
  });
@@ -2529,12 +2433,14 @@ module.exports = {
2529
2433
  SIMDJSON_PADDING,
2530
2434
  parseJSON,
2531
2435
  defineSchema,
2436
+ schemaOf,
2532
2437
  };
2533
2438
  // How an entry registers the code generator. Not enumerable, so the exported
2534
2439
  // surface of `require('ata-validator')` is what it was.
2535
2440
  Object.defineProperty(module.exports, '_registerCodegen', { value: _registerCodegen, enumerable: false });
2536
2441
  Object.defineProperty(module.exports, '_setCodegenLoader', { value: (load) => { _loadCodegen = load; }, enumerable: false });
2537
2442
  Object.defineProperty(module.exports, '_registerTiering', { value: _registerTiering, enumerable: false });
2443
+ Object.defineProperty(module.exports, '_registerBuiltHook', { value: _registerBuiltHook, enumerable: false });
2538
2444
  Object.defineProperty(module.exports, '_codegenRegistered', { value: () => _codegen !== null, enumerable: false });
2539
2445
  Object.defineProperty(module.exports, '_tiering', { get: () => _tiering, enumerable: false });
2540
2446
  Object.defineProperty(module.exports, '_registerAot', { value: _registerAot, enumerable: false });
package/lib/verify.js ADDED
@@ -0,0 +1,321 @@
1
+ 'use strict'
2
+ // `ata verify`: the proof that a compiled module answers as the runtime
3
+ // Validator does, run against a project's own schemas. For every schema the
4
+ // module written by `ata build` is loaded, documents are made from the schema
5
+ // (valid ones, and ones with one rule broken) and read from the project's
6
+ // samples, and both the module and a Validator built from the schema at
7
+ // verification time answer each one; the verdicts and the error lists have to
8
+ // agree. The module's schema hash is checked against the schema as it is now,
9
+ // so a module built from an older schema is reported stale.
10
+ //
11
+ // A validator that wrongly rejects gets a bug report; one that wrongly accepts
12
+ // does not. This command exists so a build-time module gets checked in CI the
13
+ // way the engine is checked here, on every change.
14
+
15
+ const fs = require('fs')
16
+ const path = require('path')
17
+ const { pathToFileURL } = require('url')
18
+
19
+ // A small deterministic generator (xorshift32), so a run can be repeated.
20
+ function rng (seed) {
21
+ let x = (seed >>> 0) || 0x9e3779b9
22
+ const next = () => { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; return (x >>> 0) / 4294967296 }
23
+ return { next, int: (n) => Math.floor(next() * n), pick: (a) => a[Math.floor(next() * a.length)], chance: (p) => next() < p }
24
+ }
25
+
26
+ const WORDS = ['ada', 'Zoë', 'order', 'x', 'north-west', 'a b', 'ÄÖÜ', 'sku-1001', 'TENOFF', 'hello world', '', '0', 'null', '😀', 'line\nbreak', ' ']
27
+ // Strings tried against a `pattern` the generator cannot read: the common
28
+ // shapes of identifiers, codes, addresses, dates and numbers.
29
+ const PATTERN_CANDIDATES = [
30
+ 'a', 'ab', 'abc', 'abc-123', 'ABC', 'A1', 'x_y', 'hello', 'Hello World', 'sku-1001', 'SKU-1001', 'TENOFF',
31
+ 'zoe@example.com', 'a@b.co', 'user.name+tag@example.org', '2026-10-25', '2026-10-25T10:00:00Z', '10:00:00',
32
+ '0', '1', '42', '-7', '3.14', '1e3', '0001', '+905551112233', '555-1234', '#fff', '#a1b2c3',
33
+ 'https://example.com/path', 'http://x.io', 'example.com', '/a/b/c', 'v1.2.3', '1.2.3', 'en-US', 'TR',
34
+ '123e4567-e89b-12d3-a456-426614174000', '192.168.0.1', '::1', 'true', 'null', 'aaaaaaaaaaaaaaaa', 'ÄÖÜ', 'çş'
35
+ ]
36
+ const FORMAT_EXAMPLES = {
37
+ email: ['zoe@example.com', 'a@b.co', 'not an email', 'x@', '@y.com'],
38
+ 'date-time': ['2026-10-25T10:00:00Z', '2026-10-25T10:00:00+03:00', '2026-10-25 10:00', 'yesterday'],
39
+ date: ['2026-10-25', '2026-13-01', '2026-2-5'],
40
+ time: ['10:00:00Z', '25:00:00Z', '10:00'],
41
+ uuid: ['123e4567-e89b-12d3-a456-426614174000', '123e4567-e89b-12d3-a456', 'not-a-uuid'],
42
+ uri: ['https://example.com/a?b=1', 'example.com', '/relative', 'mailto:a@b.co'],
43
+ 'uri-reference': ['https://example.com', '/path', '#frag', ' spaced'],
44
+ ipv4: ['192.168.0.1', '256.1.1.1', '1.2.3'],
45
+ ipv6: ['::1', '2001:db8::1', '2001:db8:::1', '1.2.3.4'],
46
+ hostname: ['example.com', 'a-b.c', '-bad.com', 'x'.repeat(70)],
47
+ regex: ['^a+$', '([', '\\d+'],
48
+ 'json-pointer': ['/a/b', 'a/b', '/a~1b', '/a~2'],
49
+ }
50
+
51
+ // A value meant to satisfy `schema`, built from its keywords; where the
52
+ // schema says little, something plausible of the kind.
53
+ function makeValid (schema, r, defs, depth) {
54
+ if (schema === true || schema === undefined) return r.pick([1, 'x', true, null, {}, []])
55
+ if (schema === false) return undefined
56
+ if (typeof schema !== 'object' || schema === null) return null
57
+ if (schema.$ref !== undefined && typeof schema.$ref === 'string') {
58
+ const t = resolveLocal(schema.$ref, defs)
59
+ if (t !== undefined && depth < 6) return makeValid(t, r, defs, depth + 1)
60
+ }
61
+ if (schema.const !== undefined) return clone(schema.const)
62
+ if (Array.isArray(schema.enum) && schema.enum.length) return clone(r.pick(schema.enum))
63
+ if (Array.isArray(schema.oneOf) && schema.oneOf.length) return makeValid(r.pick(schema.oneOf), r, defs, depth + 1)
64
+ if (Array.isArray(schema.anyOf) && schema.anyOf.length) return makeValid(r.pick(schema.anyOf), r, defs, depth + 1)
65
+ if (Array.isArray(schema.allOf) && schema.allOf.length) return makeValid(mergeAllOf(schema), r, defs, depth + 1)
66
+ let type = schema.type
67
+ if (Array.isArray(type)) type = r.pick(type)
68
+ if (type === undefined) {
69
+ if (schema.properties || schema.required || schema.additionalProperties !== undefined) type = 'object'
70
+ else if (schema.items || schema.prefixItems || schema.minItems !== undefined) type = 'array'
71
+ else if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern !== undefined || schema.format !== undefined) type = 'string'
72
+ else if (schema.minimum !== undefined || schema.maximum !== undefined || schema.multipleOf !== undefined) type = 'number'
73
+ else type = r.pick(['string', 'number', 'object', 'array', 'boolean', 'null'])
74
+ }
75
+ switch (type) {
76
+ case 'null': return null
77
+ case 'boolean': return r.chance(0.5)
78
+ case 'integer':
79
+ case 'number': {
80
+ let lo = schema.minimum !== undefined ? schema.minimum : schema.exclusiveMinimum !== undefined && typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum + 1 : -1000
81
+ let hi = schema.maximum !== undefined ? schema.maximum : schema.exclusiveMaximum !== undefined && typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum - 1 : 1000
82
+ if (hi < lo) hi = lo
83
+ let v = lo + r.next() * (hi - lo)
84
+ if (type === 'integer' || r.chance(0.6)) v = Math.round(v)
85
+ if (typeof schema.multipleOf === 'number' && schema.multipleOf > 0) v = Math.round(v / schema.multipleOf) * schema.multipleOf
86
+ if (v < lo) v = lo
87
+ if (v > hi) v = hi
88
+ return v
89
+ }
90
+ case 'string': {
91
+ if (schema.format && FORMAT_EXAMPLES[schema.format]) return r.pick(FORMAT_EXAMPLES[schema.format])
92
+ const min = schema.minLength || 0
93
+ const max = schema.maxLength !== undefined ? schema.maxLength : Math.max(min, 12)
94
+ if (schema.pattern) {
95
+ const simple = /^\^?\[([a-zA-Z0-9-]+)\]([+*])?\$?$/.exec(schema.pattern)
96
+ if (simple) {
97
+ const set = simple[1].includes('a-z') ? 'abcdefgh' : simple[1].includes('0-9') ? '0123456789' : simple[1].includes('A-Z') ? 'ABCDEFGH' : simple[1].replace(/-/g, '')
98
+ let s = ''
99
+ const n = Math.max(min, Math.min(max, 1 + r.int(8)))
100
+ for (let i = 0; i < n; i++) s += set[r.int(set.length)]
101
+ return s
102
+ }
103
+ // Any other pattern: a candidate that matches, when one does, so the
104
+ // generated documents reach past the pattern; else a word, which the
105
+ // schema then refuses, and that is a document too.
106
+ let re = null
107
+ try { re = new RegExp(schema.pattern, 'u') } catch {}
108
+ if (re) {
109
+ const matching = PATTERN_CANDIDATES.filter((c) => re.test(c) && [...c].length >= min && [...c].length <= max)
110
+ if (matching.length) return r.pick(matching)
111
+ }
112
+ }
113
+ let s = r.pick(WORDS)
114
+ while ([...s].length < min) s += 'a'
115
+ if ([...s].length > max) s = [...s].slice(0, max).join('')
116
+ return s
117
+ }
118
+ case 'array': {
119
+ const min = schema.minItems || 0
120
+ const max = schema.maxItems !== undefined ? schema.maxItems : Math.max(min, 4)
121
+ const n = depth > 4 ? min : min + r.int(Math.max(1, max - min + 1))
122
+ const out = []
123
+ for (let i = 0; i < n; i++) {
124
+ const sub = Array.isArray(schema.prefixItems) && i < schema.prefixItems.length ? schema.prefixItems[i] : Array.isArray(schema.items) ? (schema.items[i] !== undefined ? schema.items[i] : schema.additionalItems) : schema.items
125
+ if (sub === false) break
126
+ out.push(makeValid(sub === undefined ? true : sub, r, defs, depth + 1))
127
+ }
128
+ if (schema.uniqueItems) return out.filter((v, i, a) => a.findIndex((w) => JSON.stringify(w) === JSON.stringify(v)) === i)
129
+ return out
130
+ }
131
+ case 'object':
132
+ default: {
133
+ const out = {}
134
+ const props = schema.properties && typeof schema.properties === 'object' ? schema.properties : {}
135
+ const required = new Set(Array.isArray(schema.required) ? schema.required : [])
136
+ for (const k of Object.keys(props)) {
137
+ if (k === '__proto__') continue
138
+ if (required.has(k) || r.chance(0.7)) out[k] = makeValid(props[k], r, defs, depth + 1)
139
+ }
140
+ for (const k of required) if (!(k in out) && k !== '__proto__') out[k] = makeValid(props[k] === undefined ? true : props[k], r, defs, depth + 1)
141
+ if (schema.additionalProperties !== false && !schema.patternProperties && r.chance(0.2) && depth < 4) out[`extra${r.int(3)}`] = makeValid(typeof schema.additionalProperties === 'object' ? schema.additionalProperties : true, r, defs, depth + 1)
142
+ if (schema.patternProperties && r.chance(0.5)) {
143
+ for (const [p, sub] of Object.entries(schema.patternProperties)) {
144
+ const m = /^\^?([a-zA-Z_]+)/.exec(p)
145
+ if (m) out[m[1] + r.int(9)] = makeValid(sub, r, defs, depth + 1)
146
+ }
147
+ }
148
+ return out
149
+ }
150
+ }
151
+ }
152
+
153
+ function mergeAllOf (schema) {
154
+ const merged = {}
155
+ for (const b of schema.allOf) if (typeof b === 'object' && b !== null) Object.assign(merged, b, { properties: { ...(merged.properties || {}), ...(b.properties || {}) }, required: [...(merged.required || []), ...(b.required || [])] })
156
+ for (const k of Object.keys(schema)) if (k !== 'allOf' && merged[k] === undefined) merged[k] = schema[k]
157
+ return merged
158
+ }
159
+
160
+ function resolveLocal (ref, defs) {
161
+ const m = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(ref)
162
+ if (!m || !defs[m[1]]) return undefined
163
+ const key = decodeURIComponent(m[2].replace(/~1/g, '/').replace(/~0/g, '~'))
164
+ return Object.prototype.hasOwnProperty.call(defs[m[1]], key) ? defs[m[1]][key] : undefined
165
+ }
166
+
167
+ function clone (v) { return v === undefined ? v : JSON.parse(JSON.stringify(v)) }
168
+
169
+ // One rule broken somewhere in `doc`: a kind swapped, a key dropped or added,
170
+ // a number pushed out of range, a string lengthened or emptied, an array
171
+ // emptied or an element duplicated. The point is a document the schema is
172
+ // likely to refuse, so both engines get to disagree on refusals too.
173
+ function mutate (doc, r) {
174
+ const paths = []
175
+ const walk = (v, p) => {
176
+ paths.push(p)
177
+ if (Array.isArray(v)) v.forEach((x, i) => walk(x, [...p, i]))
178
+ else if (v && typeof v === 'object') for (const k of Object.keys(v)) walk(v[k], [...p, k])
179
+ }
180
+ walk(doc, [])
181
+ const p = r.pick(paths)
182
+ const get = (o, q) => q.reduce((a, k) => (a == null ? a : a[k]), o)
183
+ const set = (o, q, val) => { if (q.length === 0) return val; const parent = get(o, q.slice(0, -1)); if (parent != null) parent[q[q.length - 1]] = val; return o }
184
+ const del = (o, q) => { const parent = get(o, q.slice(0, -1)); if (Array.isArray(parent)) parent.splice(q[q.length - 1], 1); else if (parent) delete parent[q[q.length - 1]]; return o }
185
+ const v = get(doc, p)
186
+ switch (r.int(8)) {
187
+ case 0: return set(doc, p, r.pick([null, true, 'str', 0, [], {}]))
188
+ case 1: return p.length ? del(doc, p) : set(doc, p, null)
189
+ case 2: if (v && typeof v === 'object' && !Array.isArray(v)) { v[`unexpected${r.int(5)}`] = r.pick([1, 'x', null]); return doc } return set(doc, p, 'unexpected')
190
+ case 3: return set(doc, p, typeof v === 'number' ? r.pick([v * 1000 + 1, -v - 1, v + 0.5, Number.MAX_SAFE_INTEGER, -1]) : -1)
191
+ case 4: return set(doc, p, typeof v === 'string' ? r.pick(['', v.repeat(40), v + ' ', '😀' + v, v.toUpperCase()]) : '')
192
+ case 5: if (Array.isArray(v)) { if (v.length && r.chance(0.5)) v.push(clone(v[0])); else v.length = 0; return doc } return set(doc, p, [])
193
+ case 6: return set(doc, p, typeof v === 'boolean' ? !v : typeof v === 'number' ? String(v) : typeof v === 'string' ? Number(v) || 7 : v)
194
+ default: return JSON.parse(JSON.stringify(doc).replace(/"([a-zA-Z]+)":/, '"$1_":'))
195
+ }
196
+ }
197
+
198
+ // Documents for `schema`: `count` made, half of them with one rule broken.
199
+ function generate (schema, count, seed) {
200
+ const r = rng(seed)
201
+ const defs = { $defs: schema && schema.$defs, definitions: schema && schema.definitions }
202
+ const docs = []
203
+ for (let i = 0; i < count; i++) {
204
+ let d = makeValid(schema, r, defs, 0)
205
+ if (i % 2 === 1 && d !== undefined) d = mutate(clone(d), r)
206
+ docs.push(d)
207
+ }
208
+ return docs
209
+ }
210
+
211
+ // The project's own documents: a JSON file is one document, a JSON Lines
212
+ // file one per line, a JSON array is taken as one document, not several.
213
+ function readSamples (paths) {
214
+ const docs = []
215
+ const files = []
216
+ for (const p of paths) {
217
+ if (!fs.existsSync(p)) continue
218
+ if (fs.statSync(p).isDirectory()) {
219
+ for (const f of fs.readdirSync(p).sort()) if (/\.(json|jsonl|ndjson)$/.test(f)) files.push(path.join(p, f))
220
+ } else files.push(p)
221
+ }
222
+ for (const f of files) {
223
+ const text = fs.readFileSync(f, 'utf8')
224
+ if (/\.(jsonl|ndjson)$/.test(f)) {
225
+ text.split('\n').forEach((line, i) => { if (line.trim()) { try { docs.push({ source: `${f}:${i + 1}`, doc: JSON.parse(line) }) } catch {} } })
226
+ } else {
227
+ try { docs.push({ source: f, doc: JSON.parse(text) }) } catch { docs.push({ source: f, doc: undefined, bad: true }) }
228
+ }
229
+ }
230
+ return docs
231
+ }
232
+
233
+ function errorKey (e) { return `${e.keyword || ''}@${e.instancePath || ''}` }
234
+ function summarize (result) {
235
+ if (!result || typeof result !== 'object') return 'no result'
236
+ const errs = Array.isArray(result.errors) ? result.errors.map(errorKey).sort() : []
237
+ return `${result.valid ? 'valid' : 'invalid'} ${errs.join(' ')}`
238
+ }
239
+
240
+ async function loadModule (file) {
241
+ if (/\.cjs$/.test(file)) { delete require.cache[require.resolve(file)]; return require(file) }
242
+ return import(pathToFileURL(file).href + `?t=${Date.now()}`)
243
+ }
244
+
245
+ // Verifies one schema against its module. Returns a report object.
246
+ async function verifyOne ({ schemaFile, moduleFile, schema, generate: count = 200, seed = 1, samples = [] }) {
247
+ const { Validator } = require('..')
248
+ const { schemaHash } = require('./schema-hash')
249
+ const report = { schema: schemaFile, module: moduleFile, documents: 0, generated: 0, samples: 0, valid: 0, agree: 0, disagreements: [], stale: false, missing: false, ataVersion: null, error: null }
250
+ if (!fs.existsSync(moduleFile)) { report.missing = true; return report }
251
+ let mod
252
+ try { mod = await loadModule(moduleFile) } catch (e) { report.error = `module does not load: ${e.message}`; return report }
253
+ if (typeof mod.isValid !== 'function' || typeof mod.validate !== 'function') { report.error = 'module exports no isValid and validate'; return report }
254
+ report.ataVersion = mod.ataVersion || null
255
+ const hash = schemaHash(schema)
256
+ if (mod.schemaHash !== undefined && mod.schemaHash !== hash) report.stale = true
257
+ let runtime
258
+ try { runtime = new Validator(schema, {}) } catch (e) { report.error = `schema does not compile: ${e.message}`; return report }
259
+ const docs = generate(schema, count, seed).map((d, i) => ({ source: `generated #${i + 1}`, doc: d }))
260
+ report.generated = docs.length
261
+ const sampleDocs = readSamples(samples)
262
+ report.samples = sampleDocs.length
263
+ for (const { source, doc, bad } of [...docs, ...sampleDocs]) {
264
+ if (bad || doc === undefined) continue
265
+ report.documents++
266
+ const text = JSON.stringify(doc)
267
+ const a = runtime.validate(JSON.parse(text))
268
+ const b = mod.validate(JSON.parse(text))
269
+ const aIs = runtime.isValidObject(JSON.parse(text))
270
+ const bIs = mod.isValid(JSON.parse(text))
271
+ if (a.valid) report.valid++
272
+ const sa = summarize(a), sb = summarize(b)
273
+ if (sa === sb && aIs === bIs && aIs === a.valid) { report.agree++; continue }
274
+ if (report.disagreements.length < 20) report.disagreements.push({ source, document: text.length > 400 ? text.slice(0, 400) + '…' : text, runtime: sa, module: sb, runtimeIsValid: aIs, moduleIsValid: bIs })
275
+ else report.disagreements.push(null)
276
+ }
277
+ report.disagreements = report.disagreements.filter(Boolean).concat(report.documents - report.agree > 20 ? [{ more: report.documents - report.agree - 20 }] : [])
278
+ return report
279
+ }
280
+
281
+ function renderReport (reports, { json = false } = {}) {
282
+ if (json) return JSON.stringify({ ok: reports.every(ok), schemas: reports }, null, 2)
283
+ const lines = []
284
+ for (const r of reports) {
285
+ const name = path.relative(process.cwd(), r.schema) || r.schema
286
+ if (r.missing) { lines.push(`${name}: no module at ${path.relative(process.cwd(), r.module)}; run ata build first`); continue }
287
+ if (r.error) { lines.push(`${name}: ${r.error}`); continue }
288
+ const dis = r.documents - r.agree
289
+ lines.push(`${name}: ${r.documents} documents (${r.generated} generated, ${r.samples} from samples), ${r.valid} valid by the runtime, ${r.agree} agree, ${dis} disagree${r.stale ? '; STALE: the module was built from a different schema' : ''}${r.ataVersion ? ` (module from ata ${r.ataVersion})` : ''}`)
290
+ for (const d of r.disagreements) {
291
+ if (d.more) { lines.push(` ... and ${d.more} more`); continue }
292
+ lines.push(` ${d.source}`)
293
+ lines.push(` document ${d.document}`)
294
+ lines.push(` runtime ${d.runtime}${d.runtimeIsValid !== (d.runtime.startsWith('valid')) ? ` (isValidObject ${d.runtimeIsValid})` : ''}`)
295
+ lines.push(` module ${d.module}${d.moduleIsValid !== (d.module.startsWith('valid')) ? ` (isValid ${d.moduleIsValid})` : ''}`)
296
+ }
297
+ }
298
+ const bad = reports.filter((r) => !ok(r))
299
+ lines.push(bad.length === 0
300
+ ? `ata verify: ${reports.length} schema${reports.length === 1 ? '' : 's'}, every module answers as the runtime does`
301
+ : `ata verify: ${bad.length} of ${reports.length} schema${reports.length === 1 ? '' : 's'} failed`)
302
+ return lines.join('\n')
303
+ }
304
+
305
+ function ok (r) { return !r.missing && !r.error && !r.stale && r.documents === r.agree }
306
+
307
+ // A module given as source text (a plugin's inlined module) held against the
308
+ // runtime the same way: written to a temporary file, loaded, verified.
309
+ async function verifySource (schema, src, opts = {}) {
310
+ const os = require('os')
311
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ata-verify-src-'))
312
+ const file = path.join(dir, `m.${opts.format === 'cjs' ? 'cjs' : 'mjs'}`)
313
+ fs.writeFileSync(file, src)
314
+ try {
315
+ return await verifyOne({ schemaFile: opts.name || 'schema', moduleFile: file, schema, generate: opts.generate, seed: opts.seed, samples: opts.samples || [] })
316
+ } finally {
317
+ fs.rmSync(dir, { recursive: true, force: true })
318
+ }
319
+ }
320
+
321
+ module.exports = { verifyOne, verifySource, renderReport, generate, mutate, readSamples, ok }
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.59.0';
10
+ module.exports = '1.61.0';
package/lite.js CHANGED
@@ -21,5 +21,6 @@ module.exports = {
21
21
  validateAsync: core.validateAsync,
22
22
  parseAsync: core.parseAsync,
23
23
  defineSchema: core.defineSchema,
24
+ schemaOf: core.schemaOf,
24
25
  version: core.version,
25
26
  };
package/lite.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import mod from './lite.js';
2
- export const { Validator, validate, validateAsync, parseAsync, defineSchema, version } = mod;
2
+ export const { Validator, validate, validateAsync, parseAsync, defineSchema, schemaOf, version } = mod;
3
3
  export default mod;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.59.0",
3
+ "version": "1.61.0",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -88,7 +88,8 @@
88
88
  "./lib/native-load.js": "./lib/native-load.browser.js",
89
89
  "./lib/aot.js": "./lib/aot.browser.js",
90
90
  "./lib/buffer-gate.js": "./lib/buffer-gate.browser.js",
91
- "./lib/interpreter.js": "./lib/interpreter.browser.js"
91
+ "./lib/interpreter.js": "./lib/interpreter.browser.js",
92
+ "./lib/aot-order.js": false
92
93
  },
93
94
  "scripts": {
94
95
  "prepublishOnly": "node tests/test_pack_purity.js && ATA_ALLOW_DOC_PLACEHOLDERS=1 node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_embed.js && node tests/test_version_sync.js",
@@ -186,13 +187,13 @@
186
187
  "codegen.d.ts"
187
188
  ],
188
189
  "optionalDependencies": {
189
- "@ata-validator/native-darwin-arm64": "1.59.0",
190
- "@ata-validator/native-darwin-x64": "1.59.0",
191
- "@ata-validator/native-linux-arm64-gnu": "1.59.0",
192
- "@ata-validator/native-linux-arm64-musl": "1.59.0",
193
- "@ata-validator/native-linux-x64-gnu": "1.59.0",
194
- "@ata-validator/native-linux-x64-musl": "1.59.0",
195
- "@ata-validator/native-win32-x64": "1.59.0"
190
+ "@ata-validator/native-darwin-arm64": "1.61.0",
191
+ "@ata-validator/native-darwin-x64": "1.61.0",
192
+ "@ata-validator/native-linux-arm64-gnu": "1.61.0",
193
+ "@ata-validator/native-linux-arm64-musl": "1.61.0",
194
+ "@ata-validator/native-linux-x64-gnu": "1.61.0",
195
+ "@ata-validator/native-linux-x64-musl": "1.61.0",
196
+ "@ata-validator/native-win32-x64": "1.61.0"
196
197
  },
197
198
  "peerDependencies": {
198
199
  "yaml": "^2.0.0"