ata-validator 1.19.0 → 1.21.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.
@@ -1,4 +1,9 @@
1
1
  'use strict';
2
+ const { collapseBranches } = require('./branch-collapse');
3
+
4
+ // One branch title, for the collapsed anyOf/oneOf error. The compiled node
5
+ // keeps the schema it came from, which is where a `title` lives.
6
+ const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string' ? n.schema.title : '');
2
7
 
3
8
  // Compiles interpreter Plans into a tree of specialized closures. No source
4
9
  // generation, no `new Function`: this is plain closure composition, so it
@@ -334,13 +339,13 @@ function install(deps) {
334
339
  if (collect) {
335
340
  cstep = (data, errors, instancePath, schemaPath, stack, rec) => {
336
341
  const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
337
- const ok = target.c(data, errors, instancePath, schemaPath + seg, stack, rec);
342
+ const ok = target.c(data, errors, instancePath, schemaPath, stack, rec);
338
343
  if (!ok) undo(rec, pl, n0, xl);
339
344
  return ok;
340
345
  };
341
346
  vstep = (data, stack, rec) => target.v(data, stack, rec);
342
347
  } else {
343
- cstep = (data, errors, instancePath, schemaPath, stack) => target.c(data, errors, instancePath, schemaPath + seg, stack);
348
+ cstep = (data, errors, instancePath, schemaPath, stack) => target.c(data, errors, instancePath, schemaPath, stack);
344
349
  vstep = target.v;
345
350
  }
346
351
  if (ctx.guard) {
@@ -447,7 +452,10 @@ function install(deps) {
447
452
  ok = false;
448
453
  }
449
454
  if (maxC !== undefined && matched > maxC) {
450
- if (errors !== NOERRORS) errors.push(err('maxContains', 'maxContains', instancePath, schemaPath + '/maxContains', { limit: maxC }, `must NOT contain more than ${maxC} valid item(s)`));
455
+ // Reported as `contains` at `#/contains`, the way the generated code
456
+ // and the reference implementation both do it: maxContains bounds the
457
+ // same keyword rather than being one of its own.
458
+ if (errors !== NOERRORS) errors.push(err('contains', 'contains', instancePath, schemaPath + '/contains', { minContains: minC, maxContains: maxC }, `must NOT contain more than ${maxC} valid item(s)`));
451
459
  ok = false;
452
460
  }
453
461
  return ok;
@@ -493,6 +501,13 @@ function install(deps) {
493
501
  ? P.patternProperties.map((e) => ({ re: e.re, src: e.src, fn: child(e.node) }))
494
502
  : null;
495
503
  const apFn = P.additionalProperties !== undefined ? child(P.additionalProperties) : null;
504
+ // `additionalProperties: false` is a boolean subschema, but its error is
505
+ // not the generic "boolean schema is false": every consumer switches on
506
+ // keyword === 'additionalProperties' and reads params.additionalProperty
507
+ // to learn which key was rejected. Reporting it as `not` at the key's own
508
+ // path loses both, and that shape is what the generated code emits, so
509
+ // the two engines have to agree here.
510
+ const apFalse = P.additionalProperties === false;
496
511
  const pnFn = P.propertyNames !== undefined ? child(P.propertyNames) : null;
497
512
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
498
513
  if (dataBits(data) !== T_OBJECT) return true;
@@ -500,7 +515,12 @@ function install(deps) {
500
515
  const keys = collect ? keysOf(rec, data) : Object.keys(data);
501
516
  if (pnFn !== null) {
502
517
  for (const k of keys) {
503
- if (!pnFn.c(k, errors, instancePath + '/' + escapePointer(k), schemaPath + '/propertyNames', stack)) {
518
+ // propertyNames applies its subschema to the property NAME, not to
519
+ // the value sitting at that key, so the failure is reported against
520
+ // the object that carries the name rather than against `/<key>`.
521
+ // The generated code and the compat shim both do it this way, and
522
+ // the shim has to because that is where the reference puts it.
523
+ if (!pnFn.c(k, errors, instancePath, schemaPath + '/propertyNames', stack)) {
504
524
  ok = false;
505
525
  if (errors === NOERRORS) return false;
506
526
  }
@@ -530,7 +550,11 @@ function install(deps) {
530
550
  }
531
551
  }
532
552
  if (!evaluated && apFn !== null) {
533
- if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
553
+ if (apFalse) {
554
+ ok = false;
555
+ if (errors === NOERRORS) return false;
556
+ errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
557
+ } else if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
534
558
  ok = false;
535
559
  if (errors === NOERRORS) return false;
536
560
  }
@@ -717,20 +741,31 @@ function install(deps) {
717
741
  }
718
742
  if (P.anyOf !== null) {
719
743
  const fns = P.anyOf.map((v) => inPlace(v, base));
744
+ const titles = P.anyOf.map(branchTitle);
720
745
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
721
746
  const scratch = errors === NOERRORS ? NOERRORS : [];
722
747
  let any = false;
748
+ // Per-branch errors, taken as slices of the shared scratch rather than
749
+ // an array each, so a branch that passes costs nothing extra.
750
+ const branches = errors === NOERRORS ? null : [];
723
751
  for (let i = 0; i < fns.length; i++) {
752
+ const mark = branches === null ? 0 : scratch.length;
753
+ let ok;
724
754
  if (collect) {
725
755
  const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
726
- if (fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, rec)) any = true;
756
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack, rec);
757
+ if (ok) any = true;
727
758
  else undo(rec, pl, n0, xl);
728
- } else if (fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack)) any = true;
759
+ } else {
760
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/anyOf/' + i, stack);
761
+ if (ok) any = true;
762
+ }
763
+ if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: titles[i] });
729
764
  }
730
765
  if (!any) {
731
- if (errors !== NOERRORS) {
732
- for (const e of scratch) errors.push(e);
733
- errors.push(err('anyOf', 'anyOf', instancePath, schemaPath + '/anyOf', {}, 'must match a schema in anyOf'));
766
+ if (branches !== null) {
767
+ const collapsed = collapseBranches({ keyword: 'anyOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/anyOf' });
768
+ if (collapsed !== null) errors.push(collapsed);
734
769
  }
735
770
  return false;
736
771
  }
@@ -756,25 +791,36 @@ function install(deps) {
756
791
  }
757
792
  if (P.oneOf !== null) {
758
793
  const fns = P.oneOf.map((v) => inPlace(v, base));
794
+ const titles = P.oneOf.map(branchTitle);
759
795
  steps.push((data, errors, instancePath, schemaPath, stack, rec) => {
760
796
  const scratch = errors === NOERRORS ? NOERRORS : [];
761
797
  let count = 0;
798
+ // Per-branch results, so a failing oneOf collapses to the branch that
799
+ // came closest instead of every branch's complaints at once.
800
+ const branches = errors === NOERRORS ? null : [];
762
801
  // With a record: the winner's annotations stay, every other branch's
763
802
  // are undone, and a second winner undoes everything since the node
764
803
  // fails and merges nothing.
765
804
  const pl0 = collect ? pLen(rec) : 0, n00 = collect ? rec.n : 0, xl0 = collect ? xLen(rec) : 0;
766
805
  for (let i = 0; i < fns.length; i++) {
806
+ const mark = branches === null ? 0 : scratch.length;
807
+ let ok;
767
808
  if (collect) {
768
809
  const pl = pLen(rec), n0 = rec.n, xl = xLen(rec);
769
- if (fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, rec)) count++;
810
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack, rec);
811
+ if (ok) count++;
770
812
  else undo(rec, pl, n0, xl);
771
- } else if (fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack)) count++;
813
+ } else {
814
+ ok = fns[i].c(data, scratch, instancePath, schemaPath + '/oneOf/' + i, stack);
815
+ if (ok) count++;
816
+ }
817
+ if (branches !== null) branches.push({ valid: ok, errors: scratch.slice(mark), title: titles[i] });
772
818
  }
773
819
  if (count !== 1) {
774
820
  if (collect) undo(rec, pl0, n00, xl0);
775
- if (errors !== NOERRORS) {
776
- if (count === 0) for (const e of scratch) errors.push(e);
777
- errors.push(err('oneOf', 'oneOf', instancePath, schemaPath + '/oneOf', { passingSchemas: count }, 'must match exactly one schema in oneOf'));
821
+ if (branches !== null) {
822
+ const collapsed = collapseBranches({ keyword: 'oneOf', branchResults: branches, parentPath: instancePath, parentSchemaPath: schemaPath + '/oneOf' });
823
+ if (collapsed !== null) errors.push(collapsed);
778
824
  }
779
825
  return false;
780
826
  }
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ // The string to send back to a model when its structured output failed
4
+ // validation.
5
+ //
6
+ // This exists because of a measurement rather than a preference. Feeding the
7
+ // conventional error text back into a retry loop, one model, an invoice schema,
8
+ // paired so the same failing output went to both arms:
9
+ //
10
+ // constraints the model could infer from context 40 of 40 recovered
11
+ // constraints it could not (an internal vocabulary) 0 of 30 recovered
12
+ //
13
+ // With the expected values named in the message, the second case was 30 of 30.
14
+ // The conventional message for `enum` is "must be equal to one of the allowed
15
+ // values", which names neither the values nor what arrived, so the model
16
+ // guesses. It guesses plausibly and it is always wrong: "HH" for a Hamburg
17
+ // office, "FR" for a Lyon desk. Every retry came back well formed and still
18
+ // invalid, and no number of further retries can fix it, because what is needed
19
+ // was never in the loop.
20
+ //
21
+ // ata already carries the missing halves on each error, `detail` and
22
+ // `received`. The trap is that joining `message`, which is the obvious thing to
23
+ // do, throws both away. So this is one call that does not.
24
+
25
+ function line (error) {
26
+ const where = error.instancePath || error.path || '';
27
+ const body = typeof error.detail === 'string' && error.detail ? error.detail : error.message;
28
+ // `detail` usually quotes the offending value already; saying it twice reads
29
+ // like a stutter in a string that is going into a prompt. A container is
30
+ // summarised as `[object, ~0.1KB]` rather than shown, which tells a model
31
+ // nothing it cannot see in its own output, so that is left out too.
32
+ const raw = error.received === undefined || error.received === null ? '' : String(error.received);
33
+ const useless = raw.charAt(0) === '[' && /^\[(object|array)\b/.test(raw);
34
+ const received = useless ? '' : raw;
35
+ const got = received && body && !body.includes(received) ? `, got ${received}` : '';
36
+ return `${where || '/'}: ${body}${got}`;
37
+ }
38
+
39
+ // toRetryMessage(errors, opts)
40
+ // opts.limit most errors to include (default 20). A model does not act on
41
+ // forty complaints, and the prompt is not free.
42
+ function toRetryMessage (errors, opts) {
43
+ if (!errors || typeof errors.length !== 'number' || errors.length === 0) return '';
44
+ const limit = opts && typeof opts.limit === 'number' ? opts.limit : 20;
45
+ const shown = errors.length > limit ? Array.prototype.slice.call(errors, 0, limit) : errors;
46
+ const lines = [];
47
+ for (let i = 0; i < shown.length; i++) lines.push(line(shown[i]));
48
+ if (errors.length > shown.length) lines.push(`and ${errors.length - shown.length} more`);
49
+ return lines.join('\n');
50
+ }
51
+
52
+ module.exports = { toRetryMessage };
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.19.0';
10
+ module.exports = '1.21.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.19.0",
3
+ "version": "1.21.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",
@@ -51,7 +51,7 @@
51
51
  "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",
52
52
  "build": "cmake-js build --target ata",
53
53
  "rebuild": "cmake-js rebuild --target ata",
54
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.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",
54
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_format_single_error.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.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",
55
55
  "bench:size": "node benchmark/bench_aot_size.mjs",
56
56
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
57
57
  "test:compat": "node tests/test_compat.js",
@@ -122,13 +122,13 @@
122
122
  "LICENSE"
123
123
  ],
124
124
  "optionalDependencies": {
125
- "@ata-validator/native-darwin-arm64": "1.19.0",
126
- "@ata-validator/native-darwin-x64": "1.19.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.19.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.19.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.19.0",
130
- "@ata-validator/native-linux-x64-musl": "1.19.0",
131
- "@ata-validator/native-win32-x64": "1.19.0"
125
+ "@ata-validator/native-darwin-arm64": "1.21.0",
126
+ "@ata-validator/native-darwin-x64": "1.21.0",
127
+ "@ata-validator/native-linux-arm64-gnu": "1.21.0",
128
+ "@ata-validator/native-linux-arm64-musl": "1.21.0",
129
+ "@ata-validator/native-linux-x64-gnu": "1.21.0",
130
+ "@ata-validator/native-linux-x64-musl": "1.21.0",
131
+ "@ata-validator/native-win32-x64": "1.21.0"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"