ata-validator 1.18.0 → 1.19.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
@@ -10,7 +10,7 @@ const {
10
10
  compileToJSCodegenWithErrors,
11
11
  compileToJSCombined,
12
12
  } = require("./lib/js-compiler");
13
- const { normalizeDraft7, normalizeNullable, stripFormatAssertions } = require("./lib/draft7");
13
+ const { normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./lib/draft7");
14
14
  const { enabledKeywords, stripDisabledKeywords } = require("./lib/vocabularies");
15
15
  const { needsNormalization } = require("./lib/schema-scan");
16
16
  const { isV1Dialect } = require("./lib/dialect");
@@ -618,6 +618,7 @@ function _normalizeCallerSchema(s, inheritDraft7) {
618
618
  const copy = _deepCloneWithSymbols(s)
619
619
  if (needsDraft7) normalizeDraft7(copy, true)
620
620
  normalizeNullable(copy)
621
+ normalizeExclusiveBounds(copy)
621
622
  // Return original when normalization produced no change, copy otherwise.
622
623
  // Kept even though the walk has already said there is work, so that a walk
623
624
  // which over-reports still returns exactly what it returned before.
package/lib/draft7.js CHANGED
@@ -105,6 +105,50 @@ function _normalize(schema) {
105
105
  // 'null' (`{ type: 'X', nullable: true }` -> `{ type: ['X', 'null'] }`), the
106
106
  // same shape AJV produces under its `nullable` option. Recurses only through
107
107
  // schema-bearing keywords so it never touches data values (default, const, etc.).
108
+
109
+ // draft-04 spelled the exclusive bounds as booleans modifying `minimum` and
110
+ // `maximum`; every dialect since spells them as the bound itself. The engines
111
+ // read the numeric form, and the codegen path compared the value against the
112
+ // boolean directly, so `true` became 1: a schema saying "below 5, exclusive"
113
+ // rejected 1 and a schema saying "above 3, exclusive" accepted 3. Rewriting
114
+ // the pair here gives every engine the same numeric keyword, which is also
115
+ // what makes the two agree.
116
+ function normalizeExclusiveBounds(schema) {
117
+ _walkExclusive(schema, new Set())
118
+ return schema
119
+ }
120
+
121
+ function _walkExclusive(node, seen) {
122
+ if (typeof node !== 'object' || node === null) return
123
+ if (Array.isArray(node)) {
124
+ for (const item of node) _walkExclusive(item, seen)
125
+ return
126
+ }
127
+ if (seen.has(node)) return
128
+ seen.add(node)
129
+
130
+ if (typeof node.exclusiveMinimum === 'boolean') {
131
+ if (node.exclusiveMinimum === true && typeof node.minimum === 'number') {
132
+ node.exclusiveMinimum = node.minimum
133
+ delete node.minimum
134
+ } else {
135
+ // `false` restates the inclusive bound, and a boolean with no bound
136
+ // beside it says nothing at all.
137
+ delete node.exclusiveMinimum
138
+ }
139
+ }
140
+ if (typeof node.exclusiveMaximum === 'boolean') {
141
+ if (node.exclusiveMaximum === true && typeof node.maximum === 'number') {
142
+ node.exclusiveMaximum = node.maximum
143
+ delete node.maximum
144
+ } else {
145
+ delete node.exclusiveMaximum
146
+ }
147
+ }
148
+
149
+ for (const key of Object.keys(node)) _walkExclusive(node[key], seen)
150
+ }
151
+
108
152
  function normalizeNullable(schema) {
109
153
  if (typeof schema !== 'object' || schema === null) return schema
110
154
  _normalizeNullable(schema)
@@ -190,4 +234,4 @@ function _stripFormat(schema, seen) {
190
234
  }
191
235
  }
192
236
 
193
- module.exports = { isDraft7, normalizeDraft7, normalizeNullable, stripFormatAssertions }
237
+ module.exports = { isDraft7, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions }
@@ -523,7 +523,7 @@ function evalLeafV(P, data) {
523
523
  if (!found) return false;
524
524
  }
525
525
  if (P.hasConst && !deepEqual(P.const, data)) return false;
526
- if (P.hasNumber && typeof data === 'number') {
526
+ if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
527
527
  if (P.minimum !== undefined && !(data >= P.minimum)) return false;
528
528
  if (P.maximum !== undefined && !(data <= P.maximum)) return false;
529
529
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) return false;
@@ -640,7 +640,7 @@ function evalLeaf(P, data, errors, instancePath, schemaPath) {
640
640
  if (errors !== NOERRORS) errors.push(err('const', 'const', instancePath, schemaPath + '/const', { allowedValue: P.const }, 'must be equal to constant'));
641
641
  valid = false;
642
642
  }
643
- if (P.hasNumber && typeof data === 'number') {
643
+ if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
644
644
  if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
645
645
  if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
646
646
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
@@ -931,7 +931,7 @@ class Interpreter {
931
931
  }
932
932
 
933
933
  // numbers
934
- if (P.hasNumber && typeof data === 'number') {
934
+ if (P.hasNumber && typeof data === 'number' && isFinite(data)) {
935
935
  if (P.minimum !== undefined && !(data >= P.minimum)) { errors === NOERRORS || errors.push(err('minimum', 'minimum', instancePath, schemaPath + '/minimum', { comparison: '>=', limit: P.minimum }, `must be >= ${P.minimum}`)); valid = false; }
936
936
  if (P.maximum !== undefined && !(data <= P.maximum)) { errors === NOERRORS || errors.push(err('maximum', 'maximum', instancePath, schemaPath + '/maximum', { comparison: '<=', limit: P.maximum }, `must be <= ${P.maximum}`)); valid = false; }
937
937
  if (P.exclusiveMinimum !== undefined && !(data > P.exclusiveMinimum)) { errors === NOERRORS || errors.push(err('exclusiveMinimum', 'exclusiveMinimum', instancePath, schemaPath + '/exclusiveMinimum', { comparison: '>', limit: P.exclusiveMinimum }, `must be > ${P.exclusiveMinimum}`)); valid = false; }
@@ -404,29 +404,33 @@ function compileToJS(schema, defs, schemaMap) {
404
404
  })
405
405
  }
406
406
 
407
- // numeric
407
+ // numeric. A non-finite number carries no type here, so an empty schema
408
+ // accepts it and these must let it through for the same reason: otherwise
409
+ // `{ minimum: 3 }` turns away a value `{}` admits, and the two engines
410
+ // answer differently for it.
408
411
  if (schema.minimum !== undefined) {
409
412
  const min = schema.minimum
410
- checks.push((d) => typeof d !== 'number' || d >= min)
413
+ checks.push((d) => typeof d !== 'number' || !isFinite(d) || d >= min)
411
414
  }
412
415
  if (schema.maximum !== undefined) {
413
416
  const max = schema.maximum
414
- checks.push((d) => typeof d !== 'number' || d <= max)
417
+ checks.push((d) => typeof d !== 'number' || !isFinite(d) || d <= max)
415
418
  }
416
419
  if (schema.exclusiveMinimum !== undefined) {
417
420
  const min = schema.exclusiveMinimum
418
- checks.push((d) => typeof d !== 'number' || d > min)
421
+ checks.push((d) => typeof d !== 'number' || !isFinite(d) || d > min)
419
422
  }
420
423
  if (schema.exclusiveMaximum !== undefined) {
421
424
  const max = schema.exclusiveMaximum
422
- checks.push((d) => typeof d !== 'number' || d < max)
425
+ checks.push((d) => typeof d !== 'number' || !isFinite(d) || d < max)
423
426
  }
424
427
  if (schema.multipleOf !== undefined) {
425
428
  const div = schema.multipleOf
426
429
  checks.push((d) => {
427
- if (typeof d !== 'number') return true
428
- const r = d % div
429
- return Math.abs(r) <= 1e-8 || Math.abs(r - div) <= 1e-8
430
+ if (typeof d !== 'number' || !isFinite(d)) return true
431
+ if (div === 0) return false
432
+ const q = d / div
433
+ return Number.isInteger(q) || Math.abs(q - Math.round(q)) < 1e-9
430
434
  })
431
435
  }
432
436
 
@@ -750,6 +754,21 @@ function buildTypeCheck(types) {
750
754
  }
751
755
  }
752
756
 
757
+ // `multipleOf` as the interpreter reads it: divide and ask whether the
758
+ // quotient is a whole number. The remainder form this replaces answered -1 %
759
+ // 0.1 with -0.09999999999999995, which is neither near zero nor near the
760
+ // divisor, so it called -1 not a multiple of 0.1 when -1 / 0.1 is exactly -10.
761
+ // Emitted from one place because the expression stood in six.
762
+ function multipleOfBad(v, m) {
763
+ if (m === 0) return 'true'
764
+ // Negated rather than written as "the distance is too large": a quotient
765
+ // that overflows to Infinity makes the distance NaN, and every comparison
766
+ // with NaN is false, so the direct form called 1e308 a multiple of
767
+ // 0.123456789. Asking whether it is good and negating keeps that case a
768
+ // rejection, which is what the suite wants and what the closure form does.
769
+ return `!(Number.isInteger(${v}/${m})||Math.abs(${v}/${m}-Math.round(${v}/${m}))<1e-9)`
770
+ }
771
+
753
772
  const TYPE_CHECKS = {
754
773
  string: (d) => typeof d === 'string',
755
774
  number: (d) => typeof d === 'number' && isFinite(d),
@@ -768,12 +787,30 @@ function _canonical(x) {
768
787
  }
769
788
 
770
789
  const _formats = require('./formats')
790
+ // The closure path's formats. It carried its own idea of an email and knew
791
+ // five formats in all, so a schema that reached it was told "valid" for
792
+ // anything the other engines turned away; the first call on a fresh validator
793
+ // is the one that lands here. Every entry now names the same function the
794
+ // other two paths use, and the list is the list they implement.
771
795
  const FORMAT_CHECKS = {
772
- email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
796
+ email: _formats.email,
797
+ 'idn-email': _formats.idnEmail,
773
798
  date: _formats.date,
799
+ 'date-time': _formats.dateTime,
800
+ time: _formats.time,
801
+ duration: _formats.duration,
774
802
  uuid: _formats.uuid,
803
+ uri: _formats.uri,
804
+ 'uri-reference': _formats.uriReference,
805
+ 'uri-template': _formats.uriTemplate,
806
+ iri: _formats.iri,
807
+ 'iri-reference': _formats.iriReference,
775
808
  ipv4: _formats.ipv4,
809
+ ipv6: _formats.ipv6,
776
810
  hostname: _formats.hostname,
811
+ 'json-pointer': _formats.jsonPointer,
812
+ 'relative-json-pointer': _formats.relativeJsonPointer,
813
+ regex: (s) => { try { new RegExp(s, 'u'); return true } catch { return false } },
777
814
  }
778
815
 
779
816
  // Dangerous JS property names that exist on Object.prototype
@@ -1685,7 +1722,7 @@ function tryGenCombined(schema, access, ctx) {
1685
1722
  if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
1686
1723
  if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
1687
1724
  if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
1688
- if (schema.multipleOf !== undefined) conds.push(`(Math.abs(_v%${schema.multipleOf})>1e-8&&Math.abs(_v%${schema.multipleOf}-${schema.multipleOf})>1e-8)`)
1725
+ if (schema.multipleOf !== undefined) conds.push(`(${multipleOfBad('_v', schema.multipleOf)})`)
1689
1726
  if (conds.length < 2) return null
1690
1727
  return bind(conds)
1691
1728
  }
@@ -1696,7 +1733,7 @@ function tryGenCombined(schema, access, ctx) {
1696
1733
  if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
1697
1734
  if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
1698
1735
  if (schema.exclusiveMaximum !== undefined) conds.push(`_v>=${schema.exclusiveMaximum}`)
1699
- if (schema.multipleOf !== undefined) conds.push(`(Math.abs(_v%${schema.multipleOf})>1e-8&&Math.abs(_v%${schema.multipleOf}-${schema.multipleOf})>1e-8)`)
1736
+ if (schema.multipleOf !== undefined) conds.push(`(${multipleOfBad('_v', schema.multipleOf)})`)
1700
1737
  if (conds.length < 2) return null
1701
1738
  return bind(conds)
1702
1739
  }
@@ -1956,14 +1993,19 @@ function genCode(schema, v, lines, ctx, knownType) {
1956
1993
  }
1957
1994
 
1958
1995
  // numeric — skip type guard if known numeric
1959
- if (schema.minimum !== undefined) lines.push(isNum ? `if(${v}<${schema.minimum})return false` : `if(typeof ${v}==='number'&&${v}<${schema.minimum})return false`)
1960
- if (schema.maximum !== undefined) lines.push(isNum ? `if(${v}>${schema.maximum})return false` : `if(typeof ${v}==='number'&&${v}>${schema.maximum})return false`)
1961
- if (schema.exclusiveMinimum !== undefined) lines.push(isNum ? `if(${v}<=${schema.exclusiveMinimum})return false` : `if(typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum})return false`)
1962
- if (schema.exclusiveMaximum !== undefined) lines.push(isNum ? `if(${v}>=${schema.exclusiveMaximum})return false` : `if(typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum})return false`)
1996
+ // Without `type: number` beside it, a numeric keyword has to let a
1997
+ // non-finite value through: it carries no type, an empty schema accepts it,
1998
+ // and the interpreted engine reads it the same way. With `type: number` the
1999
+ // type check has already turned it away, so the guard is only needed here.
2000
+ const numGuard = isNum ? '' : `typeof ${v}==='number'&&isFinite(${v})&&`
2001
+ if (schema.minimum !== undefined) lines.push(`if(${numGuard}${v}<${schema.minimum})return false`)
2002
+ if (schema.maximum !== undefined) lines.push(`if(${numGuard}${v}>${schema.maximum})return false`)
2003
+ if (schema.exclusiveMinimum !== undefined) lines.push(`if(${numGuard}${v}<=${schema.exclusiveMinimum})return false`)
2004
+ if (schema.exclusiveMaximum !== undefined) lines.push(`if(${numGuard}${v}>=${schema.exclusiveMaximum})return false`)
1963
2005
  if (schema.multipleOf !== undefined) {
1964
2006
  const m = schema.multipleOf
1965
- const bad = `(Math.abs(${v}%${m})>1e-8&&Math.abs(${v}%${m}-${m})>1e-8)`
1966
- lines.push(isNum ? `if${bad}return false` : `if(typeof ${v}==='number'&&${bad})return false`)
2007
+ const bad = `(${multipleOfBad(v, m)})`
2008
+ lines.push(`if(${numGuard}${bad})return false`)
1967
2009
  }
1968
2010
 
1969
2011
  // string length — skip type guard if known string.
@@ -3514,7 +3556,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3514
3556
  const m = schema.multipleOf
3515
3557
  const ci = ctx.varCounter++
3516
3558
  // Use tolerance-based check for floating point (matches C++ behavior)
3517
- lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
3559
+ lines.push(`{if(typeof ${v}==='number'&&${multipleOfBad(v, m)}){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
3518
3560
  }
3519
3561
 
3520
3562
  // string length — same s.length fast paths as the boolean codegen above.
@@ -4174,7 +4216,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4174
4216
  if (schema.multipleOf !== undefined) {
4175
4217
  const m = schema.multipleOf
4176
4218
  const ci = ctx.varCounter++
4177
- lines.push(`{const _r${ci}=typeof ${v}==='number'?${v}%${m}:NaN;if(typeof ${v}==='number'&&Math.abs(_r${ci})>1e-8&&Math.abs(_r${ci}-${m})>1e-8){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
4219
+ lines.push(`{if(typeof ${v}==='number'&&${multipleOfBad(v, m)}){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
4178
4220
  }
4179
4221
 
4180
4222
  // string length — s.length fast paths, _cpLen only in the uncertain band.
@@ -173,7 +173,11 @@ function install(deps) {
173
173
  if (P.exclusiveMaximum !== undefined) { const m = P.exclusiveMaximum; nums.push((d) => d < m); }
174
174
  if (P.multipleOf !== undefined) { const m = P.multipleOf; nums.push((d) => multipleOfOk(d, m)); }
175
175
  const run = combineValue(nums);
176
- if (run !== null) checks.push((data) => typeof data !== 'number' || run(data));
176
+ // A non-finite number is not of type `number` here: dataBits() gives it
177
+ // no type bit, and an empty schema accepts it. So the numeric keywords
178
+ // must not apply to it either, or `{ minimum: 3 }` rejects a value that
179
+ // `{}` accepts and the compiled engine admits.
180
+ if (run !== null) checks.push((data) => typeof data !== 'number' || !isFinite(data) || run(data));
177
181
  }
178
182
  if (P.hasString) {
179
183
  const strs = [];
@@ -27,6 +27,7 @@ const ANCHOR_ID = 1 << 2; // a fragment-only `$id`, which is an anchor in
27
27
  const DEFINITIONS = 1 << 3; // `definitions`, renamed to `$defs`
28
28
  const DEPENDENCIES = 1 << 4; // `dependencies`, split into two keywords
29
29
  const TUPLE_ITEMS = 1 << 5; // array-valued `items`, split into `prefixItems`
30
+ const EXCLUSIVE_BOOL = 1 << 6; // draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum`
30
31
 
31
32
  // Everything only draft-07 normalization acts on. A schema of another dialect
32
33
  // can carry these without them meaning anything, so they are read together
@@ -74,6 +75,9 @@ function scan(schema) {
74
75
  if (node.definitions !== undefined && node.$defs === undefined) bits |= DEFINITIONS;
75
76
  if (node.dependencies !== undefined) bits |= DEPENDENCIES;
76
77
  if (Array.isArray(node.items)) bits |= TUPLE_ITEMS;
78
+ if (typeof node.exclusiveMinimum === 'boolean' || typeof node.exclusiveMaximum === 'boolean') {
79
+ bits |= EXCLUSIVE_BOOL;
80
+ }
77
81
  if (typeof node.$id === 'string' && ANCHOR_ID_RE.test(node.$id)) bits |= ANCHOR_ID;
78
82
 
79
83
  // `Object.keys` rather than `for...in`: the prototype chain has nothing
@@ -99,6 +103,10 @@ function scan(schema) {
99
103
  function needsNormalization(schema, isDraft7) {
100
104
  const bits = scan(schema);
101
105
  if (bits & NULLABLE) return true;
106
+ // The boolean form of the exclusive bounds is draft-04's, and it is
107
+ // unambiguous wherever it appears: no later dialect gives a boolean any
108
+ // meaning there. Normalizing it is not gated on the declared dialect.
109
+ if (bits & EXCLUSIVE_BOOL) return true;
102
110
  return isDraft7 ? (bits & DRAFT7_WORK) !== 0 : false;
103
111
  }
104
112
 
@@ -111,5 +119,6 @@ module.exports = {
111
119
  DEFINITIONS,
112
120
  DEPENDENCIES,
113
121
  TUPLE_ITEMS,
122
+ EXCLUSIVE_BOOL,
114
123
  DRAFT7_WORK,
115
124
  };
package/lib/tier0.js CHANGED
@@ -100,6 +100,16 @@ function buildTier0Plan(schema) {
100
100
  };
101
101
  }
102
102
 
103
+ // The same reading of `multipleOf` the other two engines use: divide and ask
104
+ // whether the quotient is whole. An exact remainder test called 1 not a
105
+ // multiple of 0.1, because 1 % 0.1 is 0.09999999999999995, and this file
106
+ // answers a fresh validator's first calls, so that was the answer users saw.
107
+ function _multipleOk(v, m) {
108
+ if (m === 0) return false;
109
+ const q = v / m;
110
+ return Number.isInteger(q) || Math.abs(q - Math.round(q)) < 1e-9;
111
+ }
112
+
103
113
  // checkPrimitive stays exported for Tier 1 reuse.
104
114
  function checkPrimitive(c, v) {
105
115
  const m = c.typeMask;
@@ -117,7 +127,7 @@ function checkPrimitive(c, v) {
117
127
  if ((f & F_MAX) && v > c.max) return false;
118
128
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
119
129
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
120
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
130
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
121
131
  }
122
132
  } else if (m === T_NUMBER) {
123
133
  if (typeof v !== 'number' || !isFinite(v)) return false;
@@ -127,7 +137,7 @@ function checkPrimitive(c, v) {
127
137
  if ((f & F_MAX) && v > c.max) return false;
128
138
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
129
139
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
130
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
140
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
131
141
  }
132
142
  } else if (m === T_BOOLEAN) {
133
143
  if (typeof v !== 'boolean') return false;
@@ -171,7 +181,7 @@ function tier0ValidateObject(plan, data) {
171
181
  if ((f & F_MAX) && v > c.max) return false;
172
182
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
173
183
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
174
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
184
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
175
185
  }
176
186
  } else if (m === T_NUMBER) {
177
187
  if (typeof v !== 'number' || !isFinite(v)) return false;
@@ -181,7 +191,7 @@ function tier0ValidateObject(plan, data) {
181
191
  if ((f & F_MAX) && v > c.max) return false;
182
192
  if ((f & F_EXCL_MIN) && v <= c.exclMin) return false;
183
193
  if ((f & F_EXCL_MAX) && v >= c.exclMax) return false;
184
- if ((f & F_MULT) && v % c.multipleOf !== 0) return false;
194
+ if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
185
195
  }
186
196
  } else if (m === T_BOOLEAN) {
187
197
  if (typeof v !== 'boolean') return false;
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.18.0';
10
+ module.exports = '1.19.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.18.0",
3
+ "version": "1.19.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_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_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",
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.18.0",
126
- "@ata-validator/native-darwin-x64": "1.18.0",
127
- "@ata-validator/native-linux-arm64-gnu": "1.18.0",
128
- "@ata-validator/native-linux-arm64-musl": "1.18.0",
129
- "@ata-validator/native-linux-x64-gnu": "1.18.0",
130
- "@ata-validator/native-linux-x64-musl": "1.18.0",
131
- "@ata-validator/native-win32-x64": "1.18.0"
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"
132
132
  },
133
133
  "peerDependencies": {
134
134
  "yaml": "^2.0.0"