ata-validator 1.60.0 → 1.61.1

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.
Files changed (56) hide show
  1. package/README.md +134 -6
  2. package/bin/ata.js +84 -0
  3. package/build.d.ts +29 -0
  4. package/build.mjs +2 -0
  5. package/compiled-bare-rich.d.ts +6 -0
  6. package/compiled-bare.d.ts +7 -0
  7. package/compiled-bare.js +5 -0
  8. package/compiled-bare.mjs +35 -0
  9. package/compiled-object.d.ts +11 -0
  10. package/compiled-object.js +6 -0
  11. package/compiled-object.mjs +3 -0
  12. package/compiled-plain.d.ts +16 -0
  13. package/compiled-plain.js +5 -0
  14. package/compiled-plain.mjs +3 -0
  15. package/compiled.d.ts +3 -1
  16. package/index.browser.mjs +2 -2
  17. package/index.bundle.mjs +2 -2
  18. package/index.d.ts +9 -0
  19. package/index.edge.mjs +2 -2
  20. package/index.js +48 -0
  21. package/index.mjs +1 -1
  22. package/index.node.mjs +1 -1
  23. package/lib/aot-build.js +81 -6
  24. package/lib/aot-impl.js +24 -21
  25. package/lib/bun-target.js +612 -0
  26. package/lib/codegen-engine.js +9 -3
  27. package/lib/combined-runtime.js +1 -1
  28. package/lib/compiled-bare-rich.js +54 -0
  29. package/lib/compiled-bare.js +44 -0
  30. package/lib/compiled-common.js +17 -2
  31. package/lib/compiled-defaults.browser.js +9 -0
  32. package/lib/compiled-defaults.js +8 -0
  33. package/lib/compiled-object.js +123 -0
  34. package/lib/compiled-plain.js +57 -0
  35. package/lib/compiled.js +2 -1
  36. package/lib/email-literal.js +29 -0
  37. package/lib/enrich-error.js +5 -1
  38. package/lib/error-codes.js +6 -76
  39. package/lib/formats-source.js +20 -3
  40. package/lib/formats.js +1 -1
  41. package/lib/js-compiler-errors.js +49 -10
  42. package/lib/js-compiler.js +92 -13
  43. package/lib/json-rejections.js +159 -0
  44. package/lib/native-codes.js +67 -0
  45. package/lib/native-message.js +8 -0
  46. package/lib/native.browser.js +11 -0
  47. package/lib/public-api.js +1 -0
  48. package/lib/rejections.js +13 -152
  49. package/lib/standard-schema.js +1 -1
  50. package/lib/tier0.js +1 -1
  51. package/lib/validator-core.js +46 -4
  52. package/lib/verify.js +386 -0
  53. package/lib/version.js +1 -1
  54. package/lite.js +1 -0
  55. package/lite.mjs +1 -1
  56. package/package.json +44 -9
package/lib/verify.js ADDED
@@ -0,0 +1,386 @@
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
+ // The values on either side of each bound the schema states, for the value
199
+ // at one place: a module that wrote a bound off by one (`< 12` for a minimum
200
+ // of 13) answers every random document as the runtime does, and differs only
201
+ // here. A bound is where a hand edit, a stale module or a generator bug shows.
202
+ function boundaryValues (schema, type) {
203
+ const out = []
204
+ const int = type === 'integer'
205
+ const step = int ? 1 : 0.5
206
+ const num = (b) => typeof b === 'number' && Number.isFinite(b)
207
+ if (type === 'integer' || type === 'number') {
208
+ if (num(schema.minimum)) out.push(schema.minimum, schema.minimum - step)
209
+ if (num(schema.maximum)) out.push(schema.maximum, schema.maximum + step)
210
+ if (num(schema.exclusiveMinimum)) out.push(schema.exclusiveMinimum, schema.exclusiveMinimum + step)
211
+ if (num(schema.exclusiveMaximum)) out.push(schema.exclusiveMaximum, schema.exclusiveMaximum - step)
212
+ if (num(schema.multipleOf) && schema.multipleOf > 0) out.push(schema.multipleOf, schema.multipleOf * 1.5)
213
+ }
214
+ if (type === 'string') {
215
+ const str = (n) => 'abcdefghij'.repeat(Math.ceil(n / 10) + 1).slice(0, n)
216
+ if (num(schema.minLength)) { out.push(str(schema.minLength)); if (schema.minLength > 0) out.push(str(schema.minLength - 1)) }
217
+ if (num(schema.maxLength) && schema.maxLength < 4096) out.push(str(schema.maxLength), str(schema.maxLength + 1))
218
+ }
219
+ return out
220
+ }
221
+
222
+ // Documents that differ from a valid one at a single place, by a value on
223
+ // either side of a bound stated there: properties, nested objects, the first
224
+ // element of an array, through local references. At most `limit` of them.
225
+ function boundaryDocs (schema, r, defs, limit, accepts) {
226
+ // A base the runtime accepts, where the caller can say, so that a bound
227
+ // read wrong changes the verdict and not only the error list.
228
+ let base = makeValid(schema, r, defs, 0)
229
+ if (accepts) for (let i = 0; i < 50 && base !== undefined && !accepts(base); i++) base = makeValid(schema, r, defs, 0)
230
+ if (base === undefined) return []
231
+ const out = []
232
+ const visit = (sub, path, depth) => {
233
+ if (out.length >= limit || depth > 8 || !sub || typeof sub !== 'object') return
234
+ if (typeof sub.$ref === 'string') { const t = resolveLocal(sub.$ref, defs); if (t !== undefined) visit(t, path, depth + 1); return }
235
+ if (Array.isArray(sub.allOf)) { visit(mergeAllOf(sub), path, depth + 1); return }
236
+ const types = Array.isArray(sub.type) ? sub.type : sub.type !== undefined ? [sub.type] : []
237
+ for (const t of types.length ? types : ['integer', 'number', 'string']) {
238
+ for (const val of boundaryValues(sub, t)) {
239
+ if (out.length >= limit) return
240
+ const d = clone(base)
241
+ if (path.length === 0) { out.push(val); continue }
242
+ let o = d
243
+ for (let i = 0; i < path.length - 1; i++) {
244
+ if (o[path[i]] === undefined || o[path[i]] === null || typeof o[path[i]] !== 'object') o[path[i]] = typeof path[i + 1] === 'number' ? [] : {}
245
+ o = o[path[i]]
246
+ }
247
+ o[path[path.length - 1]] = val
248
+ out.push(d)
249
+ }
250
+ }
251
+ if (sub.properties && typeof sub.properties === 'object') {
252
+ for (const k of Object.keys(sub.properties)) if (k !== '__proto__') visit(sub.properties[k], [...path, k], depth + 1)
253
+ }
254
+ const item = Array.isArray(sub.prefixItems) ? sub.prefixItems[0] : sub.items && typeof sub.items === 'object' && !Array.isArray(sub.items) ? sub.items : null
255
+ if (item) visit(item, [...path, 0], depth + 1)
256
+ }
257
+ visit(schema, [], 0)
258
+ return out
259
+ }
260
+
261
+ // Documents for `schema`: `count` made, half of them with one rule broken,
262
+ // then the documents on either side of each bound (up to `count` more).
263
+ function generate (schema, count, seed, accepts) {
264
+ const r = rng(seed)
265
+ const defs = { $defs: schema && schema.$defs, definitions: schema && schema.definitions }
266
+ const docs = []
267
+ for (let i = 0; i < count; i++) {
268
+ let d = makeValid(schema, r, defs, 0)
269
+ if (i % 2 === 1 && d !== undefined) d = mutate(clone(d), r)
270
+ docs.push(d)
271
+ }
272
+ for (const d of boundaryDocs(schema, r, defs, count, accepts)) docs.push(d)
273
+ return docs
274
+ }
275
+
276
+ // The project's own documents: a JSON file is one document, a JSON Lines
277
+ // file one per line, a JSON array is taken as one document, not several.
278
+ function readSamples (paths) {
279
+ const docs = []
280
+ const files = []
281
+ for (const p of paths) {
282
+ if (!fs.existsSync(p)) continue
283
+ if (fs.statSync(p).isDirectory()) {
284
+ for (const f of fs.readdirSync(p).sort()) if (/\.(json|jsonl|ndjson)$/.test(f)) files.push(path.join(p, f))
285
+ } else files.push(p)
286
+ }
287
+ for (const f of files) {
288
+ const text = fs.readFileSync(f, 'utf8')
289
+ if (/\.(jsonl|ndjson)$/.test(f)) {
290
+ text.split('\n').forEach((line, i) => { if (line.trim()) { try { docs.push({ source: `${f}:${i + 1}`, doc: JSON.parse(line) }) } catch {} } })
291
+ } else {
292
+ try { docs.push({ source: f, doc: JSON.parse(text) }) } catch { docs.push({ source: f, doc: undefined, bad: true }) }
293
+ }
294
+ }
295
+ return docs
296
+ }
297
+
298
+ function errorKey (e) { return `${e.keyword || ''}@${e.instancePath || ''}` }
299
+ function summarize (result) {
300
+ if (!result || typeof result !== 'object') return 'no result'
301
+ const errs = Array.isArray(result.errors) ? result.errors.map(errorKey).sort() : []
302
+ return `${result.valid ? 'valid' : 'invalid'} ${errs.join(' ')}`
303
+ }
304
+
305
+ async function loadModule (file) {
306
+ if (/\.cjs$/.test(file)) { delete require.cache[require.resolve(file)]; return require(file) }
307
+ return import(pathToFileURL(file).href + `?t=${Date.now()}`)
308
+ }
309
+
310
+ // Verifies one schema against its module. Returns a report object.
311
+ async function verifyOne ({ schemaFile, moduleFile, schema, generate: count = 200, seed = 1, samples = [] }) {
312
+ const { Validator } = require('..')
313
+ const { schemaHash } = require('./schema-hash')
314
+ 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 }
315
+ if (!fs.existsSync(moduleFile)) { report.missing = true; return report }
316
+ let mod
317
+ try { mod = await loadModule(moduleFile) } catch (e) { report.error = `module does not load: ${e.message}`; return report }
318
+ if (typeof mod.isValid !== 'function' || typeof mod.validate !== 'function') { report.error = 'module exports no isValid and validate'; return report }
319
+ report.ataVersion = mod.ataVersion || null
320
+ const hash = schemaHash(schema)
321
+ if (mod.schemaHash !== undefined && mod.schemaHash !== hash) report.stale = true
322
+ let runtime
323
+ try { runtime = new Validator(schema, {}) } catch (e) { report.error = `schema does not compile: ${e.message}`; return report }
324
+ const docs = generate(schema, count, seed, (d) => { try { return runtime.isValidObject(clone(d)) } catch { return false } }).map((d, i) => ({ source: `generated #${i + 1}`, doc: d }))
325
+ report.generated = docs.length
326
+ const sampleDocs = readSamples(samples)
327
+ report.samples = sampleDocs.length
328
+ for (const { source, doc, bad } of [...docs, ...sampleDocs]) {
329
+ if (bad || doc === undefined) continue
330
+ report.documents++
331
+ const text = JSON.stringify(doc)
332
+ const a = runtime.validate(JSON.parse(text))
333
+ const b = mod.validate(JSON.parse(text))
334
+ const aIs = runtime.isValidObject(JSON.parse(text))
335
+ const bIs = mod.isValid(JSON.parse(text))
336
+ if (a.valid) report.valid++
337
+ const sa = summarize(a), sb = summarize(b)
338
+ if (sa === sb && aIs === bIs && aIs === a.valid) { report.agree++; continue }
339
+ 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 })
340
+ else report.disagreements.push(null)
341
+ }
342
+ report.disagreements = report.disagreements.filter(Boolean).concat(report.documents - report.agree > 20 ? [{ more: report.documents - report.agree - 20 }] : [])
343
+ return report
344
+ }
345
+
346
+ function renderReport (reports, { json = false } = {}) {
347
+ if (json) return JSON.stringify({ ok: reports.every(ok), schemas: reports }, null, 2)
348
+ const lines = []
349
+ for (const r of reports) {
350
+ const name = path.relative(process.cwd(), r.schema) || r.schema
351
+ if (r.missing) { lines.push(`${name}: no module at ${path.relative(process.cwd(), r.module)}; run ata build first`); continue }
352
+ if (r.error) { lines.push(`${name}: ${r.error}`); continue }
353
+ const dis = r.documents - r.agree
354
+ 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})` : ''}`)
355
+ for (const d of r.disagreements) {
356
+ if (d.more) { lines.push(` ... and ${d.more} more`); continue }
357
+ lines.push(` ${d.source}`)
358
+ lines.push(` document ${d.document}`)
359
+ lines.push(` runtime ${d.runtime}${d.runtimeIsValid !== (d.runtime.startsWith('valid')) ? ` (isValidObject ${d.runtimeIsValid})` : ''}`)
360
+ lines.push(` module ${d.module}${d.moduleIsValid !== (d.module.startsWith('valid')) ? ` (isValid ${d.moduleIsValid})` : ''}`)
361
+ }
362
+ }
363
+ const bad = reports.filter((r) => !ok(r))
364
+ lines.push(bad.length === 0
365
+ ? `ata verify: ${reports.length} schema${reports.length === 1 ? '' : 's'}, every module answers as the runtime does`
366
+ : `ata verify: ${bad.length} of ${reports.length} schema${reports.length === 1 ? '' : 's'} failed`)
367
+ return lines.join('\n')
368
+ }
369
+
370
+ function ok (r) { return !r.missing && !r.error && !r.stale && r.documents === r.agree }
371
+
372
+ // A module given as source text (a plugin's inlined module) held against the
373
+ // runtime the same way: written to a temporary file, loaded, verified.
374
+ async function verifySource (schema, src, opts = {}) {
375
+ const os = require('os')
376
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ata-verify-src-'))
377
+ const file = path.join(dir, `m.${opts.format === 'cjs' ? 'cjs' : 'mjs'}`)
378
+ fs.writeFileSync(file, src)
379
+ try {
380
+ return await verifyOne({ schemaFile: opts.name || 'schema', moduleFile: file, schema, generate: opts.generate, seed: opts.seed, samples: opts.samples || [] })
381
+ } finally {
382
+ fs.rmSync(dir, { recursive: true, force: true })
383
+ }
384
+ }
385
+
386
+ 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.60.0';
10
+ module.exports = '1.61.1';
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.60.0",
3
+ "version": "1.61.1",
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",
@@ -60,6 +60,25 @@
60
60
  "import": "./compiled-verdict.mjs",
61
61
  "require": "./compiled-verdict.js"
62
62
  },
63
+ "./compiled-object": {
64
+ "types": "./compiled-object.d.ts",
65
+ "import": "./compiled-object.mjs",
66
+ "require": "./compiled-object.js"
67
+ },
68
+ "./compiled-plain": {
69
+ "types": "./compiled-plain.d.ts",
70
+ "import": "./compiled-plain.mjs",
71
+ "require": "./compiled-plain.js"
72
+ },
73
+ "./compiled-bare": {
74
+ "types": "./compiled-bare.d.ts",
75
+ "import": "./compiled-bare.mjs",
76
+ "require": "./compiled-bare.js"
77
+ },
78
+ "./compiled-bare-rich": {
79
+ "types": "./compiled-bare-rich.d.ts",
80
+ "default": "./lib/compiled-bare-rich.js"
81
+ },
63
82
  "./lite": {
64
83
  "types": "./lite.d.ts",
65
84
  "import": "./lite.mjs",
@@ -88,7 +107,13 @@
88
107
  "./lib/native-load.js": "./lib/native-load.browser.js",
89
108
  "./lib/aot.js": "./lib/aot.browser.js",
90
109
  "./lib/buffer-gate.js": "./lib/buffer-gate.browser.js",
91
- "./lib/interpreter.js": "./lib/interpreter.browser.js"
110
+ "./lib/interpreter.js": "./lib/interpreter.browser.js",
111
+ "./lib/aot-order.js": false,
112
+ "./lib/compiled-defaults.js": "./lib/compiled-defaults.browser.js",
113
+ "./lib/native-message.js": "./lib/native.browser.js",
114
+ "./lib/native-codes.js": "./lib/native.browser.js",
115
+ "./lib/email-literal.js": false,
116
+ "./lib/native-positions.js": "./lib/native.browser.js"
92
117
  },
93
118
  "scripts": {
94
119
  "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",
@@ -172,6 +197,16 @@
172
197
  "compiled-verdict.js",
173
198
  "compiled-verdict.mjs",
174
199
  "compiled-verdict.d.ts",
200
+ "compiled-object.js",
201
+ "compiled-object.mjs",
202
+ "compiled-object.d.ts",
203
+ "compiled-plain.js",
204
+ "compiled-plain.mjs",
205
+ "compiled-plain.d.ts",
206
+ "compiled-bare.js",
207
+ "compiled-bare.mjs",
208
+ "compiled-bare.d.ts",
209
+ "compiled-bare-rich.d.ts",
175
210
  "lite.js",
176
211
  "lite.mjs",
177
212
  "lite.d.ts",
@@ -186,13 +221,13 @@
186
221
  "codegen.d.ts"
187
222
  ],
188
223
  "optionalDependencies": {
189
- "@ata-validator/native-darwin-arm64": "1.60.0",
190
- "@ata-validator/native-darwin-x64": "1.60.0",
191
- "@ata-validator/native-linux-arm64-gnu": "1.60.0",
192
- "@ata-validator/native-linux-arm64-musl": "1.60.0",
193
- "@ata-validator/native-linux-x64-gnu": "1.60.0",
194
- "@ata-validator/native-linux-x64-musl": "1.60.0",
195
- "@ata-validator/native-win32-x64": "1.60.0"
224
+ "@ata-validator/native-darwin-arm64": "1.61.1",
225
+ "@ata-validator/native-darwin-x64": "1.61.1",
226
+ "@ata-validator/native-linux-arm64-gnu": "1.61.1",
227
+ "@ata-validator/native-linux-arm64-musl": "1.61.1",
228
+ "@ata-validator/native-linux-x64-gnu": "1.61.1",
229
+ "@ata-validator/native-linux-x64-musl": "1.61.1",
230
+ "@ata-validator/native-win32-x64": "1.61.1"
196
231
  },
197
232
  "peerDependencies": {
198
233
  "yaml": "^2.0.0"