ata-validator 1.20.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.
package/index.js CHANGED
@@ -2049,6 +2049,8 @@ const { toTypeScript } = require("./lib/ts-gen");
2049
2049
  const { renderPretty } = require("./lib/render-pretty");
2050
2050
  const { renderCompact } = require("./lib/render-compact");
2051
2051
  const { toOutput } = require("./lib/output-format");
2052
+ const { toRetryMessage } = require("./lib/retry-message");
2053
+ const { describeSchema } = require("./lib/describe-schema");
2052
2054
  const { renderJSON } = require("./lib/render-json");
2053
2055
  const { suggestFor } = require("./lib/suggestions");
2054
2056
  const { reprValue } = require("./lib/enrich-error");
@@ -2279,6 +2281,8 @@ module.exports = {
2279
2281
  renderPretty,
2280
2282
  renderCompact,
2281
2283
  toOutput,
2284
+ toRetryMessage,
2285
+ describeSchema,
2282
2286
  renderJSON,
2283
2287
  attachSuggestions, // internal: used by the renderers; not public API
2284
2288
  };
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ // Schema -> the description to put in a model's prompt.
4
+ //
5
+ // Everyone hand-writes a prose description of the shape they want and keeps it
6
+ // next to a schema that enforces something slightly different. The two drift,
7
+ // quietly, because nothing checks one against the other. This derives the
8
+ // first from the second.
9
+ //
10
+ // It is not a style preference. Measured on one model, first attempt only, 30
11
+ // documents, no retry:
12
+ //
13
+ // nothing but the task 0 of 30 valid
14
+ // a field list written by hand 0 of 30
15
+ // a careful description written by hand 0 of 30
16
+ // this 23 of 30
17
+ //
18
+ // The careful hand-written one failed on exactly two fields, in all 30 cases:
19
+ // the two whose values are an internal vocabulary. A person writes "the
20
+ // settlement status, uppercase with underscores" because a person describes
21
+ // fields. Those values cannot be described, only listed, and a generator lists
22
+ // them.
23
+ //
24
+ // Scope follows lib/ts-gen.js, which walks the same shapes: properties and
25
+ // required, arrays, enum and const, oneOf/anyOf/allOf, and $ref into local
26
+ // $defs. Anything else is described as `any` rather than guessed at.
27
+
28
+ const MAX_DEPTH = 12;
29
+
30
+ function lit (v) {
31
+ try { return JSON.stringify(v); } catch (_) { return String(v); }
32
+ }
33
+
34
+ function resolveRef (schema, defs) {
35
+ const m = typeof schema.$ref === 'string' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
36
+ if (m && defs && defs[m[1]]) return defs[m[1]];
37
+ return null;
38
+ }
39
+
40
+ // The constraints worth stating to a model, in the order a reader wants them:
41
+ // what it is, then which values, then how big.
42
+ function constraintsOf (s) {
43
+ const out = [];
44
+ if (Array.isArray(s.enum)) out.push('one of ' + s.enum.map(lit).join(', '));
45
+ else if (s.const !== undefined) out.push('exactly ' + lit(s.const));
46
+ else if (s.type) out.push(Array.isArray(s.type) ? s.type.join(' or ') : s.type);
47
+
48
+ if (typeof s.format === 'string') out.push(s.format + ' format');
49
+ if (typeof s.pattern === 'string') out.push('matching ' + s.pattern);
50
+
51
+ const range = (min, max, unit) => {
52
+ if (min !== undefined && max !== undefined) out.push(`${min} to ${max}${unit}`);
53
+ else if (min !== undefined) out.push(`at least ${min}${unit}`);
54
+ else if (max !== undefined) out.push(`at most ${max}${unit}`);
55
+ };
56
+ range(s.minLength, s.maxLength, ' characters');
57
+ range(s.minimum, s.maximum, '');
58
+ range(s.minItems, s.maxItems, ' items');
59
+ if (s.exclusiveMinimum !== undefined) out.push('greater than ' + s.exclusiveMinimum);
60
+ if (s.exclusiveMaximum !== undefined) out.push('less than ' + s.exclusiveMaximum);
61
+ if (typeof s.multipleOf === 'number') out.push(multipleOfPhrase(s.multipleOf));
62
+ if (s.uniqueItems === true) out.push('all items different');
63
+ if (typeof s.description === 'string' && s.description) out.push(s.description);
64
+ return out;
65
+ }
66
+
67
+ // `multipleOf: 0.01` is how a schema says "money". A model acts on "rounded to
68
+ // 2 decimal places" and does not reliably act on "a multiple of 0.01": in the
69
+ // measurement behind this file, that one phrase was most of the gap between
70
+ // this output and a careful description written by a person.
71
+ function multipleOfPhrase (m) {
72
+ if (m > 0 && m < 1) {
73
+ const places = Math.round(Math.log10(1 / m));
74
+ if (Math.abs(Math.pow(10, -places) - m) < Number.EPSILON * 8) {
75
+ return `rounded to ${places} decimal place${places === 1 ? '' : 's'}`;
76
+ }
77
+ }
78
+ return 'a multiple of ' + m;
79
+ }
80
+
81
+ function isObjectSchema (s) {
82
+ return s && typeof s === 'object' && s.properties && typeof s.properties === 'object';
83
+ }
84
+
85
+ function describeObjectBody (schema, depth, defs, lines) {
86
+ const pad = ' '.repeat(depth);
87
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
88
+ for (const [key, sub] of Object.entries(schema.properties)) {
89
+ describeNode(sub, key + (required.has(key) ? '' : ' (optional)'), depth, defs, lines);
90
+ }
91
+ for (const key of required) {
92
+ if (!(key in schema.properties)) lines.push(`${pad}${key}: required, any`);
93
+ }
94
+ if (schema.additionalProperties === false) lines.push(`${pad}no other fields`);
95
+ }
96
+
97
+ function describeNode (schema, label, depth, defs, lines) {
98
+ const pad = ' '.repeat(depth);
99
+ if (schema === true || schema === undefined) { lines.push(`${pad}${label}: any`); return; }
100
+ if (schema === false) { lines.push(`${pad}${label}: nothing is allowed here`); return; }
101
+ if (typeof schema !== 'object' || schema === null || depth > MAX_DEPTH) {
102
+ lines.push(`${pad}${label}: any`);
103
+ return;
104
+ }
105
+
106
+ const target = schema.$ref ? resolveRef(schema, defs) : null;
107
+ if (target) { describeNode(target, label, depth, defs, lines); return; }
108
+
109
+ // allOf is the intersection, so its parts describe the same value.
110
+ if (Array.isArray(schema.allOf) && schema.allOf.length) {
111
+ const merged = Object.assign({}, schema);
112
+ delete merged.allOf;
113
+ for (const part of schema.allOf) {
114
+ const resolved = part && part.$ref ? resolveRef(part, defs) || part : part;
115
+ if (resolved && typeof resolved === 'object') Object.assign(merged, resolved, {
116
+ properties: Object.assign({}, merged.properties, resolved.properties),
117
+ required: [].concat(merged.required || [], resolved.required || []),
118
+ });
119
+ }
120
+ if (!merged.properties) delete merged.properties;
121
+ if (!merged.required || !merged.required.length) delete merged.required;
122
+ describeNode(merged, label, depth, defs, lines);
123
+ return;
124
+ }
125
+
126
+ const alternatives = schema.oneOf || schema.anyOf;
127
+ if (Array.isArray(alternatives) && alternatives.length) {
128
+ lines.push(`${pad}${label}: one of the following shapes`);
129
+ alternatives.forEach((alt, i) => describeNode(alt, `option ${i + 1}`, depth + 1, defs, lines));
130
+ return;
131
+ }
132
+
133
+ if (isObjectSchema(schema)) {
134
+ const own = constraintsOf(schema).filter((c) => c !== 'object');
135
+ lines.push(`${pad}${label}: object${own.length ? ` (${own.join(', ')})` : ''}`);
136
+ describeObjectBody(schema, depth + 1, defs, lines);
137
+ return;
138
+ }
139
+
140
+ const rawItems = schema.items;
141
+ if (schema.type === 'array' && rawItems && typeof rawItems === 'object') {
142
+ const items = rawItems.$ref ? resolveRef(rawItems, defs) || rawItems : rawItems;
143
+ const own = constraintsOf(schema).filter((c) => c !== 'array');
144
+ const bounds = own.length ? ` (${own.join(', ')})` : '';
145
+ // An item that is itself an object gets its fields listed underneath. One
146
+ // that is a scalar reads better on the same line than as a nested entry
147
+ // called "item".
148
+ if (isObjectSchema(items)) {
149
+ lines.push(`${pad}${label}: array${bounds}, each item is an object:`);
150
+ describeObjectBody(items, depth + 1, defs, lines);
151
+ return;
152
+ }
153
+ if (items.oneOf || items.anyOf || items.allOf) {
154
+ lines.push(`${pad}${label}: array${bounds}, each item is:`);
155
+ describeNode(items, 'item', depth + 1, defs, lines);
156
+ return;
157
+ }
158
+ const inner = constraintsOf(items).join(', ') || 'any';
159
+ lines.push(`${pad}${label}: array${bounds} of ${inner}`);
160
+ return;
161
+ }
162
+
163
+ lines.push(`${pad}${label}: ${constraintsOf(schema).join(', ') || 'any'}`);
164
+ }
165
+
166
+ // describeSchema(schema, opts) -> string
167
+ // opts.name what to call the top level (default 'output')
168
+ function describeSchema (schema, opts) {
169
+ const name = (opts && opts.name) || 'output';
170
+ const defs = (schema && (schema.$defs || schema.definitions)) || null;
171
+ const lines = [];
172
+ describeNode(schema, name, 0, defs, lines);
173
+ return lines.join('\n');
174
+ }
175
+
176
+ module.exports = { describeSchema };
@@ -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.20.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.20.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_error_shape_differential.js && node tests/test_output_format.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",
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.20.0",
126
- "@ata-validator/native-darwin-x64": "1.20.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.20.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.20.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.20.0",
130
- "@ata-validator/native-linux-x64-musl": "1.20.0",
131
- "@ata-validator/native-win32-x64": "1.20.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"