ata-validator 1.26.0 → 1.27.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,17 @@
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.27.0 - 2026-09-17
6
+
7
+ ### Added
8
+
9
+ - `parse()` is now emitted for the record shape: `additionalProperties` as a schema, which is what `z.record` and every generator's map type compile to, with or without declared `properties` beside it. The key set is open by declaration, so keeping every key is exact, and every undeclared value is rebuilt against the one schema that governs it, stripping unknown keys inside it the same way a declared property's rebuild would. Defaults under a record value are not filled, because the runtime's `useDefaults` does not fill them there and `parse()` output stays equal to `validate().data`. Still declined, loudly: `additionalProperties: true` (the value is unconstrained, and this pass only copies what it can prove), applicators next to a record (a branch could constrain some keys' values beyond the record schema), and `patternProperties` (a key matching several patterns must satisfy all of them at once, which the rebuild cannot pick a shape for).
10
+ - `onWarning` now receives a second argument, `{ kind }`, naming which capability degraded: `'error-detail'` or `'parse'`. A build that requested both can tell the warnings apart instead of treating any warning as degraded errors. `build()` result warnings carry the same `kind` field. Existing single-argument callbacks keep working unchanged.
11
+
12
+ ### Fixed
13
+
14
+ - Two more fixed-name collisions in the generated-code path, the same family as the `patternProperties` helper-scope bug in 1.26.0: a record whose values are themselves records (`additionalProperties` nested in itself) generated an inner loop that redeclared the outer loop's variables and threw `ReferenceError: Cannot access '_av' before initialization` at validation time, and `patternProperties` inside a `patternProperties` value schema had the same collision on its own loop variables. All three loop names now carry a unique suffix. The combined error generator already did this; the boolean generator now matches it.
15
+
5
16
  ## 1.26.0 - 2026-09-17
6
17
 
7
18
  ### Added
package/lib/aot-build.js CHANGED
@@ -159,14 +159,16 @@ async function build(opts) {
159
159
  sourceMap,
160
160
  schemaFile,
161
161
  formatMode: opts.formatMode,
162
- onWarning: (w) => inputWarnings.push(w),
162
+ onWarning: (w, meta) => inputWarnings.push({ message: w, kind: (meta && meta.kind) || 'general' }),
163
163
  });
164
164
  if (inputWarnings.length > 0) {
165
165
  if (opts.strict) {
166
- failed.push({ input, error: inputWarnings.join('; ') });
166
+ failed.push({ input, error: inputWarnings.map((x) => x.message).join('; ') });
167
167
  continue;
168
168
  }
169
- for (const w of inputWarnings) warnings.push({ input, warning: w });
169
+ // `kind` says which capability degraded ('error-detail', 'parse'),
170
+ // so a caller that requested both can tell the warnings apart.
171
+ for (const x of inputWarnings) warnings.push({ input, warning: x.message, kind: x.kind });
170
172
  }
171
173
  if (!src) {
172
174
  const reason = 'schema is not AOT-compatible (toStandaloneModule returned null)';
package/lib/aot-impl.js CHANGED
@@ -279,15 +279,35 @@ function emitClone(node, access, depth) {
279
279
  // never widen the key set. `not` never widens it. `unevaluatedProperties`
280
280
  // is admitted only as `false` under that proof, where it is exactly
281
281
  // `additionalProperties: false`.
282
- if (node.$ref || node.patternProperties ||
283
- node.additionalProperties === true ||
284
- (node.additionalProperties && typeof node.additionalProperties === 'object')) {
282
+ if (node.$ref || node.patternProperties || node.additionalProperties === true) {
285
283
  return null;
286
284
  }
285
+ // additionalProperties as a schema is the record shape (z.record and every
286
+ // generator's map type): the key set is open by declaration, so keeping
287
+ // every key is exact, and every undeclared value is rebuilt against that
288
+ // one schema. That is a proof, not a guess, so it is admitted. What stays
289
+ // out: applicators next to a record (a branch could constrain some keys'
290
+ // values beyond the record schema, and the rebuild would ignore it), and
291
+ // `additionalProperties: true`, where the value is unconstrained and this
292
+ // pass only copies what it can name. An open object with no
293
+ // `additionalProperties` at all also stays out, for the original reason:
294
+ // validate() accepts its unknown keys, so a parse() that strips them would
295
+ // silently drop allowed data, and one that keeps them raw would not be a
296
+ // sanitiser.
297
+ const apSchema = (node.additionalProperties && typeof node.additionalProperties === 'object')
298
+ ? node.additionalProperties
299
+ : null;
300
+ if (apSchema) {
301
+ if (node.unevaluatedProperties !== undefined) return null;
302
+ if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
303
+ node.dependentSchemas !== undefined || node.dependencies !== undefined ||
304
+ node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return null;
305
+ }
287
306
  if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== false) return null;
288
- if (node.type !== 'object' || !node.properties) return null;
289
- const keys = Object.keys(node.properties);
290
- if (keys.length === 0) return null;
307
+ if (node.type !== 'object') return null;
308
+ if (!node.properties && !apSchema) return null;
309
+ const keys = node.properties ? Object.keys(node.properties) : [];
310
+ if (keys.length === 0 && !apSchema) return null;
291
311
  if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
292
312
  node.unevaluatedProperties === false) {
293
313
  if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined ||
@@ -307,13 +327,6 @@ function emitClone(node, access, depth) {
307
327
  }
308
328
  const required = new Set(Array.isArray(node.required) ? node.required : []);
309
329
 
310
- // A property is copyable only when the emitter can name everything that
311
- // may live under it: a primitive holds nothing, and an object with declared
312
- // properties is rebuilt the same way. A $ref, a composition, an array of
313
- // objects or an untyped node could all carry keys this code cannot see, and
314
- // copying the reference would smuggle them into a value that claims to be
315
- // sanitised, so the whole clone is declined instead.
316
- const PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
317
330
  const fixed = [];
318
331
  const conditional = [];
319
332
  for (const key of keys) {
@@ -336,47 +349,41 @@ function emitClone(node, access, depth) {
336
349
  if (!probe || !probe(prop.default)) return null;
337
350
  }
338
351
  const read = `${access}[${JSON.stringify(key)}]`;
339
- let value;
340
- const types = Array.isArray(prop.type) ? prop.type : [prop.type];
341
- if (types.every((t) => PRIMITIVE.has(t))) {
342
- value = read;
343
- } else if (prop.type === 'object' && prop.properties) {
344
- const nested = emitClone(prop, read, depth + 1);
345
- if (!nested) return null;
346
- // The nested schema may also admit null, in which case there is nothing
347
- // to rebuild and the value passes through.
348
- value = required.has(key) ? nested : nested;
349
- } else if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
350
- !Array.isArray(prop.items) &&
351
- (Array.isArray(prop.items.type) ? prop.items.type : [prop.items.type])
352
- .every((t) => PRIMITIVE.has(t))) {
353
- // An array of primitives has nowhere to hide a key. The reference is
354
- // shared with the input, exactly as a shallow strip would leave it.
355
- value = read;
356
- } else if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
357
- !Array.isArray(prop.items) && prop.items.type === 'object' &&
358
- prop.items.properties) {
359
- // An array of objects: rebuild each element the same way, so unknown
360
- // keys inside the array are dropped too. Most real schemas have one of
361
- // these, and declining them left parse() emitted only for shapes nobody
362
- // writes.
363
- const el = '_e' + depth;
364
- const inner = emitClone(prop.items, el, depth + 1);
365
- if (!inner) return null;
366
- value = `${read}.map((${el}) => (${inner}))`;
367
- } else {
368
- return null;
369
- }
352
+ const value = emitValueExpr(prop, read, depth);
353
+ if (value === null) return null;
370
354
  if (required.has(key)) fixed.push(`${JSON.stringify(key)}: ${value}`);
371
355
  else conditional.push({ key, value, dflt: prop.default });
372
356
  }
373
- if (fixed.length === 0 && conditional.length === 0) return null;
357
+ if (fixed.length === 0 && conditional.length === 0 && !apSchema) return null;
358
+
359
+ const tmp = `_c${depth}`;
360
+ // The record loop: every key of the input that is not a declared property
361
+ // is kept, its value rebuilt against the additionalProperties schema. The
362
+ // input is already validated, so every such value satisfies that schema;
363
+ // the rebuild only strips inside it. Defaults under a record value are
364
+ // dropped before emission because the runtime's useDefaults does not fill
365
+ // them there, and parse() must equal validate().data, not improve on it.
366
+ let recordLoop = '';
367
+ if (apSchema) {
368
+ const kVar = `_k${depth}`;
369
+ const vVar = `_v${depth}`;
370
+ const valueExpr = emitValueExpr(stripDefaultsDeep(apSchema), vVar, depth);
371
+ if (valueExpr === null) return null;
372
+ let skip = '';
373
+ let declSet = '';
374
+ if (keys.length > 8) {
375
+ declSet = `const _d${depth} = new Set(${JSON.stringify(keys)}); `;
376
+ skip = `if (_d${depth}.has(${kVar})) continue; `;
377
+ } else if (keys.length > 0) {
378
+ skip = `if (${keys.map((k) => `${kVar} === ${JSON.stringify(k)}`).join(' || ')}) continue; `;
379
+ }
380
+ recordLoop = ` ${declSet}for (const ${kVar} of Object.keys(${access})) { ${skip}const ${vVar} = ${access}[${kVar}]; ${tmp}[${kVar}] = ${valueExpr}; }`;
381
+ }
374
382
 
375
383
  const literal = `{ ${fixed.join(', ')} }`;
376
- if (conditional.length === 0) return literal;
384
+ if (conditional.length === 0 && !recordLoop) return literal;
377
385
  // Optional properties are added only when present, so the result never
378
386
  // gains a key the input did not have.
379
- const tmp = `_c${depth}`;
380
387
  const adds = conditional
381
388
  .map(({ key, value, dflt }) => {
382
389
  const set = `if (${JSON.stringify(key)} in ${access}) ${tmp}[${JSON.stringify(key)}] = ${value};`;
@@ -385,7 +392,55 @@ function emitClone(node, access, depth) {
385
392
  return dflt === undefined ? set : `${set} else ${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
386
393
  })
387
394
  .join(' ');
388
- return `(function(){ const ${tmp} = ${literal}; ${adds} return ${tmp}; })()`;
395
+ return `(function(){ const ${tmp} = ${literal}; ${adds}${recordLoop} return ${tmp}; })()`;
396
+ }
397
+
398
+ // The value dispatch shared by declared properties and the record loop. A
399
+ // value is copyable only when the emitter can name everything that may live
400
+ // under it: a primitive holds nothing, an object with declared properties or
401
+ // a record schema is rebuilt the same way, an array of primitives has
402
+ // nowhere to hide a key. A $ref, a composition or an untyped node could all
403
+ // carry keys this code cannot see, and copying the reference would smuggle
404
+ // them into a value that claims to be sanitised, so the caller declines the
405
+ // whole clone instead.
406
+ // Drop every schema-position `default` in a subtree, leaving data positions
407
+ // (const, enum, examples) untouched. Used on a record's value schema, where
408
+ // the runtime's useDefaults fills nothing.
409
+ function stripDefaultsDeep(node) {
410
+ if (!node || typeof node !== 'object') return node;
411
+ if (Array.isArray(node)) return node.map(stripDefaultsDeep);
412
+ const out = {};
413
+ for (const k of Object.keys(node)) {
414
+ if (k === 'default') continue;
415
+ out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
416
+ }
417
+ return out;
418
+ }
419
+
420
+ const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
421
+ function emitValueExpr(prop, read, depth) {
422
+ if (!prop || typeof prop !== 'object') return null;
423
+ if (prop.$ref) return null;
424
+ const types = Array.isArray(prop.type) ? prop.type : [prop.type];
425
+ if (types.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
426
+ const isRecordable = (s) => s.properties ||
427
+ (s.additionalProperties && typeof s.additionalProperties === 'object');
428
+ if (prop.type === 'object' && isRecordable(prop)) {
429
+ return emitClone(prop, read, depth + 1);
430
+ }
431
+ if (prop.type === 'array' && prop.items && typeof prop.items === 'object' &&
432
+ !Array.isArray(prop.items)) {
433
+ const it = prop.items;
434
+ const itemTypes = Array.isArray(it.type) ? it.type : [it.type];
435
+ if (itemTypes.every((t) => _CLONE_PRIMITIVE.has(t))) return read;
436
+ if (it.type === 'object' && isRecordable(it)) {
437
+ const el = '_e' + depth;
438
+ const inner = emitClone(it, el, depth + 1);
439
+ if (!inner) return null;
440
+ return `${read}.map((${el}) => (${inner}))`;
441
+ }
442
+ }
443
+ return null;
389
444
  }
390
445
 
391
446
  function toStandaloneModule(validator, opts) {
@@ -419,8 +474,12 @@ function toStandaloneModule(validator, opts) {
419
474
  // way the runtime validator does. The module still ships, with the
420
475
  // verdict exact, but every failure reports the single ATA9000 stub.
421
476
  // Silence here cost a user a debugging session; hence the channel.
477
+ // The second argument names which capability degraded, so a build that
478
+ // requested several can tell this warning from a parse decline instead
479
+ // of treating any warning as "errors degraded".
422
480
  opts.onWarning(
423
- 'error detail could not be generated for this schema; the module reports failures as the single ATA9000 abort-early error. The verdict is unaffected. For detailed errors, validate failing documents with the runtime Validator.'
481
+ 'error detail could not be generated for this schema; the module reports failures as the single ATA9000 abort-early error. The verdict is unaffected. For detailed errors, validate failing documents with the runtime Validator.',
482
+ { kind: 'error-detail' }
424
483
  );
425
484
  }
426
485
  }
@@ -501,7 +560,8 @@ function toStandaloneModule(validator, opts) {
501
560
  cloneExpr = baseSchema ? emitClone(inlineRefsForClone(baseSchema), 'data', 0) : null;
502
561
  if (!cloneExpr && typeof opts.onWarning === 'function') {
503
562
  opts.onWarning(
504
- 'parse() could not be generated for this schema: the rebuild is only emitted where the allowed key set is provable, and a remaining $ref (cyclic, external, or carrying constraining siblings), patternProperties, or an additionalProperties schema makes it someone else\'s decision. The module ships without a parse export; validate and strip with the runtime Validator instead.'
563
+ 'parse() could not be generated for this schema: the rebuild is only emitted where the allowed key set is provable, and a remaining $ref (cyclic, external, or carrying constraining siblings), patternProperties, applicators next to an open key set, or an unconstrained additionalProperties makes it someone else\'s decision. The module ships without a parse export; validate and strip with the runtime Validator instead.',
564
+ { kind: 'parse' }
505
565
  );
506
566
  }
507
567
  }
@@ -2201,17 +2201,24 @@ function genCode(schema, v, lines, ctx, knownType) {
2201
2201
  // against that sub-schema. Skip if patternProperties is present (handled by
2202
2202
  // the unified loop). Composition cases are filtered out by codegenSafe.
2203
2203
  if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null && !schema.patternProperties) {
2204
+ // The loop variables carry a unique suffix: a record whose values are
2205
+ // themselves records nests this loop inside itself, and a fixed `_av`
2206
+ // made the inner `const _av=_av[_k]` a self-reference that threw at
2207
+ // validation time.
2208
+ const apId = ctx._apLoopId = (ctx._apLoopId || 0) + 1
2209
+ const kVar = `_k${apId}`
2210
+ const avVar = `_av${apId}`
2204
2211
  const declared = schema.properties ? Object.keys(schema.properties) : []
2205
2212
  const skipCheck = declared.length === 0
2206
2213
  ? null
2207
- : declared.map(k => `_k===${JSON.stringify(k)}`).join('||')
2214
+ : declared.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
2208
2215
  const subLines = []
2209
- genCode(schema.additionalProperties, '_av', subLines, ctx)
2216
+ genCode(schema.additionalProperties, avVar, subLines, ctx)
2210
2217
  if (subLines.length > 0) {
2211
2218
  const body = subLines.join(';')
2212
2219
  const loop = skipCheck
2213
- ? `for(var _k in ${v}){if(${skipCheck})continue;const _av=${v}[_k];${body}}`
2214
- : `for(var _k in ${v}){const _av=${v}[_k];${body}}`
2220
+ ? `for(var ${kVar} in ${v}){if(${skipCheck})continue;const ${avVar}=${v}[${kVar}];${body}}`
2221
+ : `for(var ${kVar} in ${v}){const ${avVar}=${v}[${kVar}];${body}}`
2215
2222
  _deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
2216
2223
  }
2217
2224
  }
@@ -2251,7 +2258,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2251
2258
  for (let i = 0; i < ppEntries.length; i++) {
2252
2259
  const [, sub] = ppEntries[i]
2253
2260
  const subLines = []
2254
- genCode(sub, `_ppv`, subLines, ctx)
2261
+ genCode(sub, `_ppv${pi}`, subLines, ctx)
2255
2262
  subChecks.push(subLines.join(';'))
2256
2263
  }
2257
2264
 
@@ -2269,7 +2276,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2269
2276
  let apCheck = null
2270
2277
  if (apSchema) {
2271
2278
  const apLines = []
2272
- genCode(apSchema, '_apv', apLines, ctx)
2279
+ genCode(apSchema, `_apv${pi}`, apLines, ctx)
2273
2280
  apCheck = apLines.join(';')
2274
2281
  }
2275
2282
  lines.push(`${guard}{for(const ${kVar} in ${v}){`)
@@ -2304,14 +2311,14 @@ function genCode(schema, v, lines, ctx, knownType) {
2304
2311
  if (ppEntries.length > 0) {
2305
2312
  lines.push(`let _pm${pi}=false`)
2306
2313
  for (let i = 0; i < ppEntries.length; i++) {
2307
- lines.push(`if(${matchers[i].check}){_pm${pi}=true;const _ppv=${v}[${kVar}];${subChecks[i]}}`)
2314
+ lines.push(`if(${matchers[i].check}){_pm${pi}=true;const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
2308
2315
  }
2309
2316
  }
2310
2317
  // A key that is neither declared nor matched is additional. switch on
2311
2318
  // the declared names (V8 compiles string cases to a jump table); no
2312
2319
  // switch at all when nothing is declared, since a switch with no case
2313
2320
  // clause is a syntax error.
2314
- const additional = apCheck !== null ? `const _apv=${v}[${kVar}];${apCheck}` : `return false`
2321
+ const additional = apCheck !== null ? `const _apv${pi}=${v}[${kVar}];${apCheck}` : `return false`
2315
2322
  const notMatched = ppEntries.length > 0 ? `if(!_pm${pi}){${additional}}` : additional
2316
2323
  if (propKeys.length) {
2317
2324
  const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
@@ -2348,7 +2355,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2348
2355
  }
2349
2356
  }
2350
2357
  for (let i = 0; i < ppEntries.length; i++) {
2351
- lines.push(`if(${matchers[i].check}){const _ppv=${v}[${kVar}];${subChecks[i]}}`)
2358
+ lines.push(`if(${matchers[i].check}){const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
2352
2359
  }
2353
2360
  lines.push(`}}`)
2354
2361
  }
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.26.0';
10
+ module.exports = '1.27.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -122,13 +122,13 @@
122
122
  "LICENSE"
123
123
  ],
124
124
  "optionalDependencies": {
125
- "@ata-validator/native-darwin-arm64": "1.26.0",
126
- "@ata-validator/native-darwin-x64": "1.26.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.26.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.26.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.26.0",
130
- "@ata-validator/native-linux-x64-musl": "1.26.0",
131
- "@ata-validator/native-win32-x64": "1.26.0"
125
+ "@ata-validator/native-darwin-arm64": "1.27.0",
126
+ "@ata-validator/native-darwin-x64": "1.27.0",
127
+ "@ata-validator/native-linux-arm64-gnu": "1.27.0",
128
+ "@ata-validator/native-linux-arm64-musl": "1.27.0",
129
+ "@ata-validator/native-linux-x64-gnu": "1.27.0",
130
+ "@ata-validator/native-linux-x64-musl": "1.27.0",
131
+ "@ata-validator/native-win32-x64": "1.27.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"