ata-validator 1.9.0 → 1.11.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,41 @@
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.11.0 - 2026-08-31
6
+
7
+ ### Fixed
8
+
9
+ - The verdict methods answer the same question as `validate()` again. With `coerceTypes`, `removeAdditional` or a schema `default` in play, `validate()` ran the preprocess pass and `isValidObject()`, `isValidJSON()` and `validateJSON()` did not, so the same validator answered `true` from one and `false` from another for the same document: `validate({ age: '26' })` accepted where `isValidObject({ age: '26' })` rejected. Every path now runs the same pass. Verdict methods on a validator configured this way rewrite the input in place, as `validate()` already did, and the cost of the correction is 0.5 ns on `isValidObject` and 2.7 ns on `isValidJSON`, measured interleaved; validators without those options are unchanged. `tests/test_verdict_preprocess.js` holds all four methods to the same answers.
10
+
11
+ - The native engine's error codes no longer reach callers untranslated. A type failure answered by the addon came back as `code: 3` with no `keyword` and a `docUrl` pointing at a page that does not exist, while the same failure from the JavaScript engines came back as `ATA1001`; both now report the documented code, keyword and link. `tests/test_native_error_codes.js` holds the table against the enum in `include/ata.h`, so the two cannot drift apart silently.
12
+ - The error generator declined self-referencing schemas by emitting nothing for the reference, which accepted whatever that reference guarded: `{ properties: { foo: { $ref: "#" } }, additionalProperties: false }` accepted `{ foo: { bar: false } }` on that path. It now declines the schema outright and the validator falls back to an engine that answers it correctly. The entry-point agreement test covers the shape.
13
+
14
+ - `ipv6` gave two different answers depending on which engine ran it, and neither was right. The compiled path refused an IPv4-mapped address such as `::ffff:192.168.1.1`, the interpreted path accepted `::ffff:1.2.3.4.5`, and both accepted a group of five hex digits like `12345::1`. One implementation now answers for every engine, following RFC 4291, checked against Node's own `net.isIPv6` and the suite's corpus.
15
+
16
+ - `date-time` refuses dates that do not exist. The check ran a regular expression for the shape and then handed the string to `Date.parse`, which rolls an out-of-range day into the next month, so `2026-02-30T00:00:00Z`, `2026-02-29T00:00:00Z` and `2026-12-31T24:00:00Z` were all accepted. The month, the day count for that month in that year, the clock and the offset are now checked directly, per RFC 3339. Schemas that relied on the old leniency will see those values rejected.
17
+
18
+ - Data that points back at itself no longer exhausts the stack. A document with a cycle, which JSON text cannot express but an in-memory object graph can, threw `RangeError: Maximum call stack size exceeded` on the compiled path while the interpreted engine settled on an answer, so the two engines disagreed. Both now follow the same rule: a value already being checked against a schema is a fixed point and counts as satisfied, and a cycle no longer hides a real violation elsewhere in the document. Validation runs a fast pass that only counts depth and a guarded pass that runs when that depth is exceeded, so ordinary documents pay one integer operation per recursive call. Measured interleaved on a self-referencing schema: a four-node document 22.2 to 30.6 ns, a 200-node document 2077 to 1178 ns, non-recursive schemas unchanged at 3.8 ns. `tests/test_cyclic_input.js` holds all three engines to the same answers.
19
+
20
+ ### Performance
21
+
22
+ - `ipv6` and `hostname` read the string once as well: 54.7 to 29.6 ns and 45.5 to 28.2 ns, interleaved medians. `ipv6` no longer allocates two arrays per check; `hostname` keeps the answers of the expression it replaces, fuzzed over 300k strings with 0 mismatches.
23
+
24
+ - `date-time` reads the string once, with no regular expression, no date object and no allocation: 95.0 to 39.4 ns on a valid value with a `Z`, 103.8 to 45.3 ns with a numeric offset, interleaved medians. Fuzzed against a reference that spells out RFC 3339, with 0 mismatches over 300k strings; `tests/test_formats_single_pass.js` keeps both the predicate and the generated form on it.
25
+
26
+ - A constructed Validator is roughly three times smaller on the heap until it is used. The public methods and the Standard Schema entry moved from per-instance closures built in the constructor to memoized prototype accessors, and the JSON position cache is only allocated when the JSON text path first needs it. Measured per instance on a 10-key object schema, double-gc deltas over 2000 instances: 1.61 KB to 0.43 KB with a shared schema object, 2.33 KB to 1.12 KB when each instance owns its schema, 3.93 KB to 3.30 KB once compiled and used. Construction alone went from 1504 to 855 ns; construction plus first validate pays about 0.9 microseconds more, once, because the compile step's method assignments now go through a defining setter. The hot validate() path is unchanged, measured interleaved. Detached method references (`const f = v.validate`) still work; `tests/test_lazy_instance.js` pins the shape.
27
+
28
+ ## 1.10.0 - 2026-08-30
29
+
30
+ ### Performance
31
+
32
+ - The code generator takes shapes it used to decline for no correctness reason: boolean subschemas in `items`, `properties`, `patternProperties`, `dependentSchemas`, `propertyNames`, `allOf`, `anyOf`, `not`, `contains` and `if`/`then`/`else`, recursive `#/$defs/` references as named functions, and `additionalProperties` as a schema alongside composition or `patternProperties`. Each lands in all three generators and the closure path, held to the interpreter by `tests/test_codegen_edge_shapes.js` and the entry-point agreement test over the whole official suite. Every suite group that moved off the interpreter got faster, 31 of 31 on draft 2020-12 and 28 of 28 on draft 7, summed per-group time down 70 percent. The suite-wide figure, measured interleaved against the previous release in one process, did not move outside that measurement's noise; `benchmark/verdict-bench.md` has the numbers and says why.
33
+ - `date` and `ipv4` format checks read the string once with no regular expression and no allocation: 45.7 to 14.2 ns and 54.5 to 27.1 ns on a valid value, interleaved medians. Fuzzed against the previous forms with 0 mismatches; `tests/test_formats_single_pass.js` keeps it that way.
34
+
35
+ ### Fixed
36
+
37
+ - A key matched only by the second of two `patternProperties` entries, alongside `additionalProperties: false`, was rejected: the generated key loop returned at the first pattern that missed. Found while rewriting that loop; covered by the edge-shape test.
38
+ - A declared property that also matched a `patternProperties` entry skipped the pattern's schema in the generated code when `additionalProperties` was a schema. The suite's own interaction case caught it the moment the shape was allowed to compile.
39
+
5
40
  ## 1.9.0 - 2026-08-28
6
41
 
7
42
  ### Errors
package/index.js CHANGED
@@ -15,6 +15,7 @@ const { needsNormalization } = require("./lib/schema-scan");
15
15
  const { isV1Dialect } = require("./lib/dialect");
16
16
  const { classify } = require("./lib/shape-classifier");
17
17
  const { buildTier0Plan, tier0Validate } = require("./lib/tier0");
18
+ const { createCache: _createPosCache } = require("./lib/data-position-cache");
18
19
 
19
20
  // Extract default values from a schema tree. Returns a function that applies
20
21
  // defaults to an object in-place (mutates), or null if no defaults exist.
@@ -788,89 +789,15 @@ class Validator {
788
789
  // Per-validate data position cache. Populated by validateJSON before
789
790
  // dispatching to inner validate(); consulted by the rich-error wrap
790
791
  // to attach dataFrame entries to each enriched error.
791
- this._posCache = require('./lib/data-position-cache').createCache();
792
+ this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
792
793
  this._lastRawInput = null;
793
794
 
794
- // Lazy stubs: trigger compilation on first call, then re-dispatch
795
- this.validate = (data) => {
796
- this._ensureCompiled();
797
- return this.validate(data);
798
- };
799
- this.isValidObject = (data) => {
800
- // Lazy: classify + build tier 0 plan on first call, not in constructor.
801
- const _tier = classify(this._schemaObj);
802
- if (_tier.tier === 0) {
803
- const _plan = buildTier0Plan(this._schemaObj);
804
- let _n = 0;
805
- this.isValidObject = (d) => {
806
- const r = tier0Validate(_plan, d);
807
- if (++_n === 2) {
808
- try { this._ensureCodegen(); } catch {}
809
- }
810
- return r;
811
- };
812
- } else {
813
- this._ensureCodegen();
814
- // Codegen can bail on shapes it cannot represent; the full compile
815
- // binds the native path or the unsupported thrower instead of
816
- // leaving this stub to re-dispatch to itself.
817
- if (!this._jsFn) this._ensureCompiled();
818
- }
819
- return this.isValidObject(data);
820
- };
821
- this.validateJSON = (jsonStr) => {
822
- this._ensureCompiled();
823
- return this.validateJSON(jsonStr);
824
- };
825
- this.isValidJSON = (jsonStr) => {
826
- this._ensureCompiled();
827
- return this.isValidJSON(jsonStr);
828
- };
829
- this.validateAndParse = (jsonStr) => {
830
- if (!native) throw new Error('Native addon required for validateAndParse()');
831
- this._ensureCompiled();
832
- return this.validateAndParse(jsonStr);
833
- };
834
- this.isValid = (buf) => {
835
- if (!native) throw new Error('Native addon required for isValid() — use validate() or isValidObject() instead');
836
- this._ensureCompiled();
837
- return this.isValid(buf);
838
- };
839
- this.countValid = (ndjsonBuf) => {
840
- if (!native) throw new Error('Native addon required for countValid()');
841
- this._ensureCompiled();
842
- return this.countValid(ndjsonBuf);
843
- };
844
- this.batchIsValid = (buffers) => {
845
- if (!native) throw new Error('Native addon required for batchIsValid()');
846
- this._ensureCompiled();
847
- return this.batchIsValid(buffers);
848
- };
795
+ // Public methods start as memoized accessors on the prototype; nothing is
796
+ // allocated per instance until one is first read. See _defineLazyMethod
797
+ // below the class.
849
798
 
850
- // ~standard uses self.validate() -- works with lazy because it goes through
851
- // the instance property which gets swapped after compilation
852
- const self = this;
853
- Object.defineProperty(this, "~standard", {
854
- value: Object.freeze({
855
- version: 1,
856
- vendor: "ata-validator",
857
- validate(value) {
858
- const result = self.validate(value);
859
- if (result.valid) {
860
- return { value };
861
- }
862
- return {
863
- issues: result.errors.map((err) => ({
864
- message: err.message,
865
- path: parsePointerPath(err.instancePath),
866
- })),
867
- };
868
- },
869
- }),
870
- writable: false,
871
- enumerable: false,
872
- configurable: false,
873
- });
799
+ // "~standard" (Standard Schema V1) is a lazy prototype accessor too;
800
+ // see below the class. Consumers only pay for it if they read it.
874
801
 
875
802
  // Populate identity cache so repeated `new Validator(sameSchema)` short-circuits.
876
803
  if (!opts && typeof schema === "object" && schema !== null) {
@@ -882,6 +809,22 @@ class Validator {
882
809
  // meta-schema, which addSchema() may only have registered just now. Run once,
883
810
  // before anything reads the schema, and before `_schemaStr` is computed from
884
811
  // it. After this addSchema() is refused, so the answer cannot go stale.
812
+ // Whether validation is preceded by a pass that rewrites the input:
813
+ // coercion, removal of undeclared keys, or filling in defaults. The verdict
814
+ // methods have to take the same path when it is, so the quick bindings that
815
+ // answer from the compiled function alone are not used for these validators.
816
+ _needsPreprocess() {
817
+ const o = this._options;
818
+ if (o.coerceTypes || o.removeAdditional) return true;
819
+ if (o.useDefaults === false) return false;
820
+ if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
821
+ return this._schemaStr.includes('"default"');
822
+ }
823
+
824
+ _pos() {
825
+ return this._posCache || (this._posCache = _createPosCache());
826
+ }
827
+
885
828
  _ensureVocabularies() {
886
829
  if (this._vocabulariesApplied) return;
887
830
  this._vocabulariesApplied = true;
@@ -1180,14 +1123,26 @@ class Validator {
1180
1123
  return result;
1181
1124
  };
1182
1125
  }
1183
- this.isValidObject = jsFn;
1126
+ // The verdict methods answer validate()'s question without building the
1127
+ // error list, so they run the same preprocess pass. Skipping it made the
1128
+ // two disagree on input that coercion or a default would have fixed.
1129
+ this.isValidObject = preprocess
1130
+ ? (data) => { preprocess(data); return jsFn(data) }
1131
+ : jsFn;
1184
1132
  const hybridFn = jsFn._hybridFactory
1185
1133
  ? jsFn._hybridFactory(VALID_RESULT, errFn)
1186
1134
  : null;
1187
- const jsonValidateFn = safeCombinedFn
1135
+ const jsonValidateInner = safeCombinedFn
1188
1136
  || hybridFn
1189
1137
  || ((obj) => (jsFn(obj) ? VALID_RESULT : errFn(obj)));
1190
- this.validateJSON = useSimdjsonForLarge && native
1138
+ // Parsed text takes the same preprocess pass as a parsed object, so
1139
+ // validate(obj) and validateJSON(text) answer the same for the same
1140
+ // document. Without it, coercion, removal and defaults applied on one
1141
+ // path and not the other.
1142
+ const jsonValidateFn = preprocess
1143
+ ? (obj) => { preprocess(obj); return jsonValidateInner(obj) }
1144
+ : jsonValidateInner;
1145
+ this.validateJSON = useSimdjsonForLarge && native && !preprocess
1191
1146
  ? (jsonStr) => {
1192
1147
  if (jsonStr.length >= SIMDJSON_THRESHOLD) {
1193
1148
  this._ensureNative();
@@ -1214,7 +1169,22 @@ class Validator {
1214
1169
  this._ensureNative();
1215
1170
  return this._compiled.validateJSON(jsonStr);
1216
1171
  };
1217
- this.isValidJSON = useSimdjsonForLarge && native
1172
+ // The addon validates the bytes as they are, which is the wrong answer
1173
+ // when the schema asks for coercion, removal or defaults: those change
1174
+ // what counts as valid. With a preprocess pass configured the text is
1175
+ // parsed and run through the same path validate() takes.
1176
+ const verdictFromText = (jsonStr) => {
1177
+ let parsed;
1178
+ try {
1179
+ parsed = JSON.parse(jsonStr);
1180
+ } catch (e) {
1181
+ if (!(e instanceof SyntaxError)) throw e;
1182
+ return false;
1183
+ }
1184
+ if (preprocess) preprocess(parsed);
1185
+ return jsFn(parsed);
1186
+ };
1187
+ this.isValidJSON = useSimdjsonForLarge && native && !preprocess
1218
1188
  ? (jsonStr) => {
1219
1189
  if (jsonStr.length >= SIMDJSON_THRESHOLD) {
1220
1190
  this._ensureNative();
@@ -1223,21 +1193,9 @@ class Validator {
1223
1193
  Buffer.from(jsonStr),
1224
1194
  );
1225
1195
  }
1226
- try {
1227
- return jsFn(JSON.parse(jsonStr));
1228
- } catch (e) {
1229
- if (!(e instanceof SyntaxError)) throw e;
1230
- return false;
1231
- }
1196
+ return verdictFromText(jsonStr);
1232
1197
  }
1233
- : (jsonStr) => {
1234
- try {
1235
- return jsFn(JSON.parse(jsonStr));
1236
- } catch (e) {
1237
- if (!(e instanceof SyntaxError)) throw e;
1238
- return false;
1239
- }
1240
- };
1198
+ : verdictFromText;
1241
1199
  // validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
1242
1200
  // validate) so it works with or without the native addon and in browsers.
1243
1201
  {
@@ -1416,7 +1374,7 @@ class Validator {
1416
1374
  if (result && result.valid === false && result !== ABORT_EARLY_RESULT) {
1417
1375
  // Positions come from the raw input when validateJSON set one;
1418
1376
  // resolved eagerly since the cache is reset per call.
1419
- const positions = (enrich && self._lastRawInput != null) ? self._posCache.get(self._lastRawInput) : null;
1377
+ const positions = (enrich && self._lastRawInput != null) ? self._pos().get(self._lastRawInput) : null;
1420
1378
  if (positions) self._posCache.reset();
1421
1379
  let cached = null;
1422
1380
  return {
@@ -1481,7 +1439,7 @@ class Validator {
1481
1439
  let parsedData;
1482
1440
  try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
1483
1441
  if (!first || !first.docUrl) {
1484
- const positions = (this._lastRawInput != null) ? this._posCache.get(this._lastRawInput) : null;
1442
+ const positions = (this._lastRawInput != null) ? this._pos().get(this._lastRawInput) : null;
1485
1443
  const enriched = result.errors.map((e) => enrich(e, {
1486
1444
  data: parsedData,
1487
1445
  positions,
@@ -1501,7 +1459,7 @@ class Validator {
1501
1459
  return { valid: false, errors: enriched };
1502
1460
  }
1503
1461
  // Already-enriched path: still attach dataFrame if missing.
1504
- const positions = (this._lastRawInput != null) ? this._posCache.get(this._lastRawInput) : null;
1462
+ const positions = (this._lastRawInput != null) ? this._pos().get(this._lastRawInput) : null;
1505
1463
  if (positions) {
1506
1464
  for (const e of result.errors) {
1507
1465
  if (e && !e.dataFrame) {
@@ -1673,6 +1631,13 @@ class Validator {
1673
1631
 
1674
1632
  _ensureCodegen() {
1675
1633
  if (this._jsFn) return;
1634
+ // A validator that rewrites its input cannot use the binding below: that
1635
+ // one answers from the compiled function alone and would skip the rewrite,
1636
+ // so isValidObject() and validate() would disagree.
1637
+ if (this._needsPreprocess()) {
1638
+ this._ensureCompiled();
1639
+ return;
1640
+ }
1676
1641
  this._ensureVocabularies();
1677
1642
  if (typeof process !== 'undefined' && process.env && process.env.ATA_FORCE_NAPI) return;
1678
1643
  if (!this._schemaStr) this._schemaStr = JSON.stringify(this._schemaObj);
@@ -2032,6 +1997,120 @@ function defineSchema (schema) {
2032
1997
  return schema;
2033
1998
  }
2034
1999
 
2000
+ // Public methods start as memoized accessors on the prototype. A fresh
2001
+ // Validator allocates none of them; the first read of a method builds the
2002
+ // bound closure, stores it on the instance as an ordinary writable property
2003
+ // and returns it. The setter keeps the compile step's plain assignments
2004
+ // (`this.validate = fn`) working before the getter has ever run. Detached
2005
+ // use (`const f = v.validate`) keeps working because the closure binds the
2006
+ // instance.
2007
+ // Standard Schema V1. Built on first read, then pinned to the instance with
2008
+ // the same descriptor the constructor used to install eagerly.
2009
+ Object.defineProperty(Validator.prototype, "~standard", {
2010
+ configurable: true,
2011
+ get() {
2012
+ const self = this;
2013
+ const std = Object.freeze({
2014
+ version: 1,
2015
+ vendor: "ata-validator",
2016
+ validate(value) {
2017
+ const result = self.validate(value);
2018
+ if (result.valid) {
2019
+ return { value };
2020
+ }
2021
+ return {
2022
+ issues: result.errors.map((err) => ({
2023
+ message: err.message,
2024
+ path: parsePointerPath(err.instancePath),
2025
+ })),
2026
+ };
2027
+ },
2028
+ });
2029
+ Object.defineProperty(this, "~standard", {
2030
+ value: std,
2031
+ writable: false,
2032
+ enumerable: false,
2033
+ configurable: false,
2034
+ });
2035
+ return std;
2036
+ },
2037
+ });
2038
+
2039
+ function _defineLazyMethod(name, maker) {
2040
+ Object.defineProperty(Validator.prototype, name, {
2041
+ configurable: true,
2042
+ get() {
2043
+ const fn = maker(this);
2044
+ Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
2045
+ return fn;
2046
+ },
2047
+ set(fn) {
2048
+ Object.defineProperty(this, name, { value: fn, writable: true, configurable: true, enumerable: true });
2049
+ },
2050
+ });
2051
+ }
2052
+
2053
+ _defineLazyMethod('validate', (self) => (data) => {
2054
+ self._ensureCompiled();
2055
+ return self.validate(data);
2056
+ });
2057
+ _defineLazyMethod('isValidObject', (self) => (data) => {
2058
+ // A validator that rewrites its input goes through the full compile, which
2059
+ // binds a verdict method that runs the rewrite first.
2060
+ if (self._needsPreprocess()) {
2061
+ self._ensureCompiled();
2062
+ return self.isValidObject(data);
2063
+ }
2064
+ // Lazy: classify + build tier 0 plan on first call, not in constructor.
2065
+ const _tier = classify(self._schemaObj);
2066
+ if (_tier.tier === 0) {
2067
+ const _plan = buildTier0Plan(self._schemaObj);
2068
+ let _n = 0;
2069
+ self.isValidObject = (d) => {
2070
+ const r = tier0Validate(_plan, d);
2071
+ if (++_n === 2) {
2072
+ try { self._ensureCodegen(); } catch {}
2073
+ }
2074
+ return r;
2075
+ };
2076
+ } else {
2077
+ self._ensureCodegen();
2078
+ // Codegen can bail on shapes it cannot represent; the full compile
2079
+ // binds the native path or the unsupported thrower instead of
2080
+ // leaving this stub to re-dispatch to itself.
2081
+ if (!self._jsFn) self._ensureCompiled();
2082
+ }
2083
+ return self.isValidObject(data);
2084
+ });
2085
+ _defineLazyMethod('validateJSON', (self) => (jsonStr) => {
2086
+ self._ensureCompiled();
2087
+ return self.validateJSON(jsonStr);
2088
+ });
2089
+ _defineLazyMethod('isValidJSON', (self) => (jsonStr) => {
2090
+ self._ensureCompiled();
2091
+ return self.isValidJSON(jsonStr);
2092
+ });
2093
+ _defineLazyMethod('validateAndParse', (self) => (jsonStr) => {
2094
+ if (!native) throw new Error('Native addon required for validateAndParse()');
2095
+ self._ensureCompiled();
2096
+ return self.validateAndParse(jsonStr);
2097
+ });
2098
+ _defineLazyMethod('isValid', (self) => (buf) => {
2099
+ if (!native) throw new Error('Native addon required for isValid() — use validate() or isValidObject() instead');
2100
+ self._ensureCompiled();
2101
+ return self.isValid(buf);
2102
+ });
2103
+ _defineLazyMethod('countValid', (self) => (ndjsonBuf) => {
2104
+ if (!native) throw new Error('Native addon required for countValid()');
2105
+ self._ensureCompiled();
2106
+ return self.countValid(ndjsonBuf);
2107
+ });
2108
+ _defineLazyMethod('batchIsValid', (self) => (buffers) => {
2109
+ if (!native) throw new Error('Native addon required for batchIsValid()');
2110
+ self._ensureCompiled();
2111
+ return self.batchIsValid(buffers);
2112
+ });
2113
+
2035
2114
  module.exports = {
2036
2115
  Validator,
2037
2116
  compile,
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { CODES, codeFor } = require('./error-codes');
3
+ const { CODES, codeFor, fromNative } = require('./error-codes');
4
4
  const { suggestFor } = require('./suggestions');
5
5
 
6
6
  const DOC_BASE = 'https://ata-validator.com/e/';
@@ -130,12 +130,19 @@ function rankFor (keyword) {
130
130
  function enrich (rawErr, opts) {
131
131
  const data = opts && opts.data;
132
132
  const positions = opts && opts.positions;
133
- const keyword = rawErr.keyword;
134
133
  const format = rawErr.params && rawErr.params.format;
134
+ // The native engine reports its own enum as a number. Translate it to the
135
+ // keyword and public code the JavaScript engines use, so an error means the
136
+ // same thing whichever engine produced it.
137
+ const fromAddon = typeof rawErr.code === 'number' ? fromNative(rawErr.code, format) : null;
138
+ const keyword = rawErr.keyword || (fromAddon && fromAddon.keyword);
135
139
  // Prefer a code the codegen already attached (e.g. branch-collapse emits
136
140
  // ATA4001/4002/4003 distinguishing zero/multi/anyOf failure modes). The
137
141
  // keyword-derived lookup only finds the first match for `keyword: 'oneOf'`.
138
- const code = rawErr.code || codeFor(keyword, format) || 'ATA9001';
142
+ const code = (fromAddon && fromAddon.code) ||
143
+ (typeof rawErr.code === 'string' && rawErr.code) ||
144
+ codeFor(keyword, format) ||
145
+ 'ATA9001';
139
146
  const meta = CODES[code];
140
147
  const path = rawErr.instancePath != null ? rawErr.instancePath : (rawErr.path || '');
141
148
 
@@ -100,4 +100,57 @@ function codeFor (keyword, format) {
100
100
  return hit === undefined ? null : hit;
101
101
  }
102
102
 
103
- module.exports = { CODES, get, all, codeFor };
103
+
104
+ // The native engine reports its own `error_code` enum from include/ata.h as an
105
+ // ordinal. Nothing outside the addon should ever see one: an error that came
106
+ // from there carries the same public code and keyword as the same failure from
107
+ // the JavaScript engines. Ordinals are positional, so this table follows the
108
+ // enum's declaration order and `tests/test_native_error_codes.js` fails if the
109
+ // two drift apart.
110
+ const NATIVE_KEYWORDS = [
111
+ null, // 0 ok
112
+ '__parse__', // 1 invalid_json
113
+ '__compile__', // 2 invalid_schema
114
+ 'type', // 3 type_mismatch
115
+ 'required', // 4 required_property_missing
116
+ 'additionalProperties', // 5 additional_property_not_allowed
117
+ 'enum', // 6 enum_mismatch
118
+ 'const', // 7 const_mismatch
119
+ 'minimum', // 8 minimum_violation
120
+ 'maximum', // 9 maximum_violation
121
+ 'exclusiveMinimum', // 10 exclusive_minimum_violation
122
+ 'exclusiveMaximum', // 11 exclusive_maximum_violation
123
+ 'minLength', // 12 min_length_violation
124
+ 'maxLength', // 13 max_length_violation
125
+ 'pattern', // 14 pattern_mismatch
126
+ 'format', // 15 format_mismatch
127
+ 'minItems', // 16 min_items_violation
128
+ 'maxItems', // 17 max_items_violation
129
+ 'uniqueItems', // 18 unique_items_violation
130
+ 'minProperties', // 19 min_properties_violation
131
+ 'maxProperties', // 20 max_properties_violation
132
+ 'multipleOf', // 21 multiple_of_violation
133
+ 'allOf', // 22 all_of_failed
134
+ 'anyOf', // 23 any_of_failed
135
+ 'oneOf', // 24 one_of_failed
136
+ 'not', // 25 not_failed
137
+ '$ref', // 26 ref_not_found
138
+ 'if', // 27 if_then_else_failed
139
+ ]
140
+
141
+ // Keyword and public code for a native ordinal, or null when the ordinal is
142
+ // outside the enum, which would mean the addon is newer than this table.
143
+ function fromNative (ordinal, format) {
144
+ if (typeof ordinal !== 'number' || !Number.isInteger(ordinal)) return null
145
+ const keyword = NATIVE_KEYWORDS[ordinal]
146
+ if (!keyword) return null
147
+ // codeFor('format') answers with the first format code, which names a
148
+ // specific format. Without knowing which format failed, the generic one is
149
+ // the honest answer.
150
+ const code = keyword === 'format'
151
+ ? (format ? codeFor('format', format) || 'ATA3099' : 'ATA3099')
152
+ : codeFor(keyword) || 'ATA9001'
153
+ return { keyword, code }
154
+ }
155
+
156
+ module.exports = { CODES, get, all, codeFor, fromNative, NATIVE_KEYWORDS };