@amritk/validation 0.4.0 → 0.5.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/AI.md CHANGED
@@ -44,14 +44,16 @@ Every mode is reachable from `mjst` without dropping to the programmatic API:
44
44
  |:---|:---|
45
45
  | `types` | `--types-only` (alone), otherwise always emitted |
46
46
  | `guard`, `validate` | `--validators`, or `--validators-only` for no parser |
47
- | `check` | `--check` |
48
- | `coerce` | `--coerce` |
49
- | `repair` | `--repair` (emits `coerceX` too) |
47
+ | `check` | `--validators --check` |
48
+ | `coerce` | `--validators --coerce` |
49
+ | `repair` | `--validators --repair` (emits `coerceX` too) |
50
50
  | `parse` | the default on any run that is not `--types-only`/`--validators-only` |
51
51
  | `parseStrict` | `--strict` |
52
52
 
53
53
  `--validators-only` is this package's own default (`['types', 'guard', 'validate']`)
54
54
  spelled as a flag: everything that judges a document, nothing that builds one.
55
+ `--check`, `--coerce` and `--repair` shape the validator half, so the CLI rejects
56
+ any of them without one of those two flags.
55
57
 
56
58
  ## Gotchas — where agents fail
57
59
 
@@ -83,10 +85,10 @@ spelled as a flag: everything that judges a document, nothing that builds one.
83
85
  validateX(v)` pays for both passes and is no faster than `validateX`.
84
86
 
85
87
  8. **It emits each engine's exact bytes.** A single-mode build is byte-identical
86
- to what that engine emitted when it was its own package, so there is no runtime
87
- difference to reason about and no speedup to claim — do not tell a user this is
88
- faster. What it changes is that the whole matrix comes out as one type instead
89
- of two, and that a build asking for no validator mode ships no
88
+ to what the engine that owns that mode emits when called directly, so there is
89
+ no runtime difference to reason about and no speedup to claim — do not tell a
90
+ user this is faster. What it changes is that the whole matrix comes out as one
91
+ type instead of two, and that a build asking for no validator mode ships no
90
92
  `validation-result.ts`.
91
93
 
92
94
  9. **`coerceX` is not Ajv's `coerceTypes`, on purpose.** It never modifies its
package/README.md CHANGED
@@ -74,6 +74,7 @@ so no two can disagree about whether a document is valid.
74
74
  | `types` | — | — | — | — |
75
75
  | `guard` | `isX(input): input is X` | — | a boolean | the first problem |
76
76
  | `validate` | `validateX(input)` | — | every error | the end |
77
+ | `check` | `checkX(input)` | — | the first error | the first problem |
77
78
  | `coerce` | `coerceX(input)` | coerced | every error | the end |
78
79
  | `repair` | `repairX(input)` | repaired | repairs **and** errors | the end |
79
80
  | `parse` | `parseX(input)` | repaired | nothing | never fails |
@@ -84,7 +85,9 @@ it stops at the first thing wrong and builds no error object, which on invalid
84
85
  input is dramatically cheaper than reporting. `validate` is the other half: every
85
86
  error, each with a JSON Pointer, the keyword that rejected the value, and that
86
87
  keyword's own values. Reach for `guard` when the answer is a branch and
87
- `validate` when a human or an API client has to be told what to fix.
88
+ `validate` when a human or an API client has to be told what to fix. `check` sits
89
+ between them: `checkX` returns the same result type as `validateX`, holding only
90
+ the error `validateX` would have reported first, and walks no further.
88
91
 
89
92
  **Coerce and repair, with or without the errors.** `coerceX` and `repairX` hand
90
93
  back the value *and* the diagnostics; `parseX` hands back only the value and
@@ -92,6 +95,14 @@ never fails. Coercion moves a value that is already right but written in the
92
95
  wrong type; repair also substitutes a value the schema supplies for one that
93
96
  cannot be coerced, and reports the validator's own errors as the repairs.
94
97
 
98
+ That makes `repairX` an autofix tool rather than a stricter validator. It accepts
99
+ documents `validateX` rejects (a missing required name gets one; an unknown
100
+ `enum` value becomes the first member), and accepting only `valid` with an empty
101
+ `repairs` gives exactly the verdicts `coerceX` gives. An empty `repairs` alone is
102
+ not enough: a document nothing could repair comes back with none either, and
103
+ `valid: false`. To reject a bad config, use `coerceX`. To hand back a fixed one,
104
+ use `repairX`.
105
+
95
106
  **`parseX` agrees with `coerceX`.** Wherever `coerceX` accepts a document, the
96
107
  coercing `parseX` returns the very same value — through a union, an `allOf`, an
97
108
  `if`, a `$ref`, recursion included — and it only repairs what `coerceX` would
@@ -119,11 +130,19 @@ schemas with `anyOf`, `oneOf`, `allOf` and `if`/`then`/`else` checks every
119
130
  accepted document against an Ajv that does not coerce. Where the two differ, it
120
131
  is on purpose:
121
132
 
122
- - **`null` is never coerced**, in either direction. Ajv reads `null` as `""`, `0`
123
- or `false` for a string, number or boolean field, and reads those back as
124
- `null`. A `null` usually means "not set", and turning it into a real value
125
- erases that. Here it goes to the validator as written, and the validator
126
- rejects it.
133
+ - **`null` is never coerced**, in either direction, and it is the difference
134
+ you will meet most: on a real config corpus it was nearly every case where Ajv
135
+ accepted and `coerceX` did not. Ajv reads `null` as `""`, `0` or `false` for a
136
+ string, number or boolean field, and reads those back as `null`. A `null`
137
+ usually means "not set", and turning it into a real value erases that. Here it
138
+ goes to the validator as written, and the validator rejects it.
139
+
140
+ Before switching, check what your callers do with a rejected document. If one
141
+ treats rejection as absence (returns `undefined`, falls back to a default),
142
+ a single `null` deep inside a block makes the whole block vanish without an
143
+ error. Ajv's coercion used to hide that. Either let the schema say `null` is
144
+ allowed (`type: ['string', 'null']`), drop `null`s before validating, or have
145
+ the caller surface the errors instead of discarding the value.
127
146
  - **Only clean numerals become numbers.** `" "`, `" 1 "`, `"0x10"`, `"Infinity"`
128
147
  and `"1."` are rejected. Ajv turns them into `0`, `1`, `16`, a value JSON
129
148
  cannot hold, and `1`. `"007"` and `"1e3"` are still coerced.
@@ -146,8 +165,8 @@ is on purpose:
146
165
  document has to clone it first. `coerceX` returns a new value that shares
147
166
  everything it did not touch, or the input itself when nothing needed coercing.
148
167
 
149
- **It is faster.** Against Ajv cloning first, so both leave the caller's document
150
- alone (`bun run bench:validators:coerce`, or
168
+ **It is faster where the input has to survive.** Against Ajv cloning first, so
169
+ both leave the caller's document alone (`bun run bench:validators:coerce`, or
151
170
  `bench:validators:coerce:node`):
152
171
 
153
172
  | schema | runtime | valid input | needs coercing | cannot be coerced |
@@ -166,7 +185,11 @@ each engine timed in its own process (Bun 1.3.11, Node 22.22, Linux x64). A
166
185
  valid document is the common case and the widest gap: where `isX` is a
167
186
  standalone guard, `coerceX` answers it with that guard and hands the input
168
187
  straight back, without walking it. On valid input Ajv rewrites nothing, so it can
169
- also be timed without the clone; `coerceX` is still ahead there, by 1.2–3.3×.
188
+ also be timed without the clone; `coerceX` is still ahead there on these
189
+ schemas, by 1.2–3.3×, but not on every one: on a large, union-heavy document
190
+ that the caller lets Ajv coerce in place, Ajv can come out modestly ahead. The
191
+ gap above is the clone, so it is real exactly where the caller cannot let Ajv
192
+ mutate its input.
170
193
  The config case is the shape `--coerce` exists for: every coercible scalar sits
171
194
  inside a union reached through `$ref`, and every union branch is tested by a
172
195
  named function rather than a closure built per call.</sub>
@@ -191,15 +214,22 @@ holding it (`/readme`). `params.additionalProperty` names the key in both.
191
214
  | `options.schemas` | `Record<string, unknown>` | — | Documents you have already loaded, keyed by the URI a `$ref` names them by. |
192
215
  | `options.unknownKeys` | `'count-keys' \| 'count-enumerable'` | `'count-keys'` | How a closed object's fast path proves it carries no undeclared key. |
193
216
  | `options.formats` | `'all' \| string[]` | — | String `format`s the validators enforce. Unset leaves `format` an annotation. |
194
- | `options.branchErrors` | `boolean` | `false` | Explain a failing `anyOf` / `oneOf` with the branch it plainly meant. |
217
+ | `options.branchErrors` | `boolean` | `false` | Explain a failing `anyOf` / `oneOf` with the branch it plainly meant, or with the kinds it accepts when no branch takes the value's kind. |
195
218
  | `options.stripUnknown` | `boolean` | `false` | Build each parsed result from the declared properties only. |
196
219
  | `options.readonly` | `boolean` | `false` | Emit `readonly` type members. |
197
220
  | `options.caseInsensitive` | `boolean` | `false` | Normalize a mis-cased string onto an `enum` member it matches. |
198
221
  | `options.helpersMode` | `'package' \| 'embedded'` | `'package'` | Where the parser half gets its runtime helpers. |
222
+ | `options.helpersImportPrefix` | `string` | `'./'` | Prefix on emitted-helper import specifiers, when `helpersMode` is `'embedded'`. |
199
223
  | `options.extensions` | `SchemaExtensions` | — | Schema extensions, passed through to the parser half. |
224
+ | `options.importExt` | `'js' \| 'ts'` | `'js'` | Extension on every emitted relative import. `'ts'` emits the on-disk paths, so the output runs under Node's type stripping with no build step. |
225
+ | `options.logWarnings` | `boolean` | `false` | Print the parser half's warnings about the schema, such as a keyword it cannot enforce. |
200
226
 
201
227
  Returns: `Promise<GeneratedFile[]>` where `GeneratedFile = { filename: string; content: string }`.
202
228
 
229
+ The package also exports `ALL_MODES` (every mode but `parseStrict`, for a caller
230
+ who wants the lot) and the types `Mode`, `GenerateOptions`, `GeneratedFile` and
231
+ `ImportExtension`.
232
+
203
233
  ---
204
234
 
205
235
  ## Output layout
@@ -208,6 +238,8 @@ Returns: `Promise<GeneratedFile[]>` where `GeneratedFile = { filename: string; c
208
238
  config.ts the type, plus isX / validateX / coerceX / repairX
209
239
  config.parse.ts parseX, importing the type from ./config.js
210
240
  validation-result.ts the shared error and result types, and the runtime helpers
241
+ formats.ts the format checks, only when `formats` enforces one the schema names
242
+ _helpers/ the parser's runtime helpers, only when `helpersMode` is 'embedded'
211
243
  index.ts a barrel over all of it
212
244
  ```
213
245
 
@@ -801,7 +801,7 @@ const inlineNestedFastPath = (childSchema, childTypeName, objectExpr, useRefImpo
801
801
  };
802
802
  const RESULT_VARIABLE = "out";
803
803
  const PROTOTYPE_MEMBERS = new Set(Object.getOwnPropertyNames(Object.prototype));
804
- const resultMember = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? `${RESULT_VARIABLE}.${key}` : `${RESULT_VARIABLE}[${JSON.stringify(key)}]`;
804
+ const resultMember = (key) => `${RESULT_VARIABLE}[${JSON.stringify(key)}]`;
805
805
  const renderLiteralEntry = (entry, indent) => {
806
806
  if (entry.kind === "field")
807
807
  return `${indent}${safeLiteralKey(entry.key)}: ${entry.value},`;
@@ -133,7 +133,6 @@ const wrongTypeCondition = (accessor, type) => {
133
133
  return null;
134
134
  }
135
135
  };
136
- const typeLabel = (type) => type === "integer" ? "number" : type;
137
136
  const throwError = (message, suffixExpr) => {
138
137
  const literal = quoteJsString(message);
139
138
  return suffixExpr ? `throw new Error(${literal} + (${suffixExpr}))` : `throw new Error(${literal})`;
@@ -216,7 +215,7 @@ const generateItemCheck = (schema, context) => {
216
215
  }
217
216
  const scalarCheck = plainScalarItemCheck(items, "_it");
218
217
  if (scalarCheck !== null && isSchemaObject(items) && hasType(items)) {
219
- return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
218
+ return { check: scalarCheck, message: `items expected ${items.type}` };
220
219
  }
221
220
  if (hasRef(items) && context.useRefImports === true || shapeIsDelegated(items, context))
222
221
  return null;
@@ -489,7 +488,7 @@ const generatePrefixItemsAssertion = (acc, field, schema, context) => {
489
488
  const pt = pos.type;
490
489
  const wrong = wrongTypeCondition(el, pt);
491
490
  if (wrong) {
492
- lines.push(` if (${present} && (${wrong})) ${throwError(`${field}[${i}] expected ${typeLabel(pt)}, got `, `typeof ${el}`)};`);
491
+ lines.push(` if (${present} && (${wrong})) ${throwError(`${field}[${i}] expected ${pt}, got `, `typeof ${el}`)};`);
493
492
  }
494
493
  }
495
494
  const match = subschemaMatchExpr(el, prefix[i], matchContext(context));
@@ -604,7 +603,7 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
604
603
  if (multiType !== void 0) {
605
604
  const types = propSchema.type;
606
605
  if (multiType !== null) {
607
- const expected = throwError(`${field} expected ${types.map(typeLabel).join(" | ")}, got `, `typeof ${acc}`);
606
+ const expected = throwError(`${field} expected ${types.join(" | ")}, got `, `typeof ${acc}`);
608
607
  lines.push(isRequired ? ` if (!${multiType}) ${expected};` : ` if (${acc} !== undefined && !${multiType}) ${expected};`);
609
608
  }
610
609
  const nonNull = types.filter((type) => type !== "null");
@@ -618,7 +617,7 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
618
617
  const t = propSchema.type;
619
618
  const wrongType = wrongTypeCondition(acc, t);
620
619
  if (wrongType) {
621
- const expected = throwError(`${field} expected ${typeLabel(t)}, got `, `typeof ${acc}`);
620
+ const expected = throwError(`${field} expected ${t}, got `, `typeof ${acc}`);
622
621
  if (isRequired) {
623
622
  lines.push(` if (${wrongType}) ${expected};`);
624
623
  } else {
@@ -720,7 +719,7 @@ const generateScalarStrictAssertion = (schema, typeName, context = {}) => {
720
719
  if (rootMultiType !== void 0) {
721
720
  const types = schema.type;
722
721
  if (rootMultiType !== null) {
723
- lines.push(` if (!${rootMultiType}) ${throwError(`${label} expected ${types.map(typeLabel).join(" | ")}, got `, got)};`);
722
+ lines.push(` if (!${rootMultiType}) ${throwError(`${label} expected ${types.join(" | ")}, got `, got)};`);
724
723
  }
725
724
  const nonNull = types.filter((type) => type !== "null");
726
725
  if (nonNull.length === 1) {
@@ -734,7 +733,7 @@ const generateScalarStrictAssertion = (schema, typeName, context = {}) => {
734
733
  const t = schema.type;
735
734
  const wrongType = wrongTypeCondition("input", t);
736
735
  if (wrongType) {
737
- lines.push(` if (${wrongType}) ${throwError(`${label} expected ${typeLabel(t)}, got `, got)};`);
736
+ lines.push(` if (${wrongType}) ${throwError(`${label} expected ${t}, got `, got)};`);
738
737
  }
739
738
  if (t === "array") {
740
739
  lines.push(...generateContainsCheck("input", schema, label, context));
@@ -12,7 +12,7 @@ export type GeneratedFile = {
12
12
  * types plus the helpers emitted code calls as free identifiers. Exported so tests
13
13
  * can evaluate the very source that ships instead of reimplementing it.
14
14
  */
15
- export declare const VALIDATION_RESULT_CONTENT = "/**\n * A single validation error: what went wrong, where, and which keyword said so.\n *\n * The shape matches `@amritk/runtime-validators`, so an error from a generated\n * validator and one from the runtime interpreter can be handled by the same\n * code \u2014 grouped, translated, or branched on \u2014 without knowing which produced it.\n */\nexport type ValidationError = {\n /** Human-readable description of what went wrong. */\n message: string\n /** JSON Pointer to the offending value inside the instance. */\n path: string\n /** The JSON Schema keyword that rejected the value \u2014 `type`, `required`, `minimum`, \u2026 */\n keyword: string\n /**\n * The keyword's own values, as far as they explain the failure: the bound that\n * was exceeded, the property that was missing, the allowed values that were not\n * matched. Empty for a keyword with nothing to add beyond its name.\n */\n params: Record<string, unknown>\n}\n\n/**\n * The result of a generated validator function.\n * Returns `true` when the input is valid, or an object with `valid: false`\n * and a list of errors when it is not.\n */\nexport type ValidationResult = true | { valid: false; errors: ValidationError[] }\n\n/**\n * The result of a generated coercing validator.\n *\n * Unlike `ValidationResult` there is no bare `true`: a caller that coerces wants\n * the value back, and the whole point is that it may differ from what went in.\n * The input is never modified \u2014 `value` is the input itself when nothing needed\n * coercing, and otherwise a copy that shares everything the coercion did not\n * touch. So a caller does not have to clone defensively the way an in-place\n * coercer forces them to.\n */\nexport type CoercionResult<T> = { valid: true; value: T } | { valid: false; errors: ValidationError[] }\n\n/**\n * A string this will read as a number: an optional sign, digits with an optional\n * fractional part, an optional exponent. Nothing else.\n *\n * Deliberately narrower than what `Number()` accepts, which is where Ajv gets\n * its surprises \u2014 `Number(\" \")` is `0`, `Number(\"0x10\")` is `16` and\n * `Number(\"Infinity\")` is a value JSON cannot even represent. A config that says\n * `retries: \" \"` has a mistake in it, and answering `0` is the one thing worse\n * than rejecting it. Leading zeros (`\"007\"`) and exponents (`\"1e3\"`) stay: both\n * are ordinary ways to write a number in a YAML file, and neither is ambiguous.\n */\nconst isNumericString = (text: string): boolean => {\n // `/^[+-]?(?:\\d+|\\d*\\.\\d+)(?:[eE][+-]?\\d+)?$/`, walked by hand. The regex\n // cost more than the rest of a number coercion put together \u2014 a string past\n // its end reads `NaN` from `charCodeAt`, which fails every test below, so\n // the scan needs no bounds checks of its own.\n let i = 0\n let c = text.charCodeAt(0)\n if (c === 43 || c === 45) c = text.charCodeAt(++i)\n let digits = 0\n while (c >= 48 && c <= 57) {\n digits++\n c = text.charCodeAt(++i)\n }\n if (c === 46) {\n c = text.charCodeAt(++i)\n let fraction = 0\n while (c >= 48 && c <= 57) {\n fraction++\n c = text.charCodeAt(++i)\n }\n if (fraction === 0) return false\n } else if (digits === 0) return false\n if (c === 101 || c === 69) {\n c = text.charCodeAt(++i)\n if (c === 43 || c === 45) c = text.charCodeAt(++i)\n let exponent = 0\n while (c >= 48 && c <= 57) {\n exponent++\n c = text.charCodeAt(++i)\n }\n if (exponent === 0) return false\n }\n return i === text.length\n}\n\n/**\n * One scalar, coerced toward `type`, or returned untouched when that is not\n * possible.\n *\n * Returning the original on failure is what keeps the error honest: nothing is\n * substituted, so the validator that runs next rejects the value the caller\n * actually wrote, with the keyword and params that rejected it. That is the\n * difference between this and a parser's repair, which repairs toward a default\n * and leaves nothing to report.\n *\n * The table is Ajv's `coerceTypes` minus the cells where Ajv guesses. Every\n * value this coerces, Ajv coerces to the same value \u2014 `coerced-vs-ajv` pins that\n * as a property, so moving off Ajv never changes a value, it only turns some of\n * Ajv's silent repairs into errors. What is deliberately *not* coerced:\n *\n * - **Anything to or from `null`.** Ajv reads `null` as `\"\"`, `0` and\n * `false`, and reads `\"\"`, `0` and `false` back as `null`. `null` is a\n * JSON value in its own right and usually means \"not set\"; turning it into an\n * empty string, or an empty string into it, loses the distinction the document\n * drew.\n * - **Strings that are not cleanly numeric** ({@link isNumericString}) \u2014 no\n * whitespace padding, no `0x`/`0o`/`0b`, no `Infinity`, no trailing `.`.\n */\nexport const coerceScalar = (value: unknown, type: string): unknown => {\n switch (type) {\n case 'string':\n return typeof value === 'number' || typeof value === 'boolean' ? String(value) : value\n case 'number':\n case 'integer': {\n if (typeof value === 'boolean') return value ? 1 : 0\n if (typeof value !== 'string' || !isNumericString(value)) return value\n const asNumber = Number(value)\n if (!Number.isFinite(asNumber)) return value\n return type === 'integer' && asNumber % 1 !== 0 ? value : asNumber\n }\n case 'boolean':\n if (value === 'true' || value === 1) return true\n if (value === 'false' || value === 0) return false\n return value\n default:\n return value\n }\n}\n\n/**\n * One scalar at a position that offers several types \u2014 a `type` array, or a\n * union of scalar branches \u2014 coerced only when every type that can take it\n * agrees on the result.\n *\n * A value whose type is already one of the offered types is left alone: it is\n * what the schema asked for, and the question of coercion does not arise. That\n * one rule is where this parts company with Ajv, which walks its own coercion\n * list in order and so turns `\"1\"` into `1` under `[\"number\", \"string\"]` while\n * leaving it a string under `[\"string\", \"number\"]`. The answer should not depend\n * on the order someone wrote the union in.\n *\n * Otherwise every offered type is tried, and the coercion is taken only if the\n * types that succeed all agree on it. `true` against `number | string` could be\n * `1` or `\"true\"` with equal justification, so it stays `true` and the\n * validator says what is wrong with it. `\"1\"` against `number | integer` is `1`\n * either way, so it is `1`.\n */\nexport const coerceUnion = (value: unknown, types: readonly string[]): unknown => {\n const actual = value === null ? 'null' : typeof value\n for (const type of types) {\n // `integer` is satisfied by a number, so a non-integral number is not a\n // *type* mismatch here \u2014 the validator is what holds it to being whole.\n if (actual === type || (actual === 'number' && type === 'integer')) return value\n }\n\n let coerced: unknown = value\n for (const type of types) {\n const candidate = coerceScalar(value, type)\n if (candidate === value) continue\n if (coerced === value) coerced = candidate\n else if (candidate !== coerced) return value\n }\n return coerced\n}\n\n/**\n * How deep a structural comparison walks before it gives up and answers \"not\n * equal\".\n *\n * JSON data is acyclic, but a generated validator is a plain function applied to\n * whatever in-memory value a caller hands it \u2014 and a self-referential object\n * reaching a `const` / `enum` / `uniqueItems` check used to recurse until the\n * stack overflowed, so `validateFoo` threw a `RangeError` instead of returning\n * the `ValidationResult` its signature promises. The cap turns that into an\n * ordinary \"these are different\" without ever coming near real data;\n * `@amritk/runtime-validators` guards its own `deepEqual` at the same depth.\n */\nconst MAX_EQUAL_DEPTH = 512\n\n/**\n * Structural deep equality used by generated `const` checks. Objects compare by\n * their key sets rather than serialization, so `{ a: 1, b: 2 }` and\n * `{ b: 2, a: 1 }` are equal \u2014 unlike `JSON.stringify`, which is key-order\n * sensitive and would reject a reordered-but-equal value.\n */\nexport const valuesEqual = (a: unknown, b: unknown, depth = 0): boolean => {\n // SameValueZero: `===` settles every primitive except `NaN`, which counts as\n // equal to itself here. That is what the native `Set` in {@link allUnique} does,\n // what Ajv does, and what the interpreter's `deepEqual` does \u2014 leaving it out\n // made a `NaN` nested inside an object compare unequal to itself, so the same\n // array was \"unique\" here and \"duplicated\" everywhere else.\n if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false\n if (depth >= MAX_EQUAL_DEPTH) return false\n const aArray = Array.isArray(a)\n const bArray = Array.isArray(b)\n if (aArray !== bArray) return false\n if (aArray) {\n const aa = a as unknown[]\n const bb = b as unknown[]\n if (aa.length !== bb.length) return false\n for (let i = 0; i < aa.length; i++) if (!valuesEqual(aa[i], bb[i], depth + 1)) return false\n return true\n }\n const ao = a as Record<string, unknown>\n const bo = b as Record<string, unknown>\n const keys = Object.keys(ao)\n if (keys.length !== Object.keys(bo).length) return false\n for (const key of keys) {\n if (!Object.hasOwn(bo, key) || !valuesEqual(ao[key], bo[key], depth + 1)) return false\n }\n return true\n}\n\n/**\n * A cheap, order-independent structural hash consistent with {@link valuesEqual}:\n * equal values always hash the same. It buckets candidate-equal elements in\n * {@link allUnique} so the exact comparison only ever runs inside a bucket.\n *\n * Object keys are folded commutatively (XOR) so key order does not change the\n * hash, and `NaN` / `-0` collapse the way SameValueZero does. Depth-capped like\n * {@link valuesEqual}: an over-deep value simply shares a bucket and is settled by\n * the (also capped) comparison, so the cap can cost a little time and never a\n * wrong answer. This is the same hash `@amritk/runtime-validators` uses.\n */\nconst structuralHash = (value: unknown, depth = 0): number => {\n if (value === null) return 0x1a2b3c\n const t = typeof value\n if (t === 'number') {\n const n = value as number\n return Number.isNaN(n) ? 0x7ff8 : n === 0 ? 0 : Math.trunc(n * 2654435761) | 0\n }\n if (t === 'string') {\n const s = value as string\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 0x01000193)\n return h | 0\n }\n if (t === 'boolean') return value ? 1 : 2\n if (t !== 'object') return 0x5eed\n if (depth >= MAX_EQUAL_DEPTH) return 0xdee9\n if (Array.isArray(value)) {\n let h = 0x12345 ^ value.length\n for (let i = 0; i < value.length; i++) h = (Math.imul(h, 31) + structuralHash(value[i], depth + 1)) | 0\n return h | 0\n }\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj)\n let h = 0xabcde ^ keys.length\n for (const k of keys) {\n let kh = 0x811c9dc5\n for (let i = 0; i < k.length; i++) kh = Math.imul(kh ^ k.charCodeAt(i), 0x01000193)\n h = (h ^ (Math.imul(kh, 0x9e3779b1) + structuralHash(obj[k], depth + 1))) | 0\n }\n return h | 0\n}\n\n/**\n * True when every element of `arr` is distinct under structural equality\n * ({@link valuesEqual}). Backs generated `uniqueItems` checks whose items may be\n * objects or arrays, where a `JSON.stringify` dedupe key would be key-order\n * sensitive and let a reordered-but-equal duplicate (`{ a: 1, b: 2 }` vs\n * `{ b: 2, a: 1 }`) slip through.\n *\n * A native `Set` dedupes the all-primitive case in one linear pass. Object and\n * array elements are bucketed by {@link structuralHash} first, so the exact\n * comparison runs only against elements that could actually be equal: an array of\n * distinct objects costs ~O(n) instead of the O(n\u00B2) an exhaustive pairwise sweep\n * charged \u2014 4 000 rows took over half a second of pure comparison before, which\n * is a lot to hand an unauthenticated caller.\n */\nexport const allUnique = (arr: readonly unknown[]): boolean => {\n const len = arr.length\n if (len < 2) return true\n let allPrimitive = true\n for (let i = 0; i < len; i++) {\n const v = arr[i]\n if (v !== null && typeof v === 'object') {\n allPrimitive = false\n break\n }\n }\n if (allPrimitive) return new Set(arr).size === len\n const buckets = new Map<number, unknown[]>()\n for (let i = 0; i < len; i++) {\n const item = arr[i]\n const hash = structuralHash(item)\n const bucket = buckets.get(hash)\n if (bucket === undefined) {\n buckets.set(hash, [item])\n continue\n }\n for (const seen of bucket) if (valuesEqual(seen, item)) return false\n bucket.push(item)\n }\n return true\n}\n\n/**\n * True when `test` holds for every element of `arr`, holes included. Backs the\n * item check inside a generated boolean guard.\n *\n * Not `Array.prototype.every`, because that *skips holes* in a sparse array\n * (`[, 'x']`), whereas the validator's index-based loop reads a hole as\n * `undefined` and rejects it \u2014 and the guard must never accept what the validator\n * rejects. The guard used to get that by materialising `Array.from(arr)` first,\n * which copied every array it looked at; an index loop reads a hole the same way\n * and copies nothing.\n */\nexport const everyItem = (arr: readonly unknown[], test: (item: unknown) => boolean): boolean => {\n for (let i = 0; i < arr.length; i++) if (!test(arr[i])) return false\n return true\n}\n\n/**\n * Escapes one JSON Pointer segment (RFC 6901): `~` \u2192 `~0`, `/` \u2192 `~1`, in that\n * order. Generated error paths are built from *runtime* keys wherever the schema\n * did not name them \u2014 a `patternProperties` match, an `additionalProperties`\n * sweep, a `propertyNames` loop \u2014 and a key containing a `/` would otherwise read\n * back as two segments, so an error on `{\"a/b\": \u2026}` pointed at `/a/b`, which is\n * the child `b` of a property `a`. Keys the schema *does* name are escaped at\n * generation time instead, and `@amritk/runtime-validators` escapes the same way,\n * so all three agree.\n *\n * The `indexOf` pre-test keeps the common key \u2014 no `/`, no `~` \u2014 off the replace\n * path entirely, which is what the interpreter does for the same reason.\n */\nexport const escapePointer = (key: string): string =>\n key.indexOf('/') !== -1 || key.indexOf('~') !== -1 ? key.replace(/~/g, '~0').replace(/\\//g, '~1') : key\n\n/**\n * The errors of the branch that was plainly the one meant, out of every branch a\n * failing `anyOf` / `oneOf` rejected. Empty when no branch stands out.\n *\n * A failing combinator on its own says almost nothing: \"must match a schema in\n * anyOf\" names no field and no reason, and on the shape this is most often used\n * for \u2014 a union where the value plainly *is* one of the variants and one field of\n * it is wrong \u2014 that is the least useful thing a validator can say. The branch\n * errors are computed anyway to answer the yes/no question, so the only question\n * is which of them are worth reporting.\n *\n * Branches that rejected the value's *kind* go first: a branch wanting a string\n * has nothing to say about an object, so a `string | { \u2026 }` union is left with\n * the one branch that was even talking about this value. When more than one\n * survives they all describe the same kind of value, and the tie is broken the\n * way a discriminated union reads from the outside \u2014 if every survivor but one\n * was rejected on the value's *identity* (a `const` or `enum` on the value or\n * one of its own properties), the remaining one is the variant the author meant.\n *\n * Nothing is reported when no branch stands out, which is as much as can be said\n * honestly: \"the branch with the fewest errors\" would answer here too, and\n * answers wrongly on `oneOf: [aReference, theActualThing]`, where \"you did not\n * write a $ref\" is one complaint and the real mistake is two.\n *\n * `path` is where the combinator was applied, so a segment below it is a direct\n * property of the value being judged. `@amritk/runtime-validators` selects the\n * same branch by the same rule, so a generated validator and the interpreter\n * explain a failing union the same way.\n */\nexport const selectBranchErrors = (\n branches: readonly (readonly ValidationError[])[],\n path: string,\n): readonly ValidationError[] => {\n const candidates: (readonly ValidationError[])[] = []\n for (const errors of branches) {\n // The value itself is `path`, so a `type` error there is a rejected kind.\n if (!errors.some((error) => error.keyword === 'type' && error.path === path)) candidates.push(errors)\n }\n if (candidates.length === 1) return candidates[0] as readonly ValidationError[]\n\n let selected: readonly ValidationError[] | null = null\n let rejectedOnIdentity = 0\n for (const errors of candidates) {\n // One segment below `path` and no deeper: a discriminator is conventionally a\n // direct field, and a `const` buried further down is far more likely to be an\n // ordinary payload constraint.\n const identity = errors.some(\n (error) =>\n (error.keyword === 'const' || error.keyword === 'enum') && error.path.indexOf('/', path.length + 1) === -1,\n )\n if (identity) {\n rejectedOnIdentity++\n continue\n }\n // Two branches survive the discriminator, so it did not discriminate.\n if (selected !== null) return []\n selected = errors\n }\n\n return selected !== null && rejectedOnIdentity === candidates.length - 1 ? selected : []\n}\n\n/**\n * The result of a generated repairing validator.\n *\n * `repairs` is not a second, parallel account of what went wrong \u2014 it *is* the\n * errors `validateX` produced, the ones a repair was found for. So a caller that\n * logs a repair logs the same path, keyword and params it would have been\n * rejected with, and the two can never drift apart, because there is only one of\n * them.\n *\n * A document that needed no repair comes back `valid: true` with an empty\n * `repairs`, which is exactly what `coerceX` would have returned. One that was\n * fully repaired is `valid: true` with a non-empty `repairs` \u2014 the caller\n * decides whether that is acceptable by looking, rather than by being told. One\n * that could not be fully repaired is `valid: false` and carries both: the\n * repairs that were applied, and the `errors` still outstanding against the\n * value handed back.\n */\nexport type RepairResult<T> =\n | { valid: true; value: T; repairs: ValidationError[] }\n | { valid: false; value: unknown; errors: ValidationError[]; repairs: ValidationError[] }\n\n/**\n * Looks up the repaired value for one position, given that position's JSON\n * Pointer split into segments. Returns a thunk rather than a value so each\n * repair gets its own object \u2014 a shared literal would alias every site that\n * repaired to it \u2014 and `null` where the schema offers nothing to repair toward.\n */\nexport type RepairLookup = (segments: readonly string[]) => (() => unknown) | null\n\n/**\n * How many validate-and-repair rounds `repairX` will run. Repairing a child can\n * expose a parent that only became checkable once the child was there, so one\n * round is not always enough; but each round is bounded further by the rule that\n * a position is repaired at most once, so this is a backstop rather than the\n * thing doing the terminating.\n */\nexport const MAX_REPAIR_PASSES = 8\n\n/**\n * Splits a JSON Pointer into its segments, undoing the escaping\n * {@link escapePointer} applied. The root pointer (`''`) is zero segments.\n */\nexport const pointerSegments = (path: string): string[] =>\n path === ''\n ? []\n : path\n .slice(1)\n .split('/')\n .map((segment) =>\n segment.indexOf('~') === -1 ? segment : segment.replace(/~1/g, '/').replace(/~0/g, '~'),\n )\n\n/**\n * Returns `root` with the position at `segments` replaced by `value`, copying\n * only the containers along the way.\n *\n * Copying rather than writing is not politeness, it is the same promise\n * `coerceX` makes: the caller's input is never modified, so a repaired document\n * can be compared against what was actually sent. Everything the path did not\n * touch is shared, so repairing one field of a large document does not clone it.\n *\n * Returns `root` unchanged when the path does not lead anywhere \u2014 a segment into\n * a missing or non-container parent. That is not a failure to handle here: the\n * parent has an error of its own, and repairing *it* is what makes this position\n * reachable on a later pass.\n */\nexport const repairAt = (root: unknown, segments: readonly string[], value: unknown): unknown => {\n if (segments.length === 0) return value\n const [head, ...rest] = segments as [string, ...string[]]\n\n if (Array.isArray(root)) {\n const index = Number(head)\n // `index === root.length` is an append, which is what padding a short array\n // to its `minItems` is made of. Anything past that would leave a hole, and a\n // hole is not a repair.\n if (!Number.isInteger(index) || index < 0 || index > root.length) return root\n const next = repairAt(root[index], rest, value)\n if (next === root[index]) return root\n const copy = [...root]\n copy[index] = next\n return copy\n }\n\n if (typeof root !== 'object' || root === null) return root\n const obj = root as Record<string, unknown>\n // A missing key is reachable only when this is the last segment: that is the\n // `required` repair, which is putting the key there. Deeper than that and the\n // parent is the thing that needs repairing first.\n if (!Object.hasOwn(obj, head) && rest.length > 0) return root\n const next = repairAt(obj[head], rest, value)\n if (next === obj[head] && Object.hasOwn(obj, head)) return root\n return { ...obj, [head]: next }\n}\n\n/**\n * The position one error is asking to have repaired, or `null` when the error is\n * not about a position a value can be put at.\n *\n * One keyword does not point at the value that is wrong: a `required` error is\n * reported against the *object* that is missing the key, and names the key in\n * `params.missingProperty`, so the position to fill is one segment deeper.\n * `minItems` is the other exception and is handled separately, because it is\n * satisfied by adding several values rather than by replacing one.\n */\nconst repairTarget = (error: ValidationError): readonly string[] | null => {\n const segments = pointerSegments(error.path)\n if (error.keyword === 'required') {\n const missing = error.params['missingProperty']\n return typeof missing === 'string' ? [...segments, missing] : null\n }\n return segments\n}\n\n/** The value at a pointer, or `undefined` when the path does not lead anywhere. */\nconst valueAt = (root: unknown, segments: readonly string[]): unknown =>\n segments.reduce<unknown>(\n (node, segment) =>\n Array.isArray(node)\n ? node[Number(segment)]\n : typeof node === 'object' && node !== null\n ? (node as Record<string, unknown>)[segment]\n : undefined,\n root,\n )\n\n/**\n * Grows a short array to the length `minItems` asks for, or `null` when it\n * cannot.\n *\n * Padding is its own operation rather than a position repair because one\n * `minItems` error is satisfied by *several* values, not one. Doing it a single\n * element per round would work, but it would spend a round per element and a\n * `minItems: 50` would run out of them; filling the shortfall at once keeps the\n * number of rounds a question about the shape of the document rather than about\n * the size of its arrays.\n *\n * Every added element is a separate call to the thunk, so no two share an object.\n */\nconst padToMinItems = (\n root: unknown,\n error: ValidationError,\n lookup: RepairLookup,\n done: Set<string>,\n): unknown => {\n const segments = pointerSegments(error.path)\n const array = valueAt(root, segments)\n const limit = error.params['limit']\n if (!Array.isArray(array) || typeof limit !== 'number' || array.length >= limit) return null\n\n // Keyed apart from a position repair at the same path: replacing the whole\n // array and padding it are different repairs, and having done one is no reason\n // to refuse the other.\n const key = `${segments.join('/')}#minItems`\n if (done.has(key)) return null\n\n const element = lookup([...segments, String(array.length)])\n if (element === null) return null\n\n const padded = [...array]\n while (padded.length < limit) padded.push(element())\n const updated = repairAt(root, segments, padded)\n if (updated === root) return null\n done.add(key)\n return updated\n}\n\n/**\n * Applies one round of repairs: every error the schema offers a value for is\n * repaired, and the rest are handed back untouched.\n *\n * `done` carries the positions already repaired on an earlier round. A position\n * that failed again after being repaired is not repaired a second time \u2014 the\n * value the schema offered did not satisfy the schema, which is a fact about the\n * schema, and trying again would only produce the same value. Refusing keeps the\n * loop finite without relying on the pass cap, and turns the position into an\n * honest error instead of a silent spin.\n */\nexport const applyRepairs = (\n value: unknown,\n errors: readonly ValidationError[],\n lookup: RepairLookup,\n done: Set<string>,\n): { value: unknown; repaired: ValidationError[] } => {\n const repaired: ValidationError[] = []\n let next = value\n\n for (const error of errors) {\n if (error.keyword === 'minItems') {\n const padded = padToMinItems(next, error, lookup, done)\n if (padded === null) continue\n next = padded\n repaired.push(error)\n continue\n }\n\n const target = repairTarget(error)\n if (target === null) continue\n const key = target.join('/')\n if (done.has(key)) continue\n const to = lookup(target)\n if (to === null) continue\n\n const updated = repairAt(next, target, to())\n // An unreachable position leaves the document exactly as it was. Not counting\n // it as repaired is what lets the parent's own error be the thing that\n // reports, rather than this silently claiming a fix that did not land.\n if (updated === next) continue\n done.add(key)\n next = updated\n repaired.push(error)\n }\n\n return { value: next, repaired }\n}\n";
15
+ export declare const VALIDATION_RESULT_CONTENT = "/**\n * A single validation error: what went wrong, where, and which keyword said so.\n *\n * The shape matches `@amritk/runtime-validators`, so an error from a generated\n * validator and one from the runtime interpreter can be handled by the same\n * code \u2014 grouped, translated, or branched on \u2014 without knowing which produced it.\n */\nexport type ValidationError = {\n /** Human-readable description of what went wrong. */\n message: string\n /** JSON Pointer to the offending value inside the instance. */\n path: string\n /** The JSON Schema keyword that rejected the value \u2014 `type`, `required`, `minimum`, \u2026 */\n keyword: string\n /**\n * The keyword's own values, as far as they explain the failure: the bound that\n * was exceeded, the property that was missing, the allowed values that were not\n * matched. Empty for a keyword with nothing to add beyond its name.\n */\n params: Record<string, unknown>\n}\n\n/**\n * The result of a generated validator function.\n * Returns `true` when the input is valid, or an object with `valid: false`\n * and a list of errors when it is not.\n */\nexport type ValidationResult = true | { valid: false; errors: ValidationError[] }\n\n/**\n * The result of a generated coercing validator.\n *\n * Unlike `ValidationResult` there is no bare `true`: a caller that coerces wants\n * the value back, and the whole point is that it may differ from what went in.\n * The input is never modified \u2014 `value` is the input itself when nothing needed\n * coercing, and otherwise a copy that shares everything the coercion did not\n * touch. So a caller does not have to clone defensively the way an in-place\n * coercer forces them to.\n */\nexport type CoercionResult<T> = { valid: true; value: T } | { valid: false; errors: ValidationError[] }\n\n/**\n * A string this will read as a number: an optional sign, digits with an optional\n * fractional part, an optional exponent. Nothing else.\n *\n * Deliberately narrower than what `Number()` accepts, which is where Ajv gets\n * its surprises \u2014 `Number(\" \")` is `0`, `Number(\"0x10\")` is `16` and\n * `Number(\"Infinity\")` is a value JSON cannot even represent. A config that says\n * `retries: \" \"` has a mistake in it, and answering `0` is the one thing worse\n * than rejecting it. Leading zeros (`\"007\"`) and exponents (`\"1e3\"`) stay: both\n * are ordinary ways to write a number in a YAML file, and neither is ambiguous.\n */\nconst isNumericString = (text: string): boolean => {\n // `/^[+-]?(?:\\d+|\\d*\\.\\d+)(?:[eE][+-]?\\d+)?$/`, walked by hand. The regex\n // cost more than the rest of a number coercion put together \u2014 a string past\n // its end reads `NaN` from `charCodeAt`, which fails every test below, so\n // the scan needs no bounds checks of its own.\n let i = 0\n let c = text.charCodeAt(0)\n if (c === 43 || c === 45) c = text.charCodeAt(++i)\n let digits = 0\n while (c >= 48 && c <= 57) {\n digits++\n c = text.charCodeAt(++i)\n }\n if (c === 46) {\n c = text.charCodeAt(++i)\n let fraction = 0\n while (c >= 48 && c <= 57) {\n fraction++\n c = text.charCodeAt(++i)\n }\n if (fraction === 0) return false\n } else if (digits === 0) return false\n if (c === 101 || c === 69) {\n c = text.charCodeAt(++i)\n if (c === 43 || c === 45) c = text.charCodeAt(++i)\n let exponent = 0\n while (c >= 48 && c <= 57) {\n exponent++\n c = text.charCodeAt(++i)\n }\n if (exponent === 0) return false\n }\n return i === text.length\n}\n\n/**\n * One scalar, coerced toward `type`, or returned untouched when that is not\n * possible.\n *\n * Returning the original on failure is what keeps the error honest: nothing is\n * substituted, so the validator that runs next rejects the value the caller\n * actually wrote, with the keyword and params that rejected it. That is the\n * difference between this and a parser's repair, which repairs toward a default\n * and leaves nothing to report.\n *\n * The table is Ajv's `coerceTypes` minus the cells where Ajv guesses. Every\n * value this coerces, Ajv coerces to the same value \u2014 `coerced-vs-ajv` pins that\n * as a property, so moving off Ajv never changes a value, it only turns some of\n * Ajv's silent repairs into errors. What is deliberately *not* coerced:\n *\n * - **Anything to or from `null`.** Ajv reads `null` as `\"\"`, `0` and\n * `false`, and reads `\"\"`, `0` and `false` back as `null`. `null` is a\n * JSON value in its own right and usually means \"not set\"; turning it into an\n * empty string, or an empty string into it, loses the distinction the document\n * drew.\n * - **Strings that are not cleanly numeric** ({@link isNumericString}) \u2014 no\n * whitespace padding, no `0x`/`0o`/`0b`, no `Infinity`, no trailing `.`.\n */\nexport const coerceScalar = (value: unknown, type: string): unknown => {\n switch (type) {\n case 'string':\n return typeof value === 'number' || typeof value === 'boolean' ? String(value) : value\n case 'number':\n case 'integer': {\n if (typeof value === 'boolean') return value ? 1 : 0\n if (typeof value !== 'string' || !isNumericString(value)) return value\n const asNumber = Number(value)\n if (!Number.isFinite(asNumber)) return value\n return type === 'integer' && asNumber % 1 !== 0 ? value : asNumber\n }\n case 'boolean':\n if (value === 'true' || value === 1) return true\n if (value === 'false' || value === 0) return false\n return value\n default:\n return value\n }\n}\n\n/**\n * One scalar at a position that offers several types \u2014 a `type` array, or a\n * union of scalar branches \u2014 coerced only when every type that can take it\n * agrees on the result.\n *\n * A value whose type is already one of the offered types is left alone: it is\n * what the schema asked for, and the question of coercion does not arise. That\n * one rule is where this parts company with Ajv, which walks its own coercion\n * list in order and so turns `\"1\"` into `1` under `[\"number\", \"string\"]` while\n * leaving it a string under `[\"string\", \"number\"]`. The answer should not depend\n * on the order someone wrote the union in.\n *\n * Otherwise every offered type is tried, and the coercion is taken only if the\n * types that succeed all agree on it. `true` against `number | string` could be\n * `1` or `\"true\"` with equal justification, so it stays `true` and the\n * validator says what is wrong with it. `\"1\"` against `number | integer` is `1`\n * either way, so it is `1`.\n */\nexport const coerceUnion = (value: unknown, types: readonly string[]): unknown => {\n const actual = value === null ? 'null' : typeof value\n for (const type of types) {\n // `integer` is satisfied by a number, so a non-integral number is not a\n // *type* mismatch here \u2014 the validator is what holds it to being whole.\n if (actual === type || (actual === 'number' && type === 'integer')) return value\n }\n\n let coerced: unknown = value\n for (const type of types) {\n const candidate = coerceScalar(value, type)\n if (candidate === value) continue\n if (coerced === value) coerced = candidate\n else if (candidate !== coerced) return value\n }\n return coerced\n}\n\n/**\n * How deep a structural comparison walks before it gives up and answers \"not\n * equal\".\n *\n * JSON data is acyclic, but a generated validator is a plain function applied to\n * whatever in-memory value a caller hands it \u2014 and a self-referential object\n * reaching a `const` / `enum` / `uniqueItems` check used to recurse until the\n * stack overflowed, so `validateFoo` threw a `RangeError` instead of returning\n * the `ValidationResult` its signature promises. The cap turns that into an\n * ordinary \"these are different\" without ever coming near real data;\n * `@amritk/runtime-validators` guards its own `deepEqual` at the same depth.\n */\nconst MAX_EQUAL_DEPTH = 512\n\n/**\n * Structural deep equality used by generated `const` checks. Objects compare by\n * their key sets rather than serialization, so `{ a: 1, b: 2 }` and\n * `{ b: 2, a: 1 }` are equal \u2014 unlike `JSON.stringify`, which is key-order\n * sensitive and would reject a reordered-but-equal value.\n */\nexport const valuesEqual = (a: unknown, b: unknown, depth = 0): boolean => {\n // SameValueZero: `===` settles every primitive except `NaN`, which counts as\n // equal to itself here. That is what the native `Set` in {@link allUnique} does,\n // what Ajv does, and what the interpreter's `deepEqual` does \u2014 leaving it out\n // made a `NaN` nested inside an object compare unequal to itself, so the same\n // array was \"unique\" here and \"duplicated\" everywhere else.\n if (a === b || (Number.isNaN(a) && Number.isNaN(b))) return true\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false\n if (depth >= MAX_EQUAL_DEPTH) return false\n const aArray = Array.isArray(a)\n const bArray = Array.isArray(b)\n if (aArray !== bArray) return false\n if (aArray) {\n const aa = a as unknown[]\n const bb = b as unknown[]\n if (aa.length !== bb.length) return false\n for (let i = 0; i < aa.length; i++) if (!valuesEqual(aa[i], bb[i], depth + 1)) return false\n return true\n }\n const ao = a as Record<string, unknown>\n const bo = b as Record<string, unknown>\n const keys = Object.keys(ao)\n if (keys.length !== Object.keys(bo).length) return false\n for (const key of keys) {\n if (!Object.hasOwn(bo, key) || !valuesEqual(ao[key], bo[key], depth + 1)) return false\n }\n return true\n}\n\n/**\n * A cheap, order-independent structural hash consistent with {@link valuesEqual}:\n * equal values always hash the same. It buckets candidate-equal elements in\n * {@link allUnique} so the exact comparison only ever runs inside a bucket.\n *\n * Object keys are folded commutatively (XOR) so key order does not change the\n * hash, and `NaN` / `-0` collapse the way SameValueZero does. Depth-capped like\n * {@link valuesEqual}: an over-deep value simply shares a bucket and is settled by\n * the (also capped) comparison, so the cap can cost a little time and never a\n * wrong answer. This is the same hash `@amritk/runtime-validators` uses.\n */\nconst structuralHash = (value: unknown, depth = 0): number => {\n if (value === null) return 0x1a2b3c\n const t = typeof value\n if (t === 'number') {\n const n = value as number\n return Number.isNaN(n) ? 0x7ff8 : n === 0 ? 0 : Math.trunc(n * 2654435761) | 0\n }\n if (t === 'string') {\n const s = value as string\n let h = 0x811c9dc5\n for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 0x01000193)\n return h | 0\n }\n if (t === 'boolean') return value ? 1 : 2\n if (t !== 'object') return 0x5eed\n if (depth >= MAX_EQUAL_DEPTH) return 0xdee9\n if (Array.isArray(value)) {\n let h = 0x12345 ^ value.length\n for (let i = 0; i < value.length; i++) h = (Math.imul(h, 31) + structuralHash(value[i], depth + 1)) | 0\n return h | 0\n }\n const obj = value as Record<string, unknown>\n const keys = Object.keys(obj)\n let h = 0xabcde ^ keys.length\n for (const k of keys) {\n let kh = 0x811c9dc5\n for (let i = 0; i < k.length; i++) kh = Math.imul(kh ^ k.charCodeAt(i), 0x01000193)\n h = (h ^ (Math.imul(kh, 0x9e3779b1) + structuralHash(obj[k], depth + 1))) | 0\n }\n return h | 0\n}\n\n/**\n * True when every element of `arr` is distinct under structural equality\n * ({@link valuesEqual}). Backs generated `uniqueItems` checks whose items may be\n * objects or arrays, where a `JSON.stringify` dedupe key would be key-order\n * sensitive and let a reordered-but-equal duplicate (`{ a: 1, b: 2 }` vs\n * `{ b: 2, a: 1 }`) slip through.\n *\n * A native `Set` dedupes the all-primitive case in one linear pass. Object and\n * array elements are bucketed by {@link structuralHash} first, so the exact\n * comparison runs only against elements that could actually be equal: an array of\n * distinct objects costs ~O(n) instead of the O(n\u00B2) an exhaustive pairwise sweep\n * charged \u2014 4 000 rows took over half a second of pure comparison before, which\n * is a lot to hand an unauthenticated caller.\n */\nexport const allUnique = (arr: readonly unknown[]): boolean => {\n const len = arr.length\n if (len < 2) return true\n let allPrimitive = true\n for (let i = 0; i < len; i++) {\n const v = arr[i]\n if (v !== null && typeof v === 'object') {\n allPrimitive = false\n break\n }\n }\n if (allPrimitive) return new Set(arr).size === len\n const buckets = new Map<number, unknown[]>()\n for (let i = 0; i < len; i++) {\n const item = arr[i]\n const hash = structuralHash(item)\n const bucket = buckets.get(hash)\n if (bucket === undefined) {\n buckets.set(hash, [item])\n continue\n }\n for (const seen of bucket) if (valuesEqual(seen, item)) return false\n bucket.push(item)\n }\n return true\n}\n\n/**\n * True when `test` holds for every element of `arr`, holes included. Backs the\n * item check inside a generated boolean guard.\n *\n * Not `Array.prototype.every`, because that *skips holes* in a sparse array\n * (`[, 'x']`), whereas the validator's index-based loop reads a hole as\n * `undefined` and rejects it \u2014 and the guard must never accept what the validator\n * rejects. The guard used to get that by materialising `Array.from(arr)` first,\n * which copied every array it looked at; an index loop reads a hole the same way\n * and copies nothing.\n */\nexport const everyItem = (arr: readonly unknown[], test: (item: unknown) => boolean): boolean => {\n for (let i = 0; i < arr.length; i++) if (!test(arr[i])) return false\n return true\n}\n\n/**\n * Escapes one JSON Pointer segment (RFC 6901): `~` \u2192 `~0`, `/` \u2192 `~1`, in that\n * order. Generated error paths are built from *runtime* keys wherever the schema\n * did not name them \u2014 a `patternProperties` match, an `additionalProperties`\n * sweep, a `propertyNames` loop \u2014 and a key containing a `/` would otherwise read\n * back as two segments, so an error on `{\"a/b\": \u2026}` pointed at `/a/b`, which is\n * the child `b` of a property `a`. Keys the schema *does* name are escaped at\n * generation time instead, and `@amritk/runtime-validators` escapes the same way,\n * so all three agree.\n *\n * The `indexOf` pre-test keeps the common key \u2014 no `/`, no `~` \u2014 off the replace\n * path entirely, which is what the interpreter does for the same reason.\n */\nexport const escapePointer = (key: string): string =>\n key.indexOf('/') !== -1 || key.indexOf('~') !== -1 ? key.replace(/~/g, '~0').replace(/\\//g, '~1') : key\n\n/**\n * The errors of the branch that was plainly the one meant, out of every branch a\n * failing `anyOf` / `oneOf` rejected. Empty when no branch stands out.\n *\n * A failing combinator on its own says almost nothing: \"must match a schema in\n * anyOf\" names no field and no reason, and on the shape this is most often used\n * for \u2014 a union where the value plainly *is* one of the variants and one field of\n * it is wrong \u2014 that is the least useful thing a validator can say. The branch\n * errors are computed anyway to answer the yes/no question, so the only question\n * is which of them are worth reporting.\n *\n * Branches that rejected the value's *kind* go first: a branch wanting a string\n * has nothing to say about an object, so a `string | { \u2026 }` union is left with\n * the one branch that was even talking about this value. When more than one\n * survives they all describe the same kind of value, and the tie is broken the\n * way a discriminated union reads from the outside \u2014 if every survivor but one\n * was rejected on the value's *identity* (a `const` or `enum` on the value or\n * one of its own properties), the remaining one is the variant the author meant.\n *\n * When every branch rejected the value's kind, no branch was meant, but the\n * kinds they wanted together are what the union accepts, so that is reported as\n * one `type` error: \"must be object or boolean\" for a `true` written as the\n * string `'true'`. `total` is how many branches the combinator has, and the kinds\n * are only named when all of them were heard from. A branch that could not\n * explain itself might have accepted this kind, and then the list would be wrong.\n *\n * Otherwise nothing is reported when no branch stands out, which is as much as\n * can be said honestly: \"the branch with the fewest errors\" would answer here\n * too, and answers wrongly on `oneOf: [aReference, theActualThing]`, where \"you\n * did not write a $ref\" is one complaint and the real mistake is two.\n *\n * `path` is where the combinator was applied, so a segment below it is a direct\n * property of the value being judged. `@amritk/runtime-validators` selects the\n * same branch by the same rule, so a generated validator and the interpreter\n * explain a failing union the same way.\n */\nexport const selectBranchErrors = (\n branches: readonly (readonly ValidationError[])[],\n path: string,\n total?: number,\n): readonly ValidationError[] => {\n const candidates: (readonly ValidationError[])[] = []\n for (const errors of branches) {\n // The value itself is `path`, so a `type` error there is a rejected kind.\n if (!errors.some((error) => error.keyword === 'type' && error.path === path)) candidates.push(errors)\n }\n if (candidates.length === 1) return candidates[0] as readonly ValidationError[]\n if (candidates.length === 0) return branches.length === total ? acceptedKinds(branches, path) : []\n\n let selected: readonly ValidationError[] | null = null\n let rejectedOnIdentity = 0\n for (const errors of candidates) {\n // One segment below `path` and no deeper: a discriminator is conventionally a\n // direct field, and a `const` buried further down is far more likely to be an\n // ordinary payload constraint.\n const identity = errors.some(\n (error) =>\n (error.keyword === 'const' || error.keyword === 'enum') && error.path.indexOf('/', path.length + 1) === -1,\n )\n if (identity) {\n rejectedOnIdentity++\n continue\n }\n // Two branches survive the discriminator, so it did not discriminate.\n if (selected !== null) return []\n selected = errors\n }\n\n return selected !== null && rejectedOnIdentity === candidates.length - 1 ? selected : []\n}\n\n/**\n * One `type` error naming every kind the branches asked for, in branch order and\n * once each. A branch's own `type` may be a list, and a nested union reports its\n * kinds as a list too, so both are read flat.\n */\nconst acceptedKinds = (\n branches: readonly (readonly ValidationError[])[],\n path: string,\n): readonly ValidationError[] => {\n const kinds: string[] = []\n for (const errors of branches) {\n for (const error of errors) {\n if (error.keyword !== 'type' || error.path !== path) continue\n const type = error.params['type']\n for (const kind of Array.isArray(type) ? type : [type]) {\n if (typeof kind === 'string' && !kinds.includes(kind)) kinds.push(kind)\n }\n }\n }\n if (kinds.length === 0) return []\n return [\n {\n message: 'must be ' + kinds.join(' or '),\n path,\n keyword: 'type',\n params: { type: kinds.length === 1 ? kinds[0] : kinds },\n },\n ]\n}\n\n/**\n * The result of a generated repairing validator.\n *\n * `repairs` is not a second, parallel account of what went wrong \u2014 it *is* the\n * errors `validateX` produced, the ones a repair was found for. So a caller that\n * logs a repair logs the same path, keyword and params it would have been\n * rejected with, and the two can never drift apart, because there is only one of\n * them.\n *\n * A document that needed no repair comes back `valid: true` with an empty\n * `repairs`, which is exactly what `coerceX` would have returned. One that was\n * fully repaired is `valid: true` with a non-empty `repairs` \u2014 the caller\n * decides whether that is acceptable by looking, rather than by being told. One\n * that could not be fully repaired is `valid: false` and carries both: the\n * repairs that were applied, and the `errors` still outstanding against the\n * value handed back.\n */\nexport type RepairResult<T> =\n | { valid: true; value: T; repairs: ValidationError[] }\n | { valid: false; value: unknown; errors: ValidationError[]; repairs: ValidationError[] }\n\n/**\n * Looks up the repaired value for one position, given that position's JSON\n * Pointer split into segments. Returns a thunk rather than a value so each\n * repair gets its own object \u2014 a shared literal would alias every site that\n * repaired to it \u2014 and `null` where the schema offers nothing to repair toward.\n */\nexport type RepairLookup = (segments: readonly string[]) => (() => unknown) | null\n\n/**\n * How many validate-and-repair rounds `repairX` will run. Repairing a child can\n * expose a parent that only became checkable once the child was there, so one\n * round is not always enough; but each round is bounded further by the rule that\n * a position is repaired at most once, so this is a backstop rather than the\n * thing doing the terminating.\n */\nexport const MAX_REPAIR_PASSES = 8\n\n/**\n * Splits a JSON Pointer into its segments, undoing the escaping\n * {@link escapePointer} applied. The root pointer (`''`) is zero segments.\n */\nexport const pointerSegments = (path: string): string[] =>\n path === ''\n ? []\n : path\n .slice(1)\n .split('/')\n .map((segment) =>\n segment.indexOf('~') === -1 ? segment : segment.replace(/~1/g, '/').replace(/~0/g, '~'),\n )\n\n/**\n * Returns `root` with the position at `segments` replaced by `value`, copying\n * only the containers along the way.\n *\n * Copying rather than writing is not politeness, it is the same promise\n * `coerceX` makes: the caller's input is never modified, so a repaired document\n * can be compared against what was actually sent. Everything the path did not\n * touch is shared, so repairing one field of a large document does not clone it.\n *\n * Returns `root` unchanged when the path does not lead anywhere \u2014 a segment into\n * a missing or non-container parent. That is not a failure to handle here: the\n * parent has an error of its own, and repairing *it* is what makes this position\n * reachable on a later pass.\n */\nexport const repairAt = (root: unknown, segments: readonly string[], value: unknown): unknown => {\n if (segments.length === 0) return value\n const [head, ...rest] = segments as [string, ...string[]]\n\n if (Array.isArray(root)) {\n const index = Number(head)\n // `index === root.length` is an append, which is what padding a short array\n // to its `minItems` is made of. Anything past that would leave a hole, and a\n // hole is not a repair.\n if (!Number.isInteger(index) || index < 0 || index > root.length) return root\n const next = repairAt(root[index], rest, value)\n if (next === root[index]) return root\n const copy = [...root]\n copy[index] = next\n return copy\n }\n\n if (typeof root !== 'object' || root === null) return root\n const obj = root as Record<string, unknown>\n // A missing key is reachable only when this is the last segment: that is the\n // `required` repair, which is putting the key there. Deeper than that and the\n // parent is the thing that needs repairing first.\n if (!Object.hasOwn(obj, head) && rest.length > 0) return root\n const next = repairAt(obj[head], rest, value)\n if (next === obj[head] && Object.hasOwn(obj, head)) return root\n return { ...obj, [head]: next }\n}\n\n/**\n * The position one error is asking to have repaired, or `null` when the error is\n * not about a position a value can be put at.\n *\n * One keyword does not point at the value that is wrong: a `required` error is\n * reported against the *object* that is missing the key, and names the key in\n * `params.missingProperty`, so the position to fill is one segment deeper.\n * `minItems` is the other exception and is handled separately, because it is\n * satisfied by adding several values rather than by replacing one.\n */\nconst repairTarget = (error: ValidationError): readonly string[] | null => {\n const segments = pointerSegments(error.path)\n if (error.keyword === 'required') {\n const missing = error.params['missingProperty']\n return typeof missing === 'string' ? [...segments, missing] : null\n }\n return segments\n}\n\n/** The value at a pointer, or `undefined` when the path does not lead anywhere. */\nconst valueAt = (root: unknown, segments: readonly string[]): unknown =>\n segments.reduce<unknown>(\n (node, segment) =>\n Array.isArray(node)\n ? node[Number(segment)]\n : typeof node === 'object' && node !== null\n ? (node as Record<string, unknown>)[segment]\n : undefined,\n root,\n )\n\n/**\n * Grows a short array to the length `minItems` asks for, or `null` when it\n * cannot.\n *\n * Padding is its own operation rather than a position repair because one\n * `minItems` error is satisfied by *several* values, not one. Doing it a single\n * element per round would work, but it would spend a round per element and a\n * `minItems: 50` would run out of them; filling the shortfall at once keeps the\n * number of rounds a question about the shape of the document rather than about\n * the size of its arrays.\n *\n * Every added element is a separate call to the thunk, so no two share an object.\n */\nconst padToMinItems = (\n root: unknown,\n error: ValidationError,\n lookup: RepairLookup,\n done: Set<string>,\n): unknown => {\n const segments = pointerSegments(error.path)\n const array = valueAt(root, segments)\n const limit = error.params['limit']\n if (!Array.isArray(array) || typeof limit !== 'number' || array.length >= limit) return null\n\n // Keyed apart from a position repair at the same path: replacing the whole\n // array and padding it are different repairs, and having done one is no reason\n // to refuse the other.\n const key = `${segments.join('/')}#minItems`\n if (done.has(key)) return null\n\n const element = lookup([...segments, String(array.length)])\n if (element === null) return null\n\n const padded = [...array]\n while (padded.length < limit) padded.push(element())\n const updated = repairAt(root, segments, padded)\n if (updated === root) return null\n done.add(key)\n return updated\n}\n\n/**\n * Applies one round of repairs: every error the schema offers a value for is\n * repaired, and the rest are handed back untouched.\n *\n * `done` carries the positions already repaired on an earlier round. A position\n * that failed again after being repaired is not repaired a second time \u2014 the\n * value the schema offered did not satisfy the schema, which is a fact about the\n * schema, and trying again would only produce the same value. Refusing keeps the\n * loop finite without relying on the pass cap, and turns the position into an\n * honest error instead of a silent spin.\n */\nexport const applyRepairs = (\n value: unknown,\n errors: readonly ValidationError[],\n lookup: RepairLookup,\n done: Set<string>,\n): { value: unknown; repaired: ValidationError[] } => {\n const repaired: ValidationError[] = []\n let next = value\n\n for (const error of errors) {\n if (error.keyword === 'minItems') {\n const padded = padToMinItems(next, error, lookup, done)\n if (padded === null) continue\n next = padded\n repaired.push(error)\n continue\n }\n\n const target = repairTarget(error)\n if (target === null) continue\n const key = target.join('/')\n if (done.has(key)) continue\n const to = lookup(target)\n if (to === null) continue\n\n const updated = repairAt(next, target, to())\n // An unreachable position leaves the document exactly as it was. Not counting\n // it as repaired is what lets the parent's own error be the thing that\n // reports, rather than this silently claiming a fix that did not land.\n if (updated === next) continue\n done.add(key)\n next = updated\n repaired.push(error)\n }\n\n return { value: next, repaired }\n}\n";
16
16
  /**
17
17
  * Builds all TypeScript validator files from a JSON Schema by traversing all
18
18
  * `$ref` / `$dynamicRef` references recursively (via the shared
@@ -354,10 +354,17 @@ export const escapePointer = (key: string): string =>
354
354
  * was rejected on the value's *identity* (a \`const\` or \`enum\` on the value or
355
355
  * one of its own properties), the remaining one is the variant the author meant.
356
356
  *
357
- * Nothing is reported when no branch stands out, which is as much as can be said
358
- * honestly: "the branch with the fewest errors" would answer here too, and
359
- * answers wrongly on \`oneOf: [aReference, theActualThing]\`, where "you did not
360
- * write a $ref" is one complaint and the real mistake is two.
357
+ * When every branch rejected the value's kind, no branch was meant, but the
358
+ * kinds they wanted together are what the union accepts, so that is reported as
359
+ * one \`type\` error: "must be object or boolean" for a \`true\` written as the
360
+ * string \`'true'\`. \`total\` is how many branches the combinator has, and the kinds
361
+ * are only named when all of them were heard from. A branch that could not
362
+ * explain itself might have accepted this kind, and then the list would be wrong.
363
+ *
364
+ * Otherwise nothing is reported when no branch stands out, which is as much as
365
+ * can be said honestly: "the branch with the fewest errors" would answer here
366
+ * too, and answers wrongly on \`oneOf: [aReference, theActualThing]\`, where "you
367
+ * did not write a $ref" is one complaint and the real mistake is two.
361
368
  *
362
369
  * \`path\` is where the combinator was applied, so a segment below it is a direct
363
370
  * property of the value being judged. \`@amritk/runtime-validators\` selects the
@@ -367,6 +374,7 @@ export const escapePointer = (key: string): string =>
367
374
  export const selectBranchErrors = (
368
375
  branches: readonly (readonly ValidationError[])[],
369
376
  path: string,
377
+ total?: number,
370
378
  ): readonly ValidationError[] => {
371
379
  const candidates: (readonly ValidationError[])[] = []
372
380
  for (const errors of branches) {
@@ -374,6 +382,7 @@ export const selectBranchErrors = (
374
382
  if (!errors.some((error) => error.keyword === 'type' && error.path === path)) candidates.push(errors)
375
383
  }
376
384
  if (candidates.length === 1) return candidates[0] as readonly ValidationError[]
385
+ if (candidates.length === 0) return branches.length === total ? acceptedKinds(branches, path) : []
377
386
 
378
387
  let selected: readonly ValidationError[] | null = null
379
388
  let rejectedOnIdentity = 0
@@ -397,6 +406,36 @@ export const selectBranchErrors = (
397
406
  return selected !== null && rejectedOnIdentity === candidates.length - 1 ? selected : []
398
407
  }
399
408
 
409
+ /**
410
+ * One \`type\` error naming every kind the branches asked for, in branch order and
411
+ * once each. A branch's own \`type\` may be a list, and a nested union reports its
412
+ * kinds as a list too, so both are read flat.
413
+ */
414
+ const acceptedKinds = (
415
+ branches: readonly (readonly ValidationError[])[],
416
+ path: string,
417
+ ): readonly ValidationError[] => {
418
+ const kinds: string[] = []
419
+ for (const errors of branches) {
420
+ for (const error of errors) {
421
+ if (error.keyword !== 'type' || error.path !== path) continue
422
+ const type = error.params['type']
423
+ for (const kind of Array.isArray(type) ? type : [type]) {
424
+ if (typeof kind === 'string' && !kinds.includes(kind)) kinds.push(kind)
425
+ }
426
+ }
427
+ }
428
+ if (kinds.length === 0) return []
429
+ return [
430
+ {
431
+ message: 'must be ' + kinds.join(' or '),
432
+ path,
433
+ keyword: 'type',
434
+ params: { type: kinds.length === 1 ? kinds[0] : kinds },
435
+ },
436
+ ]
437
+ }
438
+
400
439
  /**
401
440
  * The result of a generated repairing validator.
402
441
  *
@@ -25,7 +25,10 @@ const generateValidatorFile = (schema, typeName, options) => {
25
25
  formats,
26
26
  standaloneGuard
27
27
  }).code : "";
28
- const repairer = options?.repair === true ? generateRepairFunction(schema, typeName, typeSuffix).code : "";
28
+ const repairer = options?.repair === true ? generateRepairFunction(schema, typeName, typeSuffix, {
29
+ ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
30
+ formats
31
+ }).code : "";
29
32
  const appended = [checker, coercer, repairer].filter((part) => part !== "");
30
33
  const body = validatorFunction + booleanGuard + (appended.length === 0 ? "" : "\n\n" + appended.join("\n\n"));
31
34
  const mentions = identifierMentions(typeDefinition + body);
@@ -22,6 +22,11 @@ export declare const repairLookupNameFor: (ref: string, typeSuffix: string) => s
22
22
  * then can its own children be judged. A position is repaired at most once, so
23
23
  * the loop terminates on progress rather than on the pass cap.
24
24
  */
25
- export declare const generateRepairFunction: (schema: JSONSchema, typeName: string, typeSuffix?: string) => {
25
+ export declare const generateRepairFunction: (schema: JSONSchema, typeName: string, typeSuffix?: string, options?: {
26
+ /** The whole document, for a `propertyNames` test that reads a `$ref`'s target. */
27
+ readonly rootSchema?: Record<string, unknown>;
28
+ /** The `format` names the validator enforces, so a key test agrees with it. */
29
+ readonly formats?: ReadonlySet<string>;
30
+ }) => {
26
31
  code: string;
27
32
  };
@@ -1,8 +1,24 @@
1
+ import { regexFlagsFor } from "@amritk/helpers/escape-regex-pattern";
1
2
  import { getDefaultValue } from "@amritk/helpers/get-default-value";
2
3
  import { readKey } from "@amritk/helpers/read-key";
3
4
  import { refToName } from "@amritk/helpers/ref-to-name";
4
5
  import { isSchemaObject } from "@amritk/helpers/schema-guards";
6
+ import { NO_FORMATS } from "./enforced-keywords.js";
7
+ import { createSubschemaMatcher } from "./generate-validator-function.js";
5
8
  const repairLookupNameFor = (ref, typeSuffix) => `repair${refToName(ref, typeSuffix)}At`;
9
+ const keyTest = (nameSchema, ctx) => {
10
+ const named = ctx.matcher.named(nameSchema);
11
+ if (named !== null)
12
+ return named;
13
+ const body = ctx.matcher.test(nameSchema);
14
+ if (typeof body !== "string")
15
+ return body;
16
+ const name = `_repairKey${ctx.next()}`;
17
+ const parameter = /\binput\b/.test(body) ? "input" : "_input";
18
+ const path = /\b_path\b/.test(body) ? [` const _path = ''`] : [];
19
+ ctx.helpers.push([`const ${name} = (${parameter}: unknown): boolean => {`, ...path, body, `}`].join("\n"));
20
+ return name;
21
+ };
6
22
  const fallbackLiteral = (schema) => {
7
23
  const literal = getDefaultValue(schema);
8
24
  return literal === "undefined" ? null : literal;
@@ -21,12 +37,20 @@ const emitWalker = (schema, ctx) => {
21
37
  const own = fallbackLiteral(schema);
22
38
  const properties = schemaMap(node, "properties") ?? {};
23
39
  const children = Object.entries(properties).map(([key, child]) => [key, emitWalker(child, ctx)]).filter((entry) => entry[1] !== null);
40
+ const patternProperties = schemaMap(node, "patternProperties") ?? {};
41
+ const patterns = Object.entries(patternProperties).map(([source, child]) => ({
42
+ source,
43
+ walker: emitWalker(child, ctx)
44
+ }));
45
+ const additional = readKey(node, "additionalProperties");
46
+ const additionalWalker = typeof additional === "object" && additional !== null ? emitWalker(additional, ctx) : null;
47
+ const mapped = additionalWalker !== null || patterns.some((pattern) => pattern.walker !== null);
24
48
  const prefixItems = readKey(node, "prefixItems");
25
49
  const tuple = Array.isArray(prefixItems) ? prefixItems : [];
26
50
  const tupleWalkers = tuple.map((item) => emitWalker(item, ctx));
27
51
  const items = readKey(node, "items");
28
52
  const itemsWalker = typeof items === "object" && items !== null && !Array.isArray(items) ? emitWalker(items, ctx) : null;
29
- const hasChildren = children.length > 0 || itemsWalker !== null || tupleWalkers.some((walker) => walker !== null);
53
+ const hasChildren = children.length > 0 || mapped || itemsWalker !== null || tupleWalkers.some((walker) => walker !== null);
30
54
  if (own === null && !hasChildren)
31
55
  return null;
32
56
  const id = ctx.next();
@@ -36,15 +60,28 @@ const emitWalker = (schema, ctx) => {
36
60
  ` if (segments.length === 0) return ${own === null ? "null" : `() => (${own})`}`
37
61
  ];
38
62
  if (hasChildren) {
39
- lines.push(` const [head, ...rest] = segments as [string, ...string[]]`);
40
- if (children.length > 0) {
63
+ const declared = mapped ? Object.keys(properties) : children.map(([key]) => key);
64
+ const indexed = tupleWalkers.some((walker) => walker !== null) || itemsWalker !== null;
65
+ const arrayKeywords = tuple.length > 0 || typeof items === "object" && items !== null && !Array.isArray(items);
66
+ const indexBlock = indexed || mapped && arrayKeywords;
67
+ const nameSchema = readKey(node, "propertyNames");
68
+ const keyCheck = nameSchema === void 0 ? true : keyTest(nameSchema, ctx);
69
+ const readsHead = declared.length > 0 || indexBlock || mapped && patterns.length > 0 || typeof keyCheck === "string";
70
+ lines.push(` const [${readsHead ? "head" : ""}, ...rest] = segments as [string, ...string[]]`);
71
+ if (keyCheck === false)
72
+ lines.push(` if (rest.length === 0) return null`);
73
+ if (typeof keyCheck === "string")
74
+ lines.push(` if (rest.length === 0 && !${keyCheck}(head)) return null`);
75
+ if (declared.length > 0) {
76
+ const walkers = new Map(children);
41
77
  lines.push(` switch (head) {`);
42
- for (const [key, walker] of children)
43
- lines.push(` case ${JSON.stringify(key)}: return ${walker}(rest)`);
78
+ for (const key of declared) {
79
+ const walker = walkers.get(key);
80
+ lines.push(` case ${JSON.stringify(key)}: return ${walker === void 0 ? "null" : `${walker}(rest)`}`);
81
+ }
44
82
  lines.push(` }`);
45
83
  }
46
- const indexed = tupleWalkers.some((walker) => walker !== null) || itemsWalker !== null;
47
- if (indexed) {
84
+ if (indexBlock) {
48
85
  lines.push(` const index = Number(head)`);
49
86
  lines.push(` if (Number.isInteger(index) && index >= 0) {`);
50
87
  tupleWalkers.forEach((walker, position) => {
@@ -55,21 +92,43 @@ const emitWalker = (schema, ctx) => {
55
92
  if (itemsWalker !== null) {
56
93
  lines.push(` if (index >= ${tuple.length}) return ${itemsWalker}(rest)`);
57
94
  }
95
+ if (mapped && arrayKeywords)
96
+ lines.push(` return null`);
58
97
  lines.push(` }`);
59
98
  }
99
+ if (mapped && patterns.length > 0) {
100
+ const regexNames = patterns.map((pattern, index) => {
101
+ const regexName = `_repairPattern${id}_${index}`;
102
+ ctx.helpers.push(`const ${regexName} = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(regexFlagsFor(pattern.source))})`);
103
+ return regexName;
104
+ });
105
+ const answer = (walker) => walker === null ? "null" : `${walker}(rest)`;
106
+ const only = patterns[0];
107
+ if (patterns.length === 1 && only !== void 0) {
108
+ lines.push(` if (${regexNames[0]}.test(head)) return ${answer(only.walker)}`);
109
+ } else {
110
+ lines.push(` const claims = [${regexNames.join(", ")}].filter((pattern) => pattern.test(head))`);
111
+ lines.push(` if (claims.length > 1) return null`);
112
+ patterns.forEach((pattern, index) => {
113
+ lines.push(` if (claims[0] === ${regexNames[index]}) return ${answer(pattern.walker)}`);
114
+ });
115
+ }
116
+ }
60
117
  }
61
- lines.push(` return null`, `}`);
118
+ lines.push(additionalWalker === null ? ` return null` : ` return ${additionalWalker}(rest)`, `}`);
62
119
  ctx.helpers.push(lines.join("\n"));
63
120
  return name;
64
121
  };
65
- const generateRepairFunction = (schema, typeName, typeSuffix = "") => {
122
+ const generateRepairFunction = (schema, typeName, typeSuffix = "", options = {}) => {
66
123
  const helpers = [];
67
124
  let counter = 0;
68
- const ctx = { typeSuffix, helpers, next: () => counter++ };
125
+ const matcher = createSubschemaMatcher(typeSuffix, options.rootSchema ?? (isSchemaObject(schema) ? schema : void 0), options.formats ?? NO_FORMATS, "_repair");
126
+ const ctx = { typeSuffix, helpers, next: () => counter++, matcher };
69
127
  const walker = emitWalker(schema, ctx);
70
128
  const lookupName = `repair${typeName}At`;
129
+ const declarations = [...matcher.declarations(helpers.join("\n")), ...helpers];
71
130
  const parts = [
72
- ...helpers.length > 0 ? [helpers.join("\n\n"), ""] : [],
131
+ ...declarations.length > 0 ? [declarations.join("\n\n"), ""] : [],
73
132
  `export const ${lookupName}: RepairLookup = ${walker === null ? "() => null" : walker}`,
74
133
  "",
75
134
  `export const repair${typeName} = (input: unknown, _path = ''): RepairResult<${typeName}> => {`,
@@ -26,11 +26,6 @@ const hasOwnCheck = (objVar, key) => {
26
26
  return `${JSON.stringify(key)} in ${objVar}`;
27
27
  };
28
28
  const missingCheck = (objVar, key) => `!(${hasOwnCheck(objVar, key)})`;
29
- const typeofString = (type) => {
30
- if (type === "integer")
31
- return "number";
32
- return type;
33
- };
34
29
  const constMismatchCondition = (accessor, value) => {
35
30
  if (value === null || typeof value !== "object") {
36
31
  return `${accessor} !== ${JSON.stringify(value)}`;
@@ -292,7 +287,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
292
287
  lines.push(` }`);
293
288
  }
294
289
  if (hasConst(schema)) {
295
- const mismatch = constMismatchCondition(raw, schema.const);
290
+ const mismatch = constMismatchCondition(typeTestAccessor(raw, ctx), schema.const);
296
291
  const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
297
292
  lines.push(` if (${presence}${mismatch}) {`);
298
293
  lines.push(` ${pushError(ctx.sink, msg, path, "const", JSON.stringify({ allowedValue: schema.const }))}`);
@@ -300,7 +295,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
300
295
  }
301
296
  if (hasEnum(schema)) {
302
297
  const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
303
- lines.push(` if (${presence}!${enumMembershipExpr(schema.enum, raw)}) {`);
298
+ lines.push(` if (${presence}!${enumMembershipExpr(schema.enum, typeTestAccessor(raw, ctx))}) {`);
304
299
  lines.push(` ${pushError(ctx.sink, JSON.stringify(`must be one of: ${label}`), path, "enum", JSON.stringify({ allowedValues: schema.enum }))}`);
305
300
  lines.push(` }`);
306
301
  }
@@ -310,7 +305,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
310
305
  const wrongType = wrongTypeCondition(typeTestAccessor(raw, ctx), t);
311
306
  if (wrongType) {
312
307
  lines.push(` if (${presence === "" ? wrongType : `${presence}(${wrongType})`}) {`);
313
- lines.push(` ${pushError(ctx.sink, `'must be ${typeofString(t)}'`, path, "type", JSON.stringify({ type: t }))}`);
308
+ lines.push(` ${pushError(ctx.sink, `'must be ${t}'`, path, "type", JSON.stringify({ type: t }))}`);
314
309
  lines.push(` }`);
315
310
  }
316
311
  }
@@ -318,7 +313,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
318
313
  if (typeArray) {
319
314
  const allWrong = typeArray.map((t) => wrongTypeCondition(typeTestAccessor(raw, ctx), t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
320
315
  if (allWrong) {
321
- const label = typeArray.map((t) => typeofString(t)).join(" or ");
316
+ const label = typeArray.join(" or ");
322
317
  lines.push(` if (${presence === "" ? allWrong : `${presence}(${allWrong})`}) {`);
323
318
  lines.push(` ${pushError(ctx.sink, JSON.stringify(`must be ${label}`), path, "type", JSON.stringify({ type: schema.type }))}`);
324
319
  lines.push(` }`);
@@ -573,7 +568,7 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
573
568
  lines.push(...generateKeywordChecks("", raw, path, propSchema, suffix, valueCtx, presence));
574
569
  return lines;
575
570
  };
576
- const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
571
+ const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\["[A-Za-z_$][\w$]*"\]|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
577
572
  const namedMatcher = (sub, suffix, ctx) => {
578
573
  const key = JSON.stringify(sub);
579
574
  const existing = ctx.namedMatchers.get(key);
@@ -609,7 +604,31 @@ const namedMatcher = (sub, suffix, ctx) => {
609
604
  ctx.hoisted.push({ declaration, reference: `${name}(`, calls: true });
610
605
  return name;
611
606
  };
612
- const generateMatchesExpr = (raw, sub, suffix, ctx, required = false, branch) => {
607
+ const namedBranchErrors = (sub, suffix, ctx) => {
608
+ if (!isSchemaObject(sub))
609
+ return null;
610
+ const key = `explain:${JSON.stringify(sub)}`;
611
+ const existing = ctx.namedMatchers.get(key);
612
+ if (existing !== void 0)
613
+ return existing;
614
+ const fnCtx = { ...ctx, objVar: "obj", pathPrefix: "${_path}", depth: 0, sink: MATCH_ERROR_SINK };
615
+ const checks = generateValueChecks("", "input", "`${_path}`", sub, suffix, fnCtx, true);
616
+ if (checks.length === 0)
617
+ return null;
618
+ const body = checks.join("\n");
619
+ const name = `${hoistPrefix(ctx)}_explain${ctx.namedMatchers.size}`;
620
+ const declaration = [
621
+ `const ${name} = (${readsBinding("input", body) ? "input" : "_input"}: unknown, _path: string): ValidationError[] => {`,
622
+ ` const ${MATCH_ERROR_SINK}: ValidationError[] = []`,
623
+ body,
624
+ ` return ${MATCH_ERROR_SINK}`,
625
+ `}`
626
+ ].join("\n");
627
+ ctx.namedMatchers.set(key, name);
628
+ ctx.hoisted.push({ declaration, reference: `${name}(`, calls: true });
629
+ return name;
630
+ };
631
+ const generateMatchesExpr = (raw, sub, suffix, ctx, required = false) => {
613
632
  if (sub === true)
614
633
  return "true";
615
634
  if (sub === false)
@@ -617,31 +636,26 @@ const generateMatchesExpr = (raw, sub, suffix, ctx, required = false, branch) =>
617
636
  if (!isSchemaObject(sub))
618
637
  return "true";
619
638
  const value = NARROWABLE_REFERENCE.test(raw) ? raw : `_mv${ctx.depth}`;
620
- const valuePath = branch === void 0 ? "`${_path}`" : branch.path;
639
+ const valuePath = "`${_path}`";
621
640
  const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
622
641
  `;
623
- if (branch === void 0) {
624
- const named = namedMatcher(sub, suffix, ctx);
625
- if (named !== null)
626
- return named === "true" ? "true" : `${named}(${raw})`;
627
- const matchState = { on: ctx.failFast.on, blocked: false };
628
- const matchCtx = { ...ctx, sink: MATCH_BOOL_SINK, failFast: matchState, branchErrors: false };
629
- const boolChecks = generateValueChecks("", value, valuePath, sub, suffix, matchCtx, required);
630
- if (boolChecks.length === 0)
631
- return "true";
632
- if (!matchState.blocked)
633
- return `((): boolean => {
642
+ const named = namedMatcher(sub, suffix, ctx);
643
+ if (named !== null)
644
+ return named === "true" ? "true" : `${named}(${raw})`;
645
+ const matchState = { on: ctx.failFast.on, blocked: false };
646
+ const matchCtx = { ...ctx, sink: MATCH_BOOL_SINK, failFast: matchState, branchErrors: false };
647
+ const boolChecks = generateValueChecks("", value, valuePath, sub, suffix, matchCtx, required);
648
+ if (boolChecks.length === 0)
649
+ return "true";
650
+ if (!matchState.blocked)
651
+ return `((): boolean => {
634
652
  ${binding}${boolChecks.join("\n")}
635
653
  return true })()`;
636
- }
637
654
  const checks = generateValueChecks("", value, valuePath, sub, suffix, { ...ctx, sink: MATCH_ERROR_SINK }, required);
638
655
  if (checks.length === 0)
639
656
  return "true";
640
- const body = checks.join("\n");
641
- const collect = branch === void 0 ? "" : `
642
- if (_m.length !== 0) (${branch.buffer} ??= []).push(_m)`;
643
657
  return `((): boolean => { const _m: ValidationError[] = []
644
- ${binding}${body}${collect}
658
+ ${binding}${checks.join("\n")}
645
659
  return _m.length === 0 })()`;
646
660
  };
647
661
  const unevaluatedMatcher = (suffix, ctx) => (accessor, schema, depth) => (
@@ -691,17 +705,18 @@ const generateUnevaluatedChecks = (raw, path, schema, suffix, ctx) => {
691
705
  }
692
706
  return lines;
693
707
  };
694
- const BRANCH_BUFFER = "_br";
695
- const selectBranchErrorsLine = (path, ctx) => `for (const _e of selectBranchErrors(${BRANCH_BUFFER} ?? [], ${path})) ${ctx.sink}.push(_e)`;
696
- const branchBufferBlock = (guard, message, keyword, path, ctx) => [
697
- ` {`,
698
- ` let ${BRANCH_BUFFER}: ValidationError[][] | null = null`,
699
- ` if (${guard}) {`,
700
- ` ${pushError(ctx.sink, message, path, keyword)}`,
701
- ` ${selectBranchErrorsLine(path, ctx)}`,
702
- ` }`,
708
+ const branchErrorsReport = (guard, raw, explain, total, message, keyword, path, ctx) => [
709
+ ` if (${guard}) {`,
710
+ ` ${pushError(ctx.sink, message, path, keyword)}`,
711
+ ...explain.length === 0 ? [] : [
712
+ ` const _bp = ${path}`,
713
+ ` const _br: ValidationError[][] = []`,
714
+ ` for (const _b of [${explain.map((name) => `${name}(${raw}, _bp)`).join(", ")}]) if (_b.length !== 0) _br.push(_b)`,
715
+ ` for (const _e of selectBranchErrors(_br, _bp${explain.length === total ? `, ${total}` : ""})) ${ctx.sink}.push(_e)`
716
+ ],
703
717
  ` }`
704
718
  ];
719
+ const branchExplainers = (branches, suffix, ctx) => branches.map((branch) => namedBranchErrors(branch, suffix, ctx)).filter((name) => name !== null);
705
720
  const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
706
721
  if (!isSchemaObject(schema))
707
722
  return [];
@@ -711,11 +726,10 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
711
726
  lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx, true));
712
727
  }
713
728
  if (hasAnyOf(schema) && schema.anyOf.length > 0) {
714
- const collector = ctx.branchErrors ? { buffer: BRANCH_BUFFER, path } : void 0;
715
- const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx, true, collector));
729
+ const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx, true));
716
730
  if (!conds.includes("true")) {
717
731
  if (ctx.branchErrors) {
718
- lines.push(...branchBufferBlock(`!(${conds.join(" || ")})`, `'must match a schema in anyOf'`, "anyOf", path, ctx));
732
+ lines.push(...branchErrorsReport(`!(${conds.join(" || ")})`, raw, branchExplainers(schema.anyOf, suffix, ctx), schema.anyOf.length, `'must match a schema in anyOf'`, "anyOf", path, ctx));
719
733
  } else {
720
734
  const condition = `!(${conds.join(" || ")})`;
721
735
  if (LITERAL_CONDITION.test(condition))
@@ -727,20 +741,18 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
727
741
  }
728
742
  }
729
743
  if (hasOneOf(schema) && schema.oneOf.length > 0) {
730
- const collector = ctx.branchErrors ? { buffer: BRANCH_BUFFER, path } : void 0;
731
- const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx, true, collector)} ? 1 : 0)`);
744
+ const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx, true)} ? 1 : 0)`);
732
745
  const message = `'must match exactly one schema in oneOf'`;
733
746
  if (!ctx.branchErrors) {
734
747
  lines.push(` if ((${conds.join(" + ")}) !== 1) {`);
735
748
  lines.push(` ${pushError(ctx.sink, message, path, "oneOf")}`);
736
749
  lines.push(` }`);
737
750
  } else {
751
+ const explain = branchExplainers(schema.oneOf, suffix, ctx);
738
752
  lines.push(` {`);
739
- lines.push(` let ${BRANCH_BUFFER}: ValidationError[][] | null = null`);
740
753
  lines.push(` const _n = ${conds.join(" + ")}`);
741
- lines.push(` if (_n === 0) {`);
742
- lines.push(` ${pushError(ctx.sink, message, path, "oneOf")}`);
743
- lines.push(` ${selectBranchErrorsLine(path, ctx)}`);
754
+ const report = branchErrorsReport("_n === 0", raw, explain, schema.oneOf.length, message, "oneOf", path, ctx).map((line) => ` ${line}`);
755
+ lines.push(...report.slice(0, -1));
744
756
  lines.push(` } else if (_n !== 1) {`);
745
757
  lines.push(` ${pushError(ctx.sink, message, path, "oneOf")}`);
746
758
  lines.push(` }`);
@@ -1567,7 +1579,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1567
1579
  if (rootTypeArray2) {
1568
1580
  const allWrong = rootTypeArray2.map((t) => wrongTypeCondition(typeTestAccessor("input", ctx), t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
1569
1581
  if (allWrong) {
1570
- const label = rootTypeArray2.map((t) => typeofString(t)).join(" or ");
1582
+ const label = rootTypeArray2.join(" or ");
1571
1583
  checks.push(` if (${allWrong}) {`);
1572
1584
  checks.push(` ${pushError(ctx.sink, JSON.stringify(`must be ${label}`), rootPath, "type", JSON.stringify({ type: schema.type }))}`);
1573
1585
  checks.push(` }`);
@@ -1577,7 +1589,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1577
1589
  const wrongType = wrongTypeCondition(typeTestAccessor("input", ctx), t);
1578
1590
  if (wrongType) {
1579
1591
  checks.push(` if (${wrongType}) {`);
1580
- checks.push(` ${pushError(ctx.sink, `'must be ${typeofString(t)}'`, rootPath, "type", JSON.stringify({ type: t }))}`);
1592
+ checks.push(` ${pushError(ctx.sink, `'must be ${t}'`, rootPath, "type", JSON.stringify({ type: t }))}`);
1581
1593
  checks.push(` }`);
1582
1594
  }
1583
1595
  }
@@ -1599,7 +1611,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1599
1611
  const checks = [];
1600
1612
  const allWrong = rootTypeArray.map((t) => wrongTypeCondition(typeTestAccessor("input", ctx), t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
1601
1613
  if (allWrong) {
1602
- const label = rootTypeArray.map((t) => typeofString(t)).join(" or ");
1614
+ const label = rootTypeArray.join(" or ");
1603
1615
  checks.push(` if (${allWrong}) {`);
1604
1616
  checks.push(` ${pushError(ctx.sink, JSON.stringify(`must be ${label}`), rootPath, "type", JSON.stringify({ type: schema.type }))}`);
1605
1617
  checks.push(` }`);
@@ -1624,7 +1636,6 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1624
1636
  if (hasType(schema)) {
1625
1637
  const t = schema.type;
1626
1638
  const wrongType = wrongTypeCondition("input", t);
1627
- const typLabel = typeofString(t);
1628
1639
  const rootCtx = createRootContext(rootSchema, formats, branchErrors, failFast);
1629
1640
  const constraintLines = generateConstraintChecks("", "_root", "`${_path}`", schema, suffix, rootCtx);
1630
1641
  if (!wrongType) {
@@ -1638,7 +1649,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1638
1649
  return [
1639
1650
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1640
1651
  ` if (${wrongType}) {`,
1641
- ` ${returnError(`'must be ${typLabel}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1652
+ ` ${returnError(`'must be ${t}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1642
1653
  ` }`,
1643
1654
  ` return true`,
1644
1655
  `}`
@@ -1647,7 +1658,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1647
1658
  return withHoisted(rootCtx.hoisted, [
1648
1659
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1649
1660
  ` if (${wrongType}) {`,
1650
- ` ${returnError(`'must be ${typLabel}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1661
+ ` ${returnError(`'must be ${t}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1651
1662
  ` }`,
1652
1663
  ...readsBinding("_root", constraintLines.join("\n")) ? [` const _root: unknown = input`] : [],
1653
1664
  ...errorsBinding(failFast),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/validation",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "One generator surface over mjst's types, validators, coercers, repairers and parsers.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -73,11 +73,11 @@
73
73
  }
74
74
  },
75
75
  "dependencies": {
76
- "@amritk/helpers": "^0.24.0",
76
+ "@amritk/helpers": "^0.25.0",
77
77
  "json-schema-typed": "^8.0.1"
78
78
  },
79
79
  "devDependencies": {
80
- "@amritk/runtime-validators": "^0.15.1",
80
+ "@amritk/runtime-validators": "^0.15.2",
81
81
  "@ryoppippi/unplugin-typia": "^2.6.5",
82
82
  "@scalar/openapi-parser": "^0.26.1",
83
83
  "@sinclair/typebox": "^0.34.49",