ata-validator 1.23.0 → 1.25.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,35 @@
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.25.0 - 2026-09-17
6
+
7
+ ### Fixed
8
+
9
+ - Emitted standalone modules compiled their regex patterns on every call. The compiler stores each pattern both as a closure variable and as an inline `const _reN = __ataSafeRe(...)` in the stored source; in process the inline line never runs (the closure binds the compiled matcher once), but every emitter placed it inside the emitted function, where it shadowed the module-scope binding and rebuilt the matcher per call. A schema with one `pattern` validated at 202 thousand ops/s as a standalone module while the in-process path ran at millions. Patterns are now declared once at load scope in all four emitters (`toStandalone`, `toStandaloneModule`, `bundleStandalone`, `bundleCompact`); the same document on the same schema now measures 27.0 million ops/s, 134x. `bundleCompact` needed care: its deduplication keyed on body text alone, and with the pattern no longer in the body, two schemas differing only in their pattern would have merged, so the key now includes the factory-scope declarations.
10
+
11
+ ### Added
12
+
13
+ - Provably local `unevaluated*` compiles real error detail on the generated-code path, and therefore in standalone modules. When every occurrence of `unevaluatedProperties`/`unevaluatedItems` sits on a node with no `$ref` and no `patternProperties`, and every in-place applicator (`allOf`, `anyOf`, `oneOf`, `if`/`then`/`else`) contributes only property names the node's own `properties` already declares, the keyword is exactly `additionalProperties`/`items` in disguise and both the error and combined generators now emit it as such, keeping the keyword's own identity: one error per stray key with `params.unevaluatedProperty` (ATA7003), one error with `params.limit` for extra items. `toStandaloneModule` with error detail requested no longer degrades on these schemas; a config schema of the common `if/then/else` plus root `unevaluatedProperties: false` shape ships an AOT module with the same per-field errors the runtime reports. Schemas where the annotations cannot be proven local (`unevaluated*` next to a `$ref`, cousin-visible shapes) still decline, loudly, as before. Verified by the error-shape differential (2,816 cases), the engine differential (18,126 cases) and the official suite on all three dialects.
14
+ - `toStandaloneModule(schema, { positions: true })` also exports `validateJSON(text)`: parse, validate, and on failure attach a `dataFrame` (`byteOffset`, `length`, `line`, `col`, `text`) to every error by walking the original text once. The mapping goes through `instancePath`, so a key that appears in several sections frames each error on its own occurrence, which a first-occurrence string search does not. A syntax error is the single ATA9001 error with a frame on the document. The walker is the runtime's own `buildDataPositionMap`, embedded verbatim so the two cannot drift, and it costs about 5 KB in the module (1.7 KB gzipped), which is why it is opt-in.
15
+ - `toStandaloneModule(schema, { parse: true })`'s `parse(data)` now fills declared `default` values for absent optional properties (object and array defaults are fresh per call) and is emitted for composed schemas under the same proof the `unevaluated*` error generators use: in-place applicators are admitted when every property name they mention is already declared in the node's own `properties`. The common config shape, `if`/`then`/`else` over declared fields with root `unevaluatedProperties: false`, now gets a `parse` that enforces the conditional, rejects undeclared keys, and returns the ready object. Two default shapes decline because they are where `parse` and the runtime would disagree: a required property with a default, and a default the property's own schema rejects.
16
+ - Every emitted module exports `schemaHash`, a 16 character content hash of the schema it was compiled from, over canonical JSON so key order does not matter, computed from the schema as the caller wrote it so dialect normalization does not break the comparison. `schemaHash(schema)` from `ata-validator/build` computes the same value; a build detects a stale artifact by comparing the two instead of embedding its own fingerprint.
17
+ - The agreement between compiled modules and the runtime is now a stated contract, in `docs/STABILITY.md` and the API reference: same version, same verdict on every document, enforced by the differential test in every CI run, with the emitters declining or degrading loudly where they cannot prove equivalence.
18
+ - The compat shim exports `attachDataFrames(errors, text)`: the same one-pass mapping for Ajv-shaped errors, for callers that keep the source text around. Not an Ajv API; it exists because hand-written line finders based on a string search get repeated keys wrong.
19
+
20
+ ## 1.24.0 - 2026-09-16
21
+
22
+ ### Added
23
+
24
+ - A schema-directed scanner: `isValidJSON()` answers the verdict from the JSON text without building the document. Parsing is about three quarters of the cost of a request that only needs yes or no, and a rejection now stops at the byte that caused it. The scanner compiles per schema, on the codegen path only, after 64 calls (generating one costs about 20 µs, so a caller that checks one document never pays), and it declines anything outside its supported core: `type`, `properties`, `required`, `additionalProperties`, `items`, `prefixItems`, the length, size and range keywords, `pattern`, `format`, `const`, `enum`, local `$ref` (inlined, acyclic, no base-changing keywords below the root), `allOf` (merged, with the `additionalProperties` cross-branch rule written out in full), and `unevaluated*` where it is provably a synonym for `additionalProperties`/`items`. Whatever it cannot answer it declines at compile time or bails from at runtime, and the parse path takes over unchanged. `tests/test_scanner_differential.js` holds the scanner and `validate()` to the same verdict on the same text: 413,130 comparisons over the official suite, a malformed-JSON corpus and generated corruptions, zero disagreements, and the count must stay at zero.
25
+ - Cross-process medians against the same call forced through `JSON.parse`: accepted documents 1.19x to 1.43x by size; rejection at the first element 37x at 4 KB and 385x at 41 KB.
26
+ - Generated config schemas are first-class: up to 4096 properties per node, and past 48 names the dispatch is a rolling hash accumulated during the key scan, one lookup in a Map passed outside the source, one `startsWith` to confirm, bodies shared per distinct subschema, and an in-order fast path for machine-written JSON that resynchronises across omitted optional keys. A 26.6 KB config with 1300 declared properties: 136.7 µs by parse-and-validate, 43.8 µs by scan.
27
+ - `isValidJSON` also remembers the last (text, verdict) pair, since the verdict is a pure function of the text: a drift monitor re-reading an unchanged file answers in 0.44 µs by native string compare. Withheld when user formats or custom keywords are present, since user functions may not be pure.
28
+ - Strict mode covers all four cases from issue #44: unknown keywords (with a spelling suggestion), unresolvable local `$ref`s, keywords the node's own `type` makes inert (`minimum` on a string can never fire), and `required` names nothing can satisfy (`additionalProperties: false` with the name absent from `properties`). `ata compile --strict-schema` and `build({ strictSchema: true })` run the same checks at build time and refuse to emit a module from a schema that fails them.
29
+
30
+ ### Fixed
31
+
32
+ - An AOT module that cannot carry error detail says so. `toStandaloneModule` with error detail requested builds the error function from the error generator, which declines some schemas, `unevaluated*` among them; the runtime validator falls back to the interpreted engine there, but a standalone module has nothing to fall back to, so it shipped with an exact verdict and a single ATA9000 stub for every failure, silently. The module still ships, since the verdict is exact and validating the rare failing document with the runtime `Validator` is a legitimate pattern, but the degradation is loud in every layer: `onWarning` on the emitter, a NOTE in the module header, a `warnings` list in the `build()` report, a printed warning from `ata compile`, and a refused build under `--strict`. Found by a gateway team that lost their per-field startup errors to it.
33
+
5
34
  ## 1.23.0 - 2026-09-16
6
35
 
7
36
  ### Added
package/bin/ata.js CHANGED
@@ -38,6 +38,10 @@ Build options:
38
38
  --cache-file <path> Cache file for incremental builds (default: cache disabled)
39
39
  --max-size <bytes> Fail build if any compiled module exceeds this gzipped size
40
40
  --strict Treat any AOT-incompatible schema as a build error (default: skip + warn)
41
+ --strict-schema Fail the build on schema authoring mistakes: unknown
42
+ keywords (with a spelling suggestion), keywords the
43
+ node's type makes inert, unresolvable local $refs,
44
+ and required names nothing can satisfy
41
45
  --watch Re-emit on schema change (Ctrl-C to exit)
42
46
  --no-types Skip .d.mts/.d.cts emission alongside compiled modules
43
47
  --source Embed schema source map (default in development)
@@ -80,6 +84,7 @@ function parseArgs(argv) {
80
84
  if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
81
85
  if (a === '--check') { out.opts.check = true; continue; }
82
86
  if (a === '--strict') { out.opts.strict = true; continue; }
87
+ if (a === '--strict-schema') { out.opts.strictSchema = true; continue; }
83
88
  if (a === '--out-dir') { out.opts.outDir = argv[++i]; continue; }
84
89
  if (a === '--suffix') { out.opts.suffix = argv[++i]; continue; }
85
90
  if (a === '--cache-file') { out.opts.cacheFile = argv[++i]; continue; }
@@ -156,7 +161,13 @@ function cmdCompile(args) {
156
161
  process.exit(1);
157
162
  }
158
163
  const input = args._[0];
159
- const format = args.opts.format || 'esm';
164
+ // With no --format, the output filename already says what the caller wants:
165
+ // -o validator.cjs used to emit an ES module into a .cjs file, which Node
166
+ // then refused to load.
167
+ const extFormat = args.opts.output && /\.cjs$/.test(args.opts.output) ? 'cjs'
168
+ : args.opts.output && /\.mjs$/.test(args.opts.output) ? 'esm'
169
+ : null;
170
+ const format = args.opts.format || extFormat || 'esm';
160
171
  if (format !== 'esm' && format !== 'cjs') {
161
172
  process.stderr.write(`error: --format must be esm or cjs (got "${format}")\n`);
162
173
  process.exit(1);
@@ -180,6 +191,19 @@ function cmdCompile(args) {
180
191
  process.exit(1);
181
192
  }
182
193
 
194
+ // The build is where an authoring mistake is cheapest to stop: a typo like
195
+ // maxLenght compiles into a module that simply lacks the rule, and nobody
196
+ // revisits a compiled module. Findings fail the compile, with the path and
197
+ // the suggested spelling.
198
+ if (args.opts.strictSchema) {
199
+ const { checkSchemaStrict } = require('../lib/strict-check');
200
+ const problems = checkSchemaStrict(schema, {});
201
+ if (problems.length > 0) {
202
+ for (const x of problems) reportCompileError(input, `strict mode: ${x.message} at ${x.path}`);
203
+ process.exit(1);
204
+ }
205
+ }
206
+
183
207
  const { Validator } = require('..');
184
208
  const aot = require('../lib/aot');
185
209
  let v;
@@ -200,7 +224,15 @@ function cmdCompile(args) {
200
224
  }
201
225
  }
202
226
  const schemaFile = path.relative(process.cwd(), input) || input;
203
- const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile });
227
+ const compileWarnings = [];
228
+ const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile, onWarning: (w) => compileWarnings.push(w) });
229
+ if (compileWarnings.length > 0) {
230
+ if (args.opts.strict) {
231
+ for (const w of compileWarnings) reportCompileError(input, w);
232
+ process.exit(1);
233
+ }
234
+ for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${w}\n`);
235
+ }
204
236
  if (!src) {
205
237
  reportCompileError(input, 'schema is too complex for standalone compilation');
206
238
  process.exit(1);
package/build.d.ts CHANGED
@@ -89,6 +89,18 @@ export interface BundleStandaloneOptions {
89
89
  }
90
90
 
91
91
  export interface ToStandaloneModuleOptions {
92
+ /**
93
+ * Run the authoring-time schema checks before emitting: unknown keywords
94
+ * (with a spelling suggestion), inert keywords, unsatisfiable required
95
+ * names, unresolvable local $refs. Throws with every finding.
96
+ */
97
+ strictSchema?: boolean;
98
+ /**
99
+ * Called when the module ships degraded: error detail was requested but the
100
+ * error generator declined this schema, so failures report the single
101
+ * ATA9000 abort-early error while the verdict stays exact.
102
+ */
103
+ onWarning?: (message: string) => void;
92
104
  format?: 'cjs' | 'esm';
93
105
  abortEarly?: boolean;
94
106
  source?: boolean;
@@ -99,12 +111,27 @@ export interface ToStandaloneModuleOptions {
99
111
  formatMode?: 'embed' | 'inject';
100
112
  /**
101
113
  * Also export `parse(data)`: validate, then return a copy of the input
102
- * holding only the properties the schema declares. Off by default because
103
- * it adds to the emitted module's size. Emitted only where the copy is
104
- * provably exact; a schema using `$ref`, a composition, `patternProperties`,
105
- * an `additionalProperties` schema or an array of objects gets no `parse`.
114
+ * holding only the properties the schema declares, with declared `default`
115
+ * values filled in for absent optional properties (object and array
116
+ * defaults are fresh per call). Off by default because it adds to the
117
+ * emitted module's size. Emitted only where the copy is provably exact:
118
+ * `$ref` and `patternProperties` decline; in-place applicators (`allOf`,
119
+ * `anyOf`, `oneOf`, `if`/`then`/`else`, and `unevaluatedProperties: false`)
120
+ * are admitted when every property name they mention is already declared
121
+ * in the node's own `properties`. A required property with a default, or
122
+ * a default its own schema rejects, also declines, because those are the
123
+ * two shapes where parse() and the runtime would disagree.
106
124
  */
107
125
  parse?: boolean;
126
+ /**
127
+ * Also export `validateJSON(text)`: parse the JSON text, validate, and on
128
+ * failure attach a `dataFrame` ({ byteOffset, length, line, col, text })
129
+ * to every error by walking the original text once, so errors point at the
130
+ * right occurrence even when the same key appears in several sections.
131
+ * Off by default because the embedded position walker adds to the emitted
132
+ * module's size.
133
+ */
134
+ positions?: boolean;
108
135
  }
109
136
 
110
137
  /** Bundle multiple schemas into one self-contained module (no ata-validator runtime). */
@@ -113,6 +140,16 @@ export function bundleStandalone(schemas: unknown[], options?: BundleStandaloneO
113
140
  /** Like {@link bundleStandalone} but deduplicates shared bodies for smaller output. */
114
141
  export function bundleCompact(schemas: unknown[], options?: BundleStandaloneOptions): string;
115
142
 
143
+ /**
144
+ * Stable content hash of a schema, 16 hex characters, over a canonical JSON
145
+ * form (keys sorted at every level). Every module from
146
+ * {@link toStandaloneModule} exports its own `schemaHash`; comparing that
147
+ * against `schemaHash(currentSchema)` tells a build the module is stale.
148
+ * An integrity aid, not a security boundary.
149
+ */
150
+ export function schemaHash(schema: unknown): string;
151
+
116
152
  /** Emit a self-contained `validate`/`isValid` module string for a single
117
- * schema, plus `parse` when {@link ToStandaloneModuleOptions.parse} is set. */
153
+ * schema, plus `parse` when {@link ToStandaloneModuleOptions.parse} is set
154
+ * and `validateJSON` when {@link ToStandaloneModuleOptions.positions} is. */
118
155
  export function toStandaloneModule(schema: unknown, options?: ToStandaloneModuleOptions): string | null;
package/compat.d.ts CHANGED
@@ -25,6 +25,14 @@ declare class Ata {
25
25
  removeKeyword(keyword: string): this;
26
26
 
27
27
  errorsText(errors?: Ata.ErrorObject[] | null, options?: { separator?: string; dataVar?: string }): string;
28
+
29
+ /**
30
+ * Attach a `dataFrame` ({ byteOffset, length, line, col, text }) to each
31
+ * error by mapping its `instancePath` into the JSON text the data was
32
+ * parsed from. One walk of the text; correct when the same key appears in
33
+ * several sections. Not an Ajv API. Mutates and returns `errors`.
34
+ */
35
+ static attachDataFrames(errors: Ata.ErrorObject[] | null | undefined, text: string | Buffer): Ata.ErrorObject[] | null | undefined;
28
36
  }
29
37
 
30
38
  declare namespace Ata {
package/compat.js CHANGED
@@ -409,6 +409,26 @@ function metaValidator(id) {
409
409
  return v;
410
410
  }
411
411
 
412
+ // Maps each error's instancePath to its position in the JSON text the data
413
+ // was parsed from and attaches it as `dataFrame` ({ byteOffset, length,
414
+ // line, col, text }). One walk of the text, and correct when the same key
415
+ // appears in several sections, which a first-occurrence string search is
416
+ // not. Not an Ajv API: exported for callers that keep the source text
417
+ // around and want file positions on Ajv-shaped errors.
418
+ Ata.attachDataFrames = function attachDataFrames(errors, text) {
419
+ if (!errors || !errors.length || text == null) return errors;
420
+ const { buildDataPositionMap } = require('./lib/data-positions');
421
+ let map;
422
+ try { map = buildDataPositionMap(text); } catch { return errors; }
423
+ for (const e of errors) {
424
+ if (!e || e.dataFrame) continue;
425
+ const ptr = e.instancePath != null ? e.instancePath : (e.dataPath || '');
426
+ const p = map[ptr];
427
+ if (p) e.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
428
+ }
429
+ return errors;
430
+ };
431
+
412
432
  module.exports = Ata;
413
433
  module.exports.default = Ata;
414
434
  module.exports.Ata = Ata;
package/index.d.ts CHANGED
@@ -395,9 +395,20 @@ export interface ValidatorOptions {
395
395
  keywords?: Record<string, KeywordDefinition | KeywordValidate>;
396
396
  /**
397
397
  * When true, validation errors include `parentSchema` (the schema object
398
- * that produced the error). Matches ajv's `verbose: true`.
398
+ * that produced the error), `schema` (the failing keyword's own value) and
399
+ * `data` (the value the error points at). Matches ajv's `verbose: true`.
399
400
  */
400
401
  verbose?: boolean;
402
+ /**
403
+ * Authoring-time schema checks, run at construction on the schema as
404
+ * written: unknown keywords (with a spelling suggestion), keywords the
405
+ * node's own `type` makes inert, `required` names nothing can satisfy, and
406
+ * local `$ref`s that do not resolve. `true` throws with every finding;
407
+ * `'log'` warns through `logger` (or the console) and continues.
408
+ */
409
+ strictSchema?: boolean | 'log';
410
+ /** Receives `strictSchema: 'log'` warnings; `false` silences them. */
411
+ logger?: { warn(...args: unknown[]): void } | false;
401
412
  /**
402
413
  * When true, validate() returns a shared frozen result on the first failure
403
414
  * instead of collecting full error details. Smaller hot-path allocation.
package/index.js CHANGED
@@ -933,6 +933,8 @@ class Validator {
933
933
  // to attach dataFrame entries to each enriched error.
934
934
  this._posCache = null; // created by _pos() on first use, only the JSON text path needs it
935
935
  this._lastRawInput = null;
936
+ // undefined: not built yet. null: this schema has no scanner.
937
+ this._scanner = undefined;
936
938
 
937
939
  // Public methods start as memoized accessors on the prototype; nothing is
938
940
  // allocated per instance until one is first read. See _defineLazyMethod
@@ -1426,6 +1428,23 @@ class Validator {
1426
1428
  return verdictFromText(jsonStr);
1427
1429
  }
1428
1430
  : verdictFromText;
1431
+
1432
+ // A schema-directed scanner answers the verdict from the JSON text
1433
+ // without building the document. Parsing is around three quarters of the
1434
+ // cost of a real request, and a caller that only wants yes or no should
1435
+ // not pay it; a rejection can also stop at the byte that caused it
1436
+ // instead of parsing the rest of a document that is already refused.
1437
+ //
1438
+ // It is wired only where the verdict IS the answer. On a path that has
1439
+ // to produce errors, scanning an invalid document is work thrown away,
1440
+ // so those keep parsing. `abortEarly` has no errors to produce, so it
1441
+ // counts as a verdict path.
1442
+ //
1443
+ // Not wired when a preprocess pass is configured: coercion, removal and
1444
+ // defaults rewrite the document before it is judged, and the scanner
1445
+ // reads what arrived. The compiler declines any schema it cannot answer
1446
+ // and a compiled scanner returns BAIL for a document shape it cannot
1447
+ // answer, and then the parse path below takes over unchanged.
1429
1448
  // validateAndParse: parse the JSON, then validate. Pure JS (JSON.parse +
1430
1449
  // validate) so it works with or without the native addon and in browsers.
1431
1450
  {
@@ -1799,6 +1818,83 @@ class Validator {
1799
1818
  const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
1800
1819
  if (bufferNeedsSlowPath(schemaObj, this._schemaMap, this._keywords)) installSlowBufferApis(this);
1801
1820
  }
1821
+ // Installed after the buffer gate on purpose: the gate replaces
1822
+ // isValidJSON for schemas whose shapes the native walker gets wrong,
1823
+ // unevaluatedProperties among them, and the scanner wiring has to wrap
1824
+ // whatever answers last or a gated schema silently loses its scanner.
1825
+ if (this._jsFn && !this._preprocess) {
1826
+ const self = this;
1827
+ // Generating a scanner costs about 20 microseconds, measured, and it
1828
+ // saves from around 85 nanoseconds on a small accepted document to
1829
+ // several microseconds on a rejected one. Building it on the first
1830
+ // call would therefore be a straight loss for a caller that checks one
1831
+ // document and exits, so it is built once a caller has asked often
1832
+ // enough that it is plainly doing this in a loop. A server passes the
1833
+ // line during warm-up and never sees it.
1834
+ const SCAN_AFTER = 64;
1835
+ let calls = 0;
1836
+ // undefined: not built. null: this schema has no scanner. Passing true
1837
+ // builds it now, which is how the differential test reaches it.
1838
+ this._ensureScanner = (now) => {
1839
+ if (self._scanner === undefined) {
1840
+ if (!now && ++calls < SCAN_AFTER) return undefined;
1841
+ const built = require('./lib/scan-compiler').compileScanner(schemaObj, { userFormats: self._userFormats });
1842
+ self._scanner = built ? built.scan : null;
1843
+ }
1844
+ return self._scanner;
1845
+ };
1846
+ const byParsing = this.isValidJSON;
1847
+ // The verdict is a pure function of the text, and the caller a
1848
+ // gateway or a drift monitor keeps asking about is usually the same
1849
+ // text: a config file re-read on a timer, a heartbeat body. One
1850
+ // remembered (text, verdict) pair answers that case with a native
1851
+ // string compare, which is a memcmp, instead of a scan. Withheld when
1852
+ // user formats or custom keywords are present, since those are user
1853
+ // functions and nothing guarantees they are pure.
1854
+ const memoizable = !self._userFormats && !self._usesKeywords;
1855
+ let _memoText = null;
1856
+ let _memoVerdict = false;
1857
+ this.isValidJSON = (jsonStr) => {
1858
+ const scan = self._ensureScanner();
1859
+ if (scan === undefined) return byParsing(jsonStr);
1860
+ if (scan === null) { self.isValidJSON = byParsing; return byParsing(jsonStr); }
1861
+ self.isValidJSON = memoizable
1862
+ ? (text) => {
1863
+ if (typeof text !== 'string') return byParsing(text);
1864
+ if (text === _memoText) return _memoVerdict;
1865
+ const r = scan(text);
1866
+ const verdict = r === -1 ? byParsing(text) : r === 1;
1867
+ _memoText = text;
1868
+ _memoVerdict = verdict;
1869
+ return verdict;
1870
+ }
1871
+ : (text) => {
1872
+ if (typeof text !== 'string') return byParsing(text);
1873
+ const r = scan(text);
1874
+ if (r === -1) return byParsing(text);
1875
+ return r === 1;
1876
+ };
1877
+ return self.isValidJSON(jsonStr);
1878
+ };
1879
+ if (options.abortEarly) {
1880
+ const validateByParsing = this.validateJSON;
1881
+ this.validateJSON = (jsonStr) => {
1882
+ const scan = self._ensureScanner();
1883
+ if (scan === undefined) return validateByParsing(jsonStr);
1884
+ if (scan === null) { self.validateJSON = validateByParsing; return validateByParsing(jsonStr); }
1885
+ self.validateJSON = (text) => {
1886
+ if (typeof text === 'string') {
1887
+ const r = scan(text);
1888
+ if (r === 1) return VALID_RESULT;
1889
+ if (r === 0) return ABORT_EARLY_RESULT;
1890
+ }
1891
+ return validateByParsing(text);
1892
+ };
1893
+ return self.validateJSON(jsonStr);
1894
+ };
1895
+ }
1896
+ }
1897
+
1802
1898
 
1803
1899
  // Save to identity cache for ultra-fast reuse with same schema object.
1804
1900
  // Only an instance built without options may answer a later
package/lib/aot-build.js CHANGED
@@ -94,6 +94,7 @@ async function build(opts) {
94
94
  const cached = [];
95
95
  const skipped = [];
96
96
  const failed = [];
97
+ const warnings = [];
97
98
 
98
99
  for (const input of inputs) {
99
100
  try {
@@ -126,6 +127,18 @@ async function build(opts) {
126
127
  continue;
127
128
  }
128
129
  const schema = parseSchemaFile(input);
130
+ // The build is where an authoring mistake is cheapest to stop: a typo
131
+ // like maxLenght compiles into a validator that simply lacks the rule,
132
+ // and a compiled module fails even more quietly than a runtime one,
133
+ // since nobody ever revisits it. Findings fail the input's build.
134
+ if (opts.strictSchema) {
135
+ const { checkSchemaStrict } = require('./strict-check');
136
+ const problems = checkSchemaStrict(schema, {});
137
+ if (problems.length > 0) {
138
+ failed.push({ input, error: problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('; ') });
139
+ continue;
140
+ }
141
+ }
129
142
  const v = new Validator(schema);
130
143
  const source = resolveSourceDefault(opts);
131
144
  let sourceMap = null;
@@ -138,6 +151,7 @@ async function build(opts) {
138
151
  }
139
152
  }
140
153
  const schemaFile = path.relative(process.cwd(), input) || input;
154
+ const inputWarnings = [];
141
155
  const src = aot.toStandaloneModule(v, {
142
156
  format,
143
157
  abortEarly: !!opts.abortEarly,
@@ -145,7 +159,15 @@ async function build(opts) {
145
159
  sourceMap,
146
160
  schemaFile,
147
161
  formatMode: opts.formatMode,
162
+ onWarning: (w) => inputWarnings.push(w),
148
163
  });
164
+ if (inputWarnings.length > 0) {
165
+ if (opts.strict) {
166
+ failed.push({ input, error: inputWarnings.join('; ') });
167
+ continue;
168
+ }
169
+ for (const w of inputWarnings) warnings.push({ input, warning: w });
170
+ }
149
171
  if (!src) {
150
172
  const reason = 'schema is not AOT-compatible (toStandaloneModule returned null)';
151
173
  if (opts.strict) failed.push({ input, error: reason });
@@ -190,12 +212,12 @@ async function build(opts) {
190
212
 
191
213
  if (opts.check) {
192
214
  const staleCount = inputs.length - cached.length;
193
- return { compiled: [], cached, skipped, failed, staleCount };
215
+ return { compiled: [], cached, skipped, failed, warnings, staleCount };
194
216
  }
195
217
 
196
218
  writeCache(opts.cacheFile, newCache);
197
219
 
198
- return { compiled, cached, skipped, failed };
220
+ return { compiled, cached, skipped, failed, warnings };
199
221
  }
200
222
 
201
223
  async function watch(opts, onReport) {
@@ -248,10 +270,22 @@ function bundleCompact(schemas, opts) {
248
270
  }
249
271
 
250
272
  function toStandaloneModule(schema, opts) {
273
+ // The same authoring-time checks `ata compile --strict-schema` runs, for
274
+ // callers driving the build API directly: a typo in a schema headed for a
275
+ // compiled module is the quietest place a typo can live.
276
+ if (opts && opts.strictSchema) {
277
+ const { checkSchemaStrict } = require('./strict-check');
278
+ const problems = checkSchemaStrict(schema, {});
279
+ if (problems.length > 0) {
280
+ throw new Error(problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('\n'));
281
+ }
282
+ }
251
283
  const v = schema instanceof Validator ? schema : new Validator(schema, opts);
252
284
  return aot.toStandaloneModule(v, opts);
253
285
  }
254
286
 
287
+ const { schemaHash } = require('./schema-hash');
288
+
255
289
  module.exports = {
256
290
  build,
257
291
  expandGlobs,
@@ -261,4 +295,5 @@ module.exports = {
261
295
  bundleStandalone,
262
296
  bundleCompact,
263
297
  toStandaloneModule,
298
+ schemaHash,
264
299
  };