@crewhaus/tool-schema 0.7.0 → 0.8.1
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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +50 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +92 -13
- package/dist/lib/assert.d.ts +36 -1
- package/dist/lib/assert.js +95 -14
- package/dist/lib/compare.js +30 -9
- package/dist/lib/formats.d.ts +13 -0
- package/dist/lib/formats.js +14 -1
- package/dist/lib/jsonschema.d.ts +170 -0
- package/dist/lib/jsonschema.js +530 -68
- package/dist/lib/records.d.ts +16 -0
- package/dist/lib/records.js +17 -3
- package/dist/lib/regex-answers.d.ts +142 -0
- package/dist/lib/regex-answers.js +289 -0
- package/dist/lib/schemadiff.d.ts +19 -0
- package/dist/lib/schemadiff.js +98 -17
- package/dist/lib/shape-index.d.ts +22 -0
- package/dist/lib/shape-index.js +157 -0
- package/dist/lib/summarize.js +28 -9
- package/dist/lib/value.d.ts +16 -2
- package/dist/lib/value.js +143 -13
- package/package.json +11 -6
package/dist/lib/records.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/lib/records.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/schemadiff.d.ts
CHANGED
|
@@ -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;
|