@crewhaus/tool-schema 0.7.0 → 0.8.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.
@@ -8,6 +8,7 @@
8
8
  * only compares.
9
9
  */
10
10
  import { checkFormat, isFormatName } from "./formats";
11
+ import { testPatternSync } from "./regex-answers";
11
12
  import { deepEqual, getPath, isPlainObject, matchesType, preview, typeOf, } from "./value";
12
13
  /** Every comparison {@link runChecks} understands. */
13
14
  export const ASSERT_OPS = [
@@ -77,22 +78,42 @@ function compareLength(actual, expected, ok, describe) {
77
78
  ? { ok: true, reason: "" }
78
79
  : { ok: false, reason: describe(length, expected) };
79
80
  }
80
- function matchRegex(actual, expected, flags, want) {
81
+ /**
82
+ * `matches` / `notMatches`. The pattern is the caller's, so it is never run
83
+ * here: the answer comes from `regex` (resolved in the worker by the tool
84
+ * that called), or, for a caller with none, from the bounded fallback on
85
+ * this thread. A pattern the screen refuses fails the check, as an invalid
86
+ * one always has; one that could not be run to an answer is undetermined,
87
+ * never "does not match" (and so never a pass for `notMatches`).
88
+ */
89
+ function matchRegex(actual, expected, flags, want, regex) {
81
90
  if (typeof actual !== "string") {
82
91
  return { ok: false, reason: `matches needs a string, found ${typeOf(actual)}` };
83
92
  }
84
93
  if (typeof expected !== "string") {
85
94
  return { ok: false, reason: "the check's expected value must be a regex source string" };
86
95
  }
87
- let re;
88
- try {
89
- re = new RegExp(expected, flags);
96
+ const answer = regex === undefined
97
+ ? testPatternSync(expected, flags, actual)
98
+ : regex.lookup(expected, flags, actual);
99
+ if (answer === undefined) {
100
+ return {
101
+ ok: false,
102
+ undetermined: true,
103
+ reason: `could not evaluate /${expected}/${flags}: its answer has not been worked out yet`,
104
+ };
90
105
  }
91
- catch (err) {
92
- return { ok: false, reason: `invalid regex /${expected}/${flags}: ${err.message}` };
106
+ if (typeof answer !== "boolean") {
107
+ if ("refused" in answer) {
108
+ return { ok: false, reason: `invalid regex /${expected}/${flags}: ${answer.refused}` };
109
+ }
110
+ return {
111
+ ok: false,
112
+ undetermined: true,
113
+ reason: `could not evaluate /${expected}/${flags}: ${answer.undetermined}`,
114
+ };
93
115
  }
94
- const matched = re.test(actual);
95
- if (matched === want)
116
+ if (answer === want)
96
117
  return { ok: true, reason: "" };
97
118
  return {
98
119
  ok: false,
@@ -101,7 +122,37 @@ function matchRegex(actual, expected, flags, want) {
101
122
  : `${preview(actual, 60)} matches /${expected}/${flags} but must not`,
102
123
  };
103
124
  }
104
- function evaluate(check, actual, found) {
125
+ /**
126
+ * Ask `answers` about every pattern `checks` will test against `value`, so
127
+ * one {@link RegexAnswers.resolve} settles them all before
128
+ * {@link runChecks} reads them. A caller evaluating several check lists
129
+ * against one value (a table's rows, a router's arms) asks for every list
130
+ * first, then resolves once.
131
+ */
132
+ export function askCheckPatterns(value, checks, answers) {
133
+ for (const check of checks) {
134
+ if (check.op !== "matches" && check.op !== "notMatches")
135
+ continue;
136
+ if (typeof check.expected !== "string")
137
+ continue;
138
+ let actual = value;
139
+ const path = check.path ?? "";
140
+ if (path !== "") {
141
+ try {
142
+ const resolution = getPath(value, path);
143
+ if (!resolution.found)
144
+ continue;
145
+ actual = resolution.value;
146
+ }
147
+ catch {
148
+ continue;
149
+ }
150
+ }
151
+ if (typeof actual === "string")
152
+ answers.lookup(check.expected, check.flags ?? "", actual);
153
+ }
154
+ }
155
+ function evaluate(check, actual, found, regex) {
105
156
  const expected = check.expected;
106
157
  switch (check.op) {
107
158
  case "exists":
@@ -156,9 +207,9 @@ function evaluate(check, actual, found) {
156
207
  };
157
208
  }
158
209
  case "matches":
159
- return matchRegex(actual, expected, check.flags ?? "", true);
210
+ return matchRegex(actual, expected, check.flags ?? "", true, regex);
160
211
  case "notMatches":
161
- return matchRegex(actual, expected, check.flags ?? "", false);
212
+ return matchRegex(actual, expected, check.flags ?? "", false, regex);
162
213
  case "startsWith":
163
214
  case "endsWith": {
164
215
  if (typeof actual !== "string" || typeof expected !== "string") {
@@ -256,8 +307,14 @@ function evaluate(check, actual, found) {
256
307
  * A malformed check (a bad regex, a non-numeric bound) fails that check with
257
308
  * an explanatory reason rather than throwing, so one typo cannot take down a
258
309
  * whole gate.
310
+ *
311
+ * `matches` and `notMatches` read their answers from `options.regex`, which
312
+ * the calling tool resolved in the worker (see {@link askCheckPatterns});
313
+ * without it they fall back to a bounded run on this thread. A check whose
314
+ * pattern could not be run to an answer is not ok and carries
315
+ * `undetermined: true`; its reason says so whatever `message` the check set.
259
316
  */
260
- export function runChecks(value, checks) {
317
+ export function runChecks(value, checks, options = {}) {
261
318
  const results = checks.map((check, index) => {
262
319
  const path = check.path ?? "";
263
320
  let found = true;
@@ -291,7 +348,7 @@ export function runChecks(value, checks) {
291
348
  reason: check.message ?? `the path "${path}" does not resolve`,
292
349
  };
293
350
  }
294
- const verdict = evaluate(check, actual, found);
351
+ const verdict = evaluate(check, actual, found, options.regex);
295
352
  return {
296
353
  index,
297
354
  path,
@@ -299,7 +356,12 @@ export function runChecks(value, checks) {
299
356
  ok: verdict.ok,
300
357
  actual: found ? preview(actual, 80) : null,
301
358
  expected: check.expected === undefined ? null : preview(check.expected, 80),
302
- reason: verdict.ok ? "" : (check.message ?? verdict.reason),
359
+ reason: verdict.ok
360
+ ? ""
361
+ : verdict.undetermined === true
362
+ ? verdict.reason
363
+ : (check.message ?? verdict.reason),
364
+ ...(verdict.undetermined === true ? { undetermined: true } : {}),
303
365
  };
304
366
  });
305
367
  const failures = results.filter((r) => !r.ok);
@@ -307,10 +369,29 @@ export function runChecks(value, checks) {
307
369
  ok: failures.length === 0,
308
370
  passed: results.length - failures.length,
309
371
  failed: failures.length,
372
+ undetermined: failures.filter((r) => r.undetermined === true).length,
310
373
  results,
311
374
  failures,
312
375
  };
313
376
  }
377
+ /**
378
+ * What a check list says as a whole, three ways. Under `all`, a check that
379
+ * definitely failed decides it (`fail`); otherwise an undetermined one leaves
380
+ * it `undetermined`. Under `any`, a check that passed decides it (`pass`);
381
+ * otherwise an undetermined one leaves it `undetermined`. A router or a
382
+ * scorer that reads `report.ok` alone would take an undetermined check for a
383
+ * miss and fall through to the next arm or the default.
384
+ */
385
+ export function checksVerdict(report, mode = "all") {
386
+ if (mode === "all") {
387
+ if (report.ok)
388
+ return "pass";
389
+ return report.failed > report.undetermined ? "fail" : "undetermined";
390
+ }
391
+ if (report.passed > 0)
392
+ return "pass";
393
+ return report.undetermined > 0 ? "undetermined" : "fail";
394
+ }
314
395
  /**
315
396
  * Which of `paths` are present and non-empty in `value`, and which are not.
316
397
  * "Present" means the path resolves; "non-empty" applies
@@ -51,6 +51,27 @@ function matchPattern(segments, pattern) {
51
51
  }
52
52
  return s === segments.length;
53
53
  }
54
+ /**
55
+ * Note a path `ignorePaths` matched, once. A set answers "listed already?":
56
+ * searching the list made a value with many ignored paths quadratic in them.
57
+ */
58
+ function noteIgnored(ctx, path) {
59
+ if (ctx.ignoredSeen.has(path))
60
+ return;
61
+ ctx.ignoredSeen.add(path);
62
+ ctx.ignored.push(path);
63
+ }
64
+ /**
65
+ * Note an array compared positionally because it was too long, once. As for
66
+ * `noteIgnored`, a set answers "listed already?": searching the list made a
67
+ * value with many long arrays quadratic in them.
68
+ */
69
+ function noteOrderedFallback(ctx, path) {
70
+ if (ctx.orderedFallbacksSeen.has(path))
71
+ return;
72
+ ctx.orderedFallbacksSeen.add(path);
73
+ ctx.orderedFallbacks.push(path);
74
+ }
54
75
  function record(ctx, diff) {
55
76
  if (ctx.differences.length >= ctx.opts.maxDifferences) {
56
77
  ctx.truncated = true;
@@ -73,15 +94,16 @@ function equalUnder(actual, expected, ctx, path) {
73
94
  differences: [],
74
95
  truncated: false,
75
96
  ignored: [],
97
+ ignoredSeen: new Set(),
76
98
  orderedFallbacks: [],
99
+ orderedFallbacksSeen: new Set(),
77
100
  };
78
101
  walk(actual, expected, path, probe);
79
102
  return probe.differences.length === 0;
80
103
  }
81
104
  function walk(actual, expected, path, ctx) {
82
105
  if (matchesAnyPattern(path, ctx.patterns)) {
83
- if (!ctx.ignored.includes(path))
84
- ctx.ignored.push(path);
106
+ noteIgnored(ctx, path);
85
107
  return;
86
108
  }
87
109
  const expectedType = typeOf(expected);
@@ -132,9 +154,8 @@ function walk(actual, expected, path, ctx) {
132
154
  }
133
155
  function compareArrays(actual, expected, path, ctx) {
134
156
  const tooLong = actual.length > UNORDERED_ARRAY_LIMIT || expected.length > UNORDERED_ARRAY_LIMIT;
135
- if (ctx.opts.ignoreArrayOrder && tooLong && !ctx.orderedFallbacks.includes(path)) {
136
- ctx.orderedFallbacks.push(path);
137
- }
157
+ if (ctx.opts.ignoreArrayOrder && tooLong)
158
+ noteOrderedFallback(ctx, path);
138
159
  if (ctx.opts.ignoreArrayOrder && !tooLong) {
139
160
  compareArraysUnordered(actual, expected, path, ctx);
140
161
  return;
@@ -208,8 +229,7 @@ function compareObjects(actual, expected, path, ctx) {
208
229
  const childPath = joinPointer(path, key);
209
230
  if (!Object.hasOwn(actual, key)) {
210
231
  if (matchesAnyPattern(childPath, ctx.patterns)) {
211
- if (!ctx.ignored.includes(childPath))
212
- ctx.ignored.push(childPath);
232
+ noteIgnored(ctx, childPath);
213
233
  continue;
214
234
  }
215
235
  record(ctx, {
@@ -230,8 +250,7 @@ function compareObjects(actual, expected, path, ctx) {
230
250
  continue;
231
251
  const childPath = joinPointer(path, key);
232
252
  if (matchesAnyPattern(childPath, ctx.patterns)) {
233
- if (!ctx.ignored.includes(childPath))
234
- ctx.ignored.push(childPath);
253
+ noteIgnored(ctx, childPath);
235
254
  continue;
236
255
  }
237
256
  record(ctx, {
@@ -260,7 +279,9 @@ export function compareValues(actual, expected, options = {}) {
260
279
  differences: [],
261
280
  truncated: false,
262
281
  ignored: [],
282
+ ignoredSeen: new Set(),
263
283
  orderedFallbacks: [],
284
+ orderedFallbacksSeen: new Set(),
264
285
  };
265
286
  walk(actual, expected, "", ctx);
266
287
  return {
@@ -65,6 +65,19 @@ export declare function isDate(value: string): FormatResult;
65
65
  * seconds. An offset with minutes over 59 is rejected.
66
66
  */
67
67
  export declare function isTime(value: string): FormatResult;
68
+ /**
69
+ * A date-time split at its separator: one character, then everything up to
70
+ * the first `T` or `t` after it, then a non-empty rest. `.` and the class
71
+ * both stop at a line break (`\n`, `\r`, U+2028, U+2029), so a value that
72
+ * holds one never splits.
73
+ *
74
+ * This was `/^(.+?)[Tt](.+)$/`, which splits the same way. Its lazy group
75
+ * tried every `T` of a long first line and ran `(.+)` from each to the line
76
+ * break that `$` cannot pass: quadratic in the line, on ordinary prose. A
77
+ * failed try now gives back only characters that cannot be a `T`, one step
78
+ * each.
79
+ */
80
+ export declare const DATE_TIME_SPLIT: RegExp;
68
81
  /**
69
82
  * RFC 3339 `date-time`: a date, a `T` (or lowercase `t`) separator, and a
70
83
  * time with an offset. A space separator, as SQL writes it, is rejected —
@@ -212,13 +212,26 @@ export function isTime(value) {
212
212
  }
213
213
  return OK;
214
214
  }
215
+ /**
216
+ * A date-time split at its separator: one character, then everything up to
217
+ * the first `T` or `t` after it, then a non-empty rest. `.` and the class
218
+ * both stop at a line break (`\n`, `\r`, U+2028, U+2029), so a value that
219
+ * holds one never splits.
220
+ *
221
+ * This was `/^(.+?)[Tt](.+)$/`, which splits the same way. Its lazy group
222
+ * tried every `T` of a long first line and ran `(.+)` from each to the line
223
+ * break that `$` cannot pass: quadratic in the line, on ordinary prose. A
224
+ * failed try now gives back only characters that cannot be a `T`, one step
225
+ * each.
226
+ */
227
+ export const DATE_TIME_SPLIT = /^(.[^Tt\n\r\u2028\u2029]*)[Tt](.+)$/;
215
228
  /**
216
229
  * RFC 3339 `date-time`: a date, a `T` (or lowercase `t`) separator, and a
217
230
  * time with an offset. A space separator, as SQL writes it, is rejected —
218
231
  * this is the strict interchange form.
219
232
  */
220
233
  export function isDateTime(value) {
221
- const split = value.match(/^(.+?)[Tt](.+)$/);
234
+ const split = value.match(DATE_TIME_SPLIT);
222
235
  if (split === null)
223
236
  return bad('expected a "T" between the date and the time');
224
237
  const date = isDate(split[1] ?? "");
@@ -1,3 +1,91 @@
1
+ /**
2
+ * A JSON Schema Draft-07 validator, written out in full here so the package
3
+ * has no runtime dependency and so its behaviour is auditable in one file.
4
+ *
5
+ * ## What is enforced
6
+ *
7
+ * | Group | Keywords |
8
+ * |---|---|
9
+ * | any | `type`, `enum`, `const` |
10
+ * | number | `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` |
11
+ * | string | `minLength`, `maxLength`, `pattern`, `format` (opt-in) |
12
+ * | array | `items` (schema or tuple), `additionalItems`, `contains`, `minItems`, `maxItems`, `uniqueItems` |
13
+ * | object | `properties`, `patternProperties`, `additionalProperties`, `propertyNames`, `required`, `minProperties`, `maxProperties` |
14
+ * | logic | `allOf`, `anyOf`, `oneOf`, `not`, `if`/`then`/`else` |
15
+ * | reference | `$ref` to a local JSON Pointer, including `#/$defs/...` and `#/definitions/...` |
16
+ *
17
+ * Boolean schemas (`true` accepts everything, `false` rejects everything) are
18
+ * supported wherever a schema is expected.
19
+ *
20
+ * ## What is NOT enforced
21
+ *
22
+ * - **Annotations**, which carry no constraint and are skipped silently:
23
+ * `$id`, `$schema`, `$comment`, `title`, `description`, `default`,
24
+ * `examples`, `readOnly`, `writeOnly`, `deprecated`, `contentMediaType`,
25
+ * `contentEncoding`, `$defs`, `definitions`.
26
+ * - **`format`**, unless `assertFormat` is set. Draft-07 makes `format` an
27
+ * annotation by default and this validator follows that; the opt-in uses
28
+ * the checks in `./formats`, whose subsets are documented there.
29
+ * - **`dependencies` / `dependentRequired` / `dependentSchemas`**,
30
+ * **`unevaluatedProperties` / `unevaluatedItems`**, the draft-04 boolean
31
+ * form of `exclusiveMinimum`/`exclusiveMaximum`, and any other keyword not
32
+ * in the table above. These are never silently dropped: every one
33
+ * encountered is listed in the result's `unsupportedKeywords`, so a caller
34
+ * can tell a clean pass from an unenforced one. Note *encountered*: the
35
+ * list is built while walking, so a keyword sitting in a subschema this
36
+ * value never reached (a property it does not carry, a `$defs` entry no
37
+ * `$ref` took) is not in it. Validate a value that exercises the branch to
38
+ * see its keywords.
39
+ * - **Remote and non-pointer `$ref`** (`http://...`, `other.json#/x`, and
40
+ * `$id`-relative resolution). A `$ref` this validator cannot resolve
41
+ * locally is reported as an error rather than treated as `true`.
42
+ *
43
+ * ## Two deliberate deviations, both in the reporting
44
+ *
45
+ * - When `type` fails, the keywords specific to that type are not also run,
46
+ * so a number arriving where an object belongs produces one error rather
47
+ * than one per property. The verdict is identical; only the error list is
48
+ * shorter. `allOf`/`anyOf`/`oneOf`/`not` still run, since those can fail
49
+ * independently of the type.
50
+ * - Errors stop accumulating at `maxErrors`, and the result says `truncated`.
51
+ *
52
+ * ## A bounded amount of work, and "undetermined" when it runs out
53
+ *
54
+ * `anyOf`/`oneOf`/`allOf` over `$ref`s that share a target multiply: a
55
+ * 1.3 KB schema can ask for 2^30 subschema evaluations, and the walk is
56
+ * synchronous. Every subschema evaluation is counted against a budget. So
57
+ * is every check whose cost follows the value's size rather than the
58
+ * schema's — a string's length or pattern, uniqueItems, an object's keys,
59
+ * each enum candidate and each (key, patternProperties pattern) test, and
60
+ * each error message built — so a small schema cannot spend the value's
61
+ * size once per leaf. When the budget runs out the walk stops and the
62
+ * result is `undetermined`, with `valid: false` and the reason — never a
63
+ * verdict either way. A failing branch is summarised in a bounded message,
64
+ * and a message is built only when it will be kept.
65
+ *
66
+ * The default budget ({@link defaultWorkLimit}) follows what an honest walk
67
+ * can cost. A schema written out as a tree (no `$ref` reached twice at one
68
+ * place) evaluates each of its subschemas at most once per value node, so a
69
+ * value node is allowed as many units as the schema has subschemas (at
70
+ * least {@link WORK_PER_VALUE_NODE}, at most {@link MAX_SCHEMA_FANOUT}); a
71
+ * wide or nested union over thousands of valid rows is answered. Only
72
+ * repeating a `$ref` at one place goes past that, which is the shape that
73
+ * multiplies. The floor is {@link DEFAULT_MAX_WORK} and the ceiling
74
+ * {@link MAX_DEFAULT_WORK}, a few seconds of the worst schemas measured.
75
+ *
76
+ * ## Depth is the value's
77
+ *
78
+ * A recursive schema walks as deep as the value does, so nesting is counted
79
+ * where the value nests — into an item or a property — not per `$ref` or
80
+ * branch. A value deeper than {@link MAX_VALUE_DEPTH}, or a schema and value
81
+ * nesting past {@link MAX_NESTING} walk frames together, is `undetermined`:
82
+ * the part not walked may be valid or not. A `$ref` that reaches itself at
83
+ * one position with nothing consumed between is a cycle, and an error.
84
+ *
85
+ * `pattern` is an unanchored ECMA-262 match, per the spec — `"pattern": "a"`
86
+ * matches `"banana"`.
87
+ */
88
+ import { type RegexAnswers } from "./regex-answers";
1
89
  /** A schema is an object of keywords, or a boolean that accepts/rejects all. */
2
90
  export type Schema = boolean | Record<string, unknown>;
3
91
  /** One failure, addressed by a JSON Pointer into the value. */
@@ -16,9 +104,81 @@ export type ValidateOptions = {
16
104
  assertFormat: boolean;
17
105
  /** Stop collecting after this many errors. */
18
106
  maxErrors: number;
107
+ /**
108
+ * Most subschema evaluations before the answer is `undetermined`. Default:
109
+ * the larger of DEFAULT_MAX_WORK and WORK_PER_VALUE_NODE per value node.
110
+ */
111
+ maxWork: number;
112
+ /**
113
+ * A budget shared with other calls (ValidateRecords draws every row from
114
+ * one). When given, `maxWork` is ignored.
115
+ */
116
+ budget: WorkBudget;
117
+ /**
118
+ * Answers for the schema's `pattern` and `patternProperties` regexes,
119
+ * resolved in the worker by the calling tool (see `withRegexAnswers`). A
120
+ * question not answered yet is recorded there and read as "no match" for
121
+ * this pass, which the caller discards and runs again. Without it, each
122
+ * pattern runs on this thread through the bounded fallback.
123
+ */
124
+ regex: RegexAnswers;
125
+ };
126
+ /** Subschema evaluations allowed and spent; shared by reference. */
127
+ export type WorkBudget = {
128
+ used: number;
129
+ readonly limit: number;
19
130
  };
131
+ /** The budget floor: about a third of a second of the worst schemas measured (Apple silicon). */
132
+ export declare const DEFAULT_MAX_WORK = 500000;
133
+ /**
134
+ * The least work allowed per unit of the value's weight, so a large document
135
+ * is not refused for its size. A schema with more subschemas than this is
136
+ * allowed its subschema count instead.
137
+ */
138
+ export declare const WORK_PER_VALUE_NODE = 64;
139
+ /** The most work per unit of weight a schema's size earns. */
140
+ export declare const MAX_SCHEMA_FANOUT = 1024;
141
+ /**
142
+ * The most work a default budget allows, however large the value: about
143
+ * seven seconds of the worst schemas measured (Apple silicon, 0.3-0.5 us a
144
+ * unit). The walk is synchronous, so this is how long one call can hold the
145
+ * harness's thread.
146
+ */
147
+ export declare const MAX_DEFAULT_WORK = 20000000;
148
+ /** Nodes in a JSON value (itself included), counted iteratively, stopping at `cap`. */
149
+ export declare function countValueNodes(value: unknown, cap?: number): number;
150
+ /**
151
+ * A value's weight: one per node, and one more per CHARS_PER_UNIT
152
+ * characters of each string and key — the size the size-bound checks are
153
+ * charged in, so the budget grows with the value in the same units.
154
+ */
155
+ export declare function valueWeight(value: unknown): number;
156
+ /**
157
+ * Subschemas in `schema` written out as a tree, `$ref`s not followed, up to
158
+ * `cap`: what one value node can honestly be checked against. Counted
159
+ * iteratively, since a schema may nest deeper than the stack.
160
+ */
161
+ export declare function schemaFanout(schema: unknown, cap?: number): number;
162
+ /**
163
+ * The default budget for validating `value` against `schema`: the value's
164
+ * weight times what the schema allows per unit of it, between
165
+ * DEFAULT_MAX_WORK and MAX_DEFAULT_WORK.
166
+ */
167
+ export declare function defaultWorkLimit(value: unknown, schema?: unknown): number;
20
168
  export type ValidationResult = {
169
+ /** Never true when `undetermined` is set. */
21
170
  valid: boolean;
171
+ /**
172
+ * Why no verdict could be reached, or null when `valid` is the verdict.
173
+ * Set when the work budget ran out: the errors listed are the ones found
174
+ * before it did, and the value may be valid or invalid.
175
+ */
176
+ undetermined: string | null;
177
+ /**
178
+ * True when `undetermined` is the work budget running out: no pattern
179
+ * answer can change that, so a caller resolving patterns in rounds stops.
180
+ */
181
+ workExhausted: boolean;
22
182
  errors: ValidationError[];
23
183
  /** True when `maxErrors` cut the list short. */
24
184
  truncated: boolean;
@@ -29,6 +189,16 @@ export type ValidationResult = {
29
189
  */
30
190
  unsupportedKeywords: string[];
31
191
  };
192
+ /** Deepest value the walk follows; past it the answer is undetermined. */
193
+ export declare const MAX_VALUE_DEPTH = 512;
194
+ /**
195
+ * Most walk frames (a subschema evaluation inside another) at once, value
196
+ * and schema nesting together; past it the answer is undetermined. It keeps
197
+ * a deeply nested schema from overflowing the stack.
198
+ */
199
+ export declare const MAX_NESTING = 2048;
200
+ /** Longest message one error carries; nested branch reasons are cut here. */
201
+ export declare const MAX_MESSAGE_CHARS = 1000;
32
202
  /**
33
203
  * Validate `value` against `schema`, returning every failure with a JSON
34
204
  * Pointer path. See the module comment for the exact supported subset; the