@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
  * foreign key point at something that exists.
9
9
  */
10
10
  import { type Schema, type ValidationError } from "./jsonschema";
11
+ import type { RegexAnswers } from "./regex-answers";
11
12
  export type RowFailure = {
12
13
  /** 0-based index of the row in the input array. */
13
14
  row: number;
@@ -16,10 +17,23 @@ export type RowFailure = {
16
17
  errors: ValidationError[];
17
18
  };
18
19
  export type RecordsReport = {
20
+ /** False when any row failed OR could not be decided. */
19
21
  ok: boolean;
20
22
  total: number;
21
23
  passed: number;
22
24
  failed: number;
25
+ /**
26
+ * Rows with no verdict, neither passed nor failed: the call's work budget
27
+ * ran out on or before them (a schema's anyOf/oneOf/allOf over shared
28
+ * $refs multiplied past it), or a `pattern` could not be run to an answer
29
+ * on them.
30
+ */
31
+ undetermined: number;
32
+ /** The first undetermined row and why, or null. */
33
+ undeterminedFrom: {
34
+ row: number;
35
+ reason: string;
36
+ } | null;
23
37
  /** Failures, capped at `maxFailedRows`. */
24
38
  failures: RowFailure[];
25
39
  /** True when the failure list was cut short; `failed` is still the true count. */
@@ -41,6 +55,8 @@ export type ValidateRecordsOptions = {
41
55
  maxFailedRows: number;
42
56
  /** A field whose value identifies the row in the report. */
43
57
  idField: string | null;
58
+ /** Answers for the schema's patterns; see `ValidateOptions.regex`. */
59
+ regex?: RegexAnswers;
44
60
  };
45
61
  export declare const DEFAULT_RECORDS_OPTIONS: ValidateRecordsOptions;
46
62
  /**
@@ -7,7 +7,7 @@
7
7
  * does every row match the schema, is the key actually unique, and does every
8
8
  * foreign key point at something that exists.
9
9
  */
10
- import { validateValue } from "./jsonschema";
10
+ import { defaultWorkLimit, validateValue, } from "./jsonschema";
11
11
  import { canonicalize, getPath, isPlainObject, preview, typeOf } from "./value";
12
12
  /** Joins a path and a keyword into one map key; a character no path contains. */
13
13
  const ISSUE_SEPARATOR = String.fromCharCode(0);
@@ -40,13 +40,25 @@ export function validateRecords(rows, schema, options = {}) {
40
40
  const issues = new Map();
41
41
  const unsupported = new Set();
42
42
  let failed = 0;
43
+ let undetermined = 0;
44
+ let undeterminedFrom = null;
45
+ // One budget for the whole call, sized by all the rows together: a
46
+ // per-row budget would let 20,000 hostile rows each spend a full one.
47
+ const budget = { used: 0, limit: defaultWorkLimit(rows, schema) };
43
48
  rows.forEach((row, index) => {
44
49
  const result = validateValue(row, schema, {
45
50
  assertFormat: opts.assertFormat,
46
51
  maxErrors: opts.maxErrorsPerRow,
52
+ budget,
53
+ ...(opts.regex === undefined ? {} : { regex: opts.regex }),
47
54
  });
48
55
  for (const keyword of result.unsupportedKeywords)
49
56
  unsupported.add(keyword);
57
+ if (result.undetermined !== null) {
58
+ undetermined += 1;
59
+ undeterminedFrom ??= { row: index, reason: result.undetermined };
60
+ return;
61
+ }
50
62
  if (result.valid)
51
63
  return;
52
64
  failed += 1;
@@ -80,10 +92,12 @@ export function validateRecords(rows, schema, options = {}) {
80
92
  const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
81
93
  const topIssues = [...issues.values()].sort((a, b) => b.rows - a.rows || byCodeUnit(a.path, b.path) || byCodeUnit(a.keyword, b.keyword));
82
94
  return {
83
- ok: failed === 0,
95
+ ok: failed === 0 && undetermined === 0,
84
96
  total: rows.length,
85
- passed: rows.length - failed,
97
+ passed: rows.length - failed - undetermined,
86
98
  failed,
99
+ undetermined,
100
+ undeterminedFrom,
87
101
  failures,
88
102
  truncated: failed > failures.length,
89
103
  topIssues,
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Caller-supplied patterns, answered off this thread.
3
+ *
4
+ * A `matches` check, a JSON Schema `pattern` and a JSONPath `=~` filter all
5
+ * run a regex someone else wrote over text someone else chose, and the
6
+ * evaluators that ask are synchronous. A synchronous `RegExp` cannot be
7
+ * interrupted: `a*a*a*a*b` over 200 characters held the whole process for
8
+ * 3.5 s, over 400 for 108 s. And when JavaScriptCore gives up on a
9
+ * backtracking match it returns "no match", so a gate read a real match as a
10
+ * miss.
11
+ *
12
+ * So the evaluator does not run the pattern. It asks {@link RegexAnswers},
13
+ * which answers from what the worker in `@crewhaus/tool-safety/regex` has
14
+ * already settled and records anything else as pending. A caller runs the
15
+ * evaluator once to learn what it will ask, {@link RegexAnswers.resolve}s the
16
+ * pending questions in one worker session under one deadline, and runs it
17
+ * again with the answers ({@link withRegexAnswers} does the loop). Every
18
+ * answer is tri-state: a pattern that could not be run to an answer is
19
+ * `undetermined`, never `false`, and one the screen refuses (bad syntax, or
20
+ * a shape that backtracks exponentially) is `refused`, with the reason.
21
+ *
22
+ * {@link testPatternSync} is the fallback for a synchronous caller that has
23
+ * no answers to hand (a library call from another package): the same screen,
24
+ * a cap on the input it will run over on this thread, and JavaScriptCore's
25
+ * give-up read as undetermined. It bounds the exponential shapes and the
26
+ * false verdict, not a polynomial pattern's time; a tool reaches for
27
+ * {@link RegexAnswers} instead.
28
+ */
29
+ import { type RegexRejectCode, type RegexSession } from "@crewhaus/tool-safety/regex";
30
+ /** What a caller's pattern says about one input. */
31
+ export type RegexAnswer = boolean
32
+ /** Refused before it ran: invalid syntax, bad flags, or a catastrophic shape. */
33
+ | {
34
+ readonly refused: string;
35
+ readonly code: RegexRejectCode;
36
+ }
37
+ /** Could not be run to an answer: the deadline, the engine giving up, a cap. */
38
+ | {
39
+ readonly undetermined: string;
40
+ };
41
+ /** Where a tool call's patterns run: its abort signal, and whose runaway workers they count as. */
42
+ export type RegexRunContext = {
43
+ readonly signal?: AbortSignal;
44
+ readonly runawayKey?: string;
45
+ };
46
+ /** The pattern screen's limits for caller patterns: long schema patterns (a URI, a language tag) pass. */
47
+ export declare const CALLER_PATTERN_LIMITS: {
48
+ readonly maxPatternChars: 10000;
49
+ };
50
+ /** Wall-clock budget for all of one call's pattern questions. */
51
+ export declare const CALLER_PATTERN_DEADLINE_MS = 5000;
52
+ /** The longest single input a pattern is run over in the worker. */
53
+ export declare const CALLER_PATTERN_MAX_ITEM_CHARS = 1048576;
54
+ /** The most distinct (pattern, input) questions one call may ask. */
55
+ export declare const MAX_PATTERN_QUESTIONS = 1000000;
56
+ /** The longest input {@link testPatternSync} runs a pattern over on the caller's thread. */
57
+ export declare const SYNC_PATTERN_MAX_INPUT_CHARS = 65536;
58
+ /**
59
+ * Test seam: {@link RegexAnswers.resolve} runs its questions in what `wrap`
60
+ * returns for the worker session it opens, so a test can script an answer
61
+ * the real engine gives only when it is slow enough (a give-up) instead of
62
+ * depending on how fast the machine is. Pass `undefined` to restore.
63
+ */
64
+ export declare function _setRegexAnswersSessionForTest(wrap: ((real: RegexSession) => RegexSession) | undefined): void;
65
+ /** The ctx fields a tool's `execute` receives that a regex run uses. */
66
+ export declare function regexRunContext(ctx: unknown): RegexRunContext;
67
+ /**
68
+ * The answers a synchronous evaluator reads, and the questions it has asked
69
+ * that are not answered yet.
70
+ */
71
+ export declare class RegexAnswers {
72
+ #private;
73
+ /**
74
+ * The answer for `pattern` (with `flags`) on `input`, or undefined when it
75
+ * is not known yet; the question is then recorded, for {@link resolve}.
76
+ * The evaluator decides what to assume meanwhile. Past
77
+ * {@link MAX_PATTERN_QUESTIONS} distinct questions in one call, a new one
78
+ * is answered undetermined rather than recorded.
79
+ */
80
+ lookup(pattern: string, flags: string, input: string): RegexAnswer | undefined;
81
+ /** How many questions wait for {@link resolve}. */
82
+ get pending(): number;
83
+ /**
84
+ * Forget the pending questions: for an evaluator whose answer no pattern
85
+ * can change (it ran out of its own work budget), so a caller running it
86
+ * in rounds stops instead of answering them.
87
+ */
88
+ discardPending(): void;
89
+ /**
90
+ * Answer every pending question in one worker session: one `testEach` per
91
+ * pattern over the inputs it was asked about, all of them within
92
+ * `deadlineMs`. What the worker cannot answer is recorded as undetermined
93
+ * (or refused), so after this nothing is pending.
94
+ */
95
+ resolve(ctx?: RegexRunContext, options?: {
96
+ readonly deadlineMs?: number;
97
+ }): Promise<void>;
98
+ }
99
+ /**
100
+ * Run a synchronous evaluator that reads {@link RegexAnswers}, resolving
101
+ * what it asked and running it again, until a run asks nothing new. An
102
+ * evaluator whose later questions depend on earlier answers (a JSON Schema
103
+ * `if`, the next filter in a path) settles in a few rounds; past
104
+ * `maxRounds` the result is undetermined rather than a guess. All rounds
105
+ * share one deadline.
106
+ */
107
+ export declare function withRegexAnswers<T>(evaluate: (answers: RegexAnswers) => T, ctx?: RegexRunContext, options?: {
108
+ readonly maxRounds?: number;
109
+ readonly deadlineMs?: number;
110
+ }): Promise<{
111
+ readonly value: T;
112
+ } | {
113
+ readonly undetermined: string;
114
+ }>;
115
+ /** A screened, compiled caller pattern, or why it was refused. */
116
+ export type ScreenedPattern = {
117
+ readonly ok: true;
118
+ readonly regex: RegExp;
119
+ } | {
120
+ readonly ok: false;
121
+ readonly answer: {
122
+ refused: string;
123
+ code: RegexRejectCode;
124
+ };
125
+ };
126
+ /** Screen and compile a caller pattern for {@link runPatternSync}; remembered per pattern. */
127
+ export declare function screenCallerPattern(pattern: string, flags: string): ScreenedPattern;
128
+ /**
129
+ * Run a screened pattern on this thread: an input longer than
130
+ * {@link SYNC_PATTERN_MAX_INPUT_CHARS} is not run, and a no-match that took
131
+ * as long as JavaScriptCore's give-up is undetermined.
132
+ */
133
+ export declare function runPatternSync(regex: RegExp, input: string): RegexAnswer;
134
+ /**
135
+ * Run a caller's pattern on this thread, for a synchronous caller with no
136
+ * {@link RegexAnswers} to hand. The pattern is screened (a refused one is
137
+ * `refused`), an input longer than {@link SYNC_PATTERN_MAX_INPUT_CHARS} is
138
+ * not run, and a no-match that took as long as JavaScriptCore's give-up is
139
+ * undetermined. That bounds the exponential shapes and the false verdict;
140
+ * a polynomial pattern's time is bounded only by the input cap.
141
+ */
142
+ export declare function testPatternSync(pattern: string, flags: string, input: string): RegexAnswer;
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Caller-supplied patterns, answered off this thread.
3
+ *
4
+ * A `matches` check, a JSON Schema `pattern` and a JSONPath `=~` filter all
5
+ * run a regex someone else wrote over text someone else chose, and the
6
+ * evaluators that ask are synchronous. A synchronous `RegExp` cannot be
7
+ * interrupted: `a*a*a*a*b` over 200 characters held the whole process for
8
+ * 3.5 s, over 400 for 108 s. And when JavaScriptCore gives up on a
9
+ * backtracking match it returns "no match", so a gate read a real match as a
10
+ * miss.
11
+ *
12
+ * So the evaluator does not run the pattern. It asks {@link RegexAnswers},
13
+ * which answers from what the worker in `@crewhaus/tool-safety/regex` has
14
+ * already settled and records anything else as pending. A caller runs the
15
+ * evaluator once to learn what it will ask, {@link RegexAnswers.resolve}s the
16
+ * pending questions in one worker session under one deadline, and runs it
17
+ * again with the answers ({@link withRegexAnswers} does the loop). Every
18
+ * answer is tri-state: a pattern that could not be run to an answer is
19
+ * `undetermined`, never `false`, and one the screen refuses (bad syntax, or
20
+ * a shape that backtracks exponentially) is `refused`, with the reason.
21
+ *
22
+ * {@link testPatternSync} is the fallback for a synchronous caller that has
23
+ * no answers to hand (a library call from another package): the same screen,
24
+ * a cap on the input it will run over on this thread, and JavaScriptCore's
25
+ * give-up read as undetermined. It bounds the exponential shapes and the
26
+ * false verdict, not a polynomial pattern's time; a tool reaches for
27
+ * {@link RegexAnswers} instead.
28
+ */
29
+ import { compileUserRegex, describeRegexOutcome, openRegexSession, } from "@crewhaus/tool-safety/regex";
30
+ /** The pattern screen's limits for caller patterns: long schema patterns (a URI, a language tag) pass. */
31
+ export const CALLER_PATTERN_LIMITS = { maxPatternChars: 10_000 };
32
+ /** Wall-clock budget for all of one call's pattern questions. */
33
+ export const CALLER_PATTERN_DEADLINE_MS = 5_000;
34
+ /** The longest single input a pattern is run over in the worker. */
35
+ export const CALLER_PATTERN_MAX_ITEM_CHARS = 1_048_576;
36
+ /** The most distinct (pattern, input) questions one call may ask. */
37
+ export const MAX_PATTERN_QUESTIONS = 1_000_000;
38
+ /** The longest input {@link testPatternSync} runs a pattern over on the caller's thread. */
39
+ export const SYNC_PATTERN_MAX_INPUT_CHARS = 65_536;
40
+ /**
41
+ * A no-match that took this long is JavaScriptCore abandoning the match,
42
+ * not a real no-match (the tool-safety README measured give-ups at 0.4-3 s,
43
+ * genuine no-matches in microseconds to milliseconds).
44
+ */
45
+ const GIVE_UP_MS = 100;
46
+ const realSession = (real) => real;
47
+ let wrapSession = realSession;
48
+ /**
49
+ * Test seam: {@link RegexAnswers.resolve} runs its questions in what `wrap`
50
+ * returns for the worker session it opens, so a test can script an answer
51
+ * the real engine gives only when it is slow enough (a give-up) instead of
52
+ * depending on how fast the machine is. Pass `undefined` to restore.
53
+ */
54
+ export function _setRegexAnswersSessionForTest(wrap) {
55
+ wrapSession = wrap ?? realSession;
56
+ }
57
+ /** The ctx fields a tool's `execute` receives that a regex run uses. */
58
+ export function regexRunContext(ctx) {
59
+ const c = ctx;
60
+ return {
61
+ ...(c?.signal === undefined ? {} : { signal: c.signal }),
62
+ ...(typeof c?.runContext?.sessionId === "string" ? { runawayKey: c.runContext.sessionId } : {}),
63
+ };
64
+ }
65
+ const keyOf = (pattern, flags) => `${flags}\u0000${pattern}`;
66
+ /**
67
+ * The answers a synchronous evaluator reads, and the questions it has asked
68
+ * that are not answered yet.
69
+ */
70
+ export class RegexAnswers {
71
+ #known = new Map();
72
+ #pending = new Map();
73
+ #asked = 0;
74
+ /**
75
+ * The answer for `pattern` (with `flags`) on `input`, or undefined when it
76
+ * is not known yet; the question is then recorded, for {@link resolve}.
77
+ * The evaluator decides what to assume meanwhile. Past
78
+ * {@link MAX_PATTERN_QUESTIONS} distinct questions in one call, a new one
79
+ * is answered undetermined rather than recorded.
80
+ */
81
+ lookup(pattern, flags, input) {
82
+ const key = keyOf(pattern, flags);
83
+ const known = this.#known.get(key)?.get(input);
84
+ if (known !== undefined)
85
+ return known;
86
+ let group = this.#pending.get(key);
87
+ if (group?.inputs.has(input) === true)
88
+ return undefined;
89
+ if (this.#asked >= MAX_PATTERN_QUESTIONS) {
90
+ return {
91
+ undetermined: `the call asked more than ${MAX_PATTERN_QUESTIONS} distinct pattern questions`,
92
+ };
93
+ }
94
+ if (group === undefined) {
95
+ group = { pattern, flags, inputs: new Set() };
96
+ this.#pending.set(key, group);
97
+ }
98
+ group.inputs.add(input);
99
+ this.#asked += 1;
100
+ return undefined;
101
+ }
102
+ /** How many questions wait for {@link resolve}. */
103
+ get pending() {
104
+ let n = 0;
105
+ for (const group of this.#pending.values())
106
+ n += group.inputs.size;
107
+ return n;
108
+ }
109
+ /**
110
+ * Forget the pending questions: for an evaluator whose answer no pattern
111
+ * can change (it ran out of its own work budget), so a caller running it
112
+ * in rounds stops instead of answering them.
113
+ */
114
+ discardPending() {
115
+ this.#pending.clear();
116
+ }
117
+ #settle(group, input, answer) {
118
+ const key = keyOf(group.pattern, group.flags);
119
+ let answers = this.#known.get(key);
120
+ if (answers === undefined) {
121
+ answers = new Map();
122
+ this.#known.set(key, answers);
123
+ }
124
+ answers.set(input, answer);
125
+ }
126
+ /**
127
+ * Answer every pending question in one worker session: one `testEach` per
128
+ * pattern over the inputs it was asked about, all of them within
129
+ * `deadlineMs`. What the worker cannot answer is recorded as undetermined
130
+ * (or refused), so after this nothing is pending.
131
+ */
132
+ async resolve(ctx = {}, options = {}) {
133
+ const deadlineMs = options.deadlineMs ?? CALLER_PATTERN_DEADLINE_MS;
134
+ const groups = [...this.#pending.values()];
135
+ this.#pending.clear();
136
+ if (groups.length === 0)
137
+ return;
138
+ const started = performance.now();
139
+ const session = wrapSession(openRegexSession());
140
+ try {
141
+ for (const group of groups) {
142
+ const inputs = [...group.inputs];
143
+ const left = Math.floor(deadlineMs - (performance.now() - started));
144
+ if (left < 1) {
145
+ for (const input of inputs) {
146
+ this.#settle(group, input, {
147
+ undetermined: `the call's ${deadlineMs} ms for running patterns ran out before this one could run`,
148
+ });
149
+ }
150
+ continue;
151
+ }
152
+ let total = inputs.length;
153
+ for (const input of inputs)
154
+ total += input.length;
155
+ const outcome = await session.run({
156
+ op: "testEach",
157
+ pattern: group.pattern,
158
+ flags: group.flags,
159
+ inputs,
160
+ maxMatches: inputs.length,
161
+ onGiveUp: "skip",
162
+ deadlineMs: left,
163
+ maxInputChars: total,
164
+ maxItemChars: CALLER_PATTERN_MAX_ITEM_CHARS,
165
+ limits: CALLER_PATTERN_LIMITS,
166
+ ...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
167
+ ...(ctx.runawayKey === undefined ? {} : { runawayKey: ctx.runawayKey }),
168
+ });
169
+ this.#record(group, inputs, outcome);
170
+ }
171
+ }
172
+ finally {
173
+ session.close();
174
+ }
175
+ }
176
+ #record(group, inputs, outcome) {
177
+ if (outcome.status === "rejected") {
178
+ const refused = { refused: describeRegexOutcome(outcome), code: outcome.code };
179
+ for (const input of inputs)
180
+ this.#settle(group, input, refused);
181
+ return;
182
+ }
183
+ // A full answer, or the part of one a timeout or give-up left behind.
184
+ const result = outcome.status === "ok"
185
+ ? outcome.result
186
+ : outcome.status === "timeout" || outcome.status === "gave-up"
187
+ ? outcome.partial
188
+ : undefined;
189
+ const why = outcome.status === "ok"
190
+ ? `the regex engine gave up on this input, or it is longer than the ${CALLER_PATTERN_MAX_ITEM_CHARS} characters a pattern is run over`
191
+ : describeRegexOutcome(outcome);
192
+ const matched = new Set(result?.matched ?? []);
193
+ const skipped = new Set(result?.undetermined ?? []);
194
+ const scanned = result?.scanned ?? 0;
195
+ for (const [i, input] of inputs.entries()) {
196
+ if (i >= scanned || skipped.has(i)) {
197
+ this.#settle(group, input, { undetermined: why });
198
+ }
199
+ else {
200
+ this.#settle(group, input, matched.has(i));
201
+ }
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Run a synchronous evaluator that reads {@link RegexAnswers}, resolving
207
+ * what it asked and running it again, until a run asks nothing new. An
208
+ * evaluator whose later questions depend on earlier answers (a JSON Schema
209
+ * `if`, the next filter in a path) settles in a few rounds; past
210
+ * `maxRounds` the result is undetermined rather than a guess. All rounds
211
+ * share one deadline.
212
+ */
213
+ export async function withRegexAnswers(evaluate, ctx = {}, options = {}) {
214
+ const maxRounds = options.maxRounds ?? 8;
215
+ const deadlineMs = options.deadlineMs ?? CALLER_PATTERN_DEADLINE_MS;
216
+ const started = performance.now();
217
+ const answers = new RegexAnswers();
218
+ for (let round = 0; round < maxRounds; round++) {
219
+ const value = evaluate(answers);
220
+ if (answers.pending === 0)
221
+ return { value };
222
+ const left = Math.max(1, Math.floor(deadlineMs - (performance.now() - started)));
223
+ await answers.resolve(ctx, { deadlineMs: left });
224
+ }
225
+ return {
226
+ undetermined: `the patterns' answers kept changing which patterns were asked for, past ${maxRounds} rounds`,
227
+ };
228
+ }
229
+ /** Recently screened patterns, least recent first: a loop over rows compiles each once. */
230
+ const screened = new Map();
231
+ const SCREENED_ENTRIES = 512;
232
+ /** Screen and compile a caller pattern for {@link runPatternSync}; remembered per pattern. */
233
+ export function screenCallerPattern(pattern, flags) {
234
+ const key = keyOf(pattern, flags);
235
+ const known = screened.get(key);
236
+ if (known !== undefined) {
237
+ screened.delete(key);
238
+ screened.set(key, known);
239
+ return known;
240
+ }
241
+ const compiled = compileUserRegex(pattern, flags, CALLER_PATTERN_LIMITS);
242
+ const out = compiled.ok
243
+ ? { ok: true, regex: compiled.regex }
244
+ : {
245
+ ok: false,
246
+ answer: {
247
+ refused: `pattern refused (${compiled.code}): ${compiled.reason}`,
248
+ code: compiled.code,
249
+ },
250
+ };
251
+ if (screened.size >= SCREENED_ENTRIES)
252
+ screened.delete(screened.keys().next().value);
253
+ screened.set(key, out);
254
+ return out;
255
+ }
256
+ /**
257
+ * Run a screened pattern on this thread: an input longer than
258
+ * {@link SYNC_PATTERN_MAX_INPUT_CHARS} is not run, and a no-match that took
259
+ * as long as JavaScriptCore's give-up is undetermined.
260
+ */
261
+ export function runPatternSync(regex, input) {
262
+ if (input.length > SYNC_PATTERN_MAX_INPUT_CHARS) {
263
+ return {
264
+ undetermined: `the input is ${input.length} characters, more than the ${SYNC_PATTERN_MAX_INPUT_CHARS} a pattern is run over on the caller's thread`,
265
+ };
266
+ }
267
+ regex.lastIndex = 0;
268
+ const started = performance.now();
269
+ const hit = regex.test(input);
270
+ const ms = performance.now() - started;
271
+ if (!hit && ms >= GIVE_UP_MS) {
272
+ return {
273
+ undetermined: `the regex engine gave up after ${Math.round(ms)} ms, which it reports as no match, so whether it matches is unknown`,
274
+ };
275
+ }
276
+ return hit;
277
+ }
278
+ /**
279
+ * Run a caller's pattern on this thread, for a synchronous caller with no
280
+ * {@link RegexAnswers} to hand. The pattern is screened (a refused one is
281
+ * `refused`), an input longer than {@link SYNC_PATTERN_MAX_INPUT_CHARS} is
282
+ * not run, and a no-match that took as long as JavaScriptCore's give-up is
283
+ * undetermined. That bounds the exponential shapes and the false verdict;
284
+ * a polynomial pattern's time is bounded only by the input cap.
285
+ */
286
+ export function testPatternSync(pattern, flags, input) {
287
+ const s = screenCallerPattern(pattern, flags);
288
+ return s.ok ? runPatternSync(s.regex, input) : s.answer;
289
+ }
@@ -19,8 +19,27 @@ export type SchemaDiffResult = {
19
19
  unknown: number;
20
20
  };
21
21
  };
22
+ /**
23
+ * The deepest a schema may nest, counting every object and array, for it to
24
+ * be diffed. Each level is a level of recursion here, and the stack's own
25
+ * limit moves with the platform and with how warm the code is: a schema
26
+ * near it came back answered in one process and as a crash in another. A
27
+ * fixed limit answers the same everywhere. It also bounds the pointers: every
28
+ * change carries its full path, so a schema differing at every level made a
29
+ * result the square of its depth. The same limit tool-data puts on a
30
+ * document.
31
+ */
32
+ export declare const MAX_SCHEMA_DEPTH = 256;
33
+ /**
34
+ * Whether `value` nests deeper than `limit` objects and arrays, read with an
35
+ * explicit stack and abandoned as soon as it does. An object reached again
36
+ * (only a caller's own value can share one) is walked again only when it is
37
+ * reached deeper, so a cycle is too deep rather than endless.
38
+ */
39
+ export declare function nestsDeeperThan(value: unknown, limit: number): boolean;
22
40
  /**
23
41
  * Diff two schemas and classify each change. See the module comment for what
24
42
  * "compatible" means here and for the cases deliberately left `unknown`.
43
+ * Refuses a schema nested past {@link MAX_SCHEMA_DEPTH}, before reading it.
25
44
  */
26
45
  export declare function diffSchemas(before: unknown, after: unknown): SchemaDiffResult;