@mmnto/totem 1.15.2 → 1.15.3
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/dist/compile-lesson.d.ts.map +1 -1
- package/dist/compile-lesson.js +25 -4
- package/dist/compile-lesson.js.map +1 -1
- package/dist/compile-lesson.test.js +120 -0
- package/dist/compile-lesson.test.js.map +1 -1
- package/dist/compiler-schema.d.ts +58 -16
- package/dist/compiler-schema.d.ts.map +1 -1
- package/dist/compiler-schema.js +79 -0
- package/dist/compiler-schema.js.map +1 -1
- package/dist/compiler-schema.test.js +160 -1
- package/dist/compiler-schema.test.js.map +1 -1
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +1 -1
- package/dist/compiler.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/regex-safety/apply-rules-bounded.d.ts +35 -0
- package/dist/regex-safety/apply-rules-bounded.d.ts.map +1 -0
- package/dist/regex-safety/apply-rules-bounded.js +114 -0
- package/dist/regex-safety/apply-rules-bounded.js.map +1 -0
- package/dist/regex-safety/apply-rules-bounded.test.d.ts +2 -0
- package/dist/regex-safety/apply-rules-bounded.test.d.ts.map +1 -0
- package/dist/regex-safety/apply-rules-bounded.test.js +136 -0
- package/dist/regex-safety/apply-rules-bounded.test.js.map +1 -0
- package/dist/regex-safety/evaluator.d.ts +95 -0
- package/dist/regex-safety/evaluator.d.ts.map +1 -0
- package/dist/regex-safety/evaluator.js +314 -0
- package/dist/regex-safety/evaluator.js.map +1 -0
- package/dist/regex-safety/evaluator.test.d.ts +2 -0
- package/dist/regex-safety/evaluator.test.d.ts.map +1 -0
- package/dist/regex-safety/evaluator.test.js +224 -0
- package/dist/regex-safety/evaluator.test.js.map +1 -0
- package/dist/regex-safety/telemetry.d.ts +50 -0
- package/dist/regex-safety/telemetry.d.ts.map +1 -0
- package/dist/regex-safety/telemetry.js +50 -0
- package/dist/regex-safety/telemetry.js.map +1 -0
- package/dist/regex-safety/telemetry.test.d.ts +2 -0
- package/dist/regex-safety/telemetry.test.d.ts.map +1 -0
- package/dist/regex-safety/telemetry.test.js +82 -0
- package/dist/regex-safety/telemetry.test.js.map +1 -0
- package/dist/regex-safety/worker.d.ts +31 -0
- package/dist/regex-safety/worker.d.ts.map +1 -0
- package/dist/regex-safety/worker.js +51 -0
- package/dist/regex-safety/worker.js.map +1 -0
- package/dist/rule-engine.d.ts +1 -0
- package/dist/rule-engine.d.ts.map +1 -1
- package/dist/rule-engine.js +1 -1
- package/dist/rule-engine.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-execution variant of `applyRulesToAdditions` (mmnto-ai/totem#1641).
|
|
3
|
+
*
|
|
4
|
+
* Routes every regex rule through the persistent-worker `RegexEvaluator`
|
|
5
|
+
* so a catastrophic-backtracking pattern terminates at the configured
|
|
6
|
+
* timeout rather than hanging the lint process indefinitely. Engine layer
|
|
7
|
+
* is policy-free: it records `RuleTimeoutOutcome` entries and lets the
|
|
8
|
+
* caller (CLI) decide whether to surface them as exit-code contributors
|
|
9
|
+
* (strict) or as skipped warnings (lenient).
|
|
10
|
+
*
|
|
11
|
+
* Scope: regex-engine rules only. ast / ast-grep rules are not ReDoS-
|
|
12
|
+
* susceptible and are evaluated by `applyAstRulesToAdditions` / the
|
|
13
|
+
* compound-rule pipeline under separate bounds.
|
|
14
|
+
*/
|
|
15
|
+
import { TotemParseError } from '../errors.js';
|
|
16
|
+
import { extractJustification, isSuppressed, matchesGlob, } from '../rule-engine.js';
|
|
17
|
+
import { redactPath } from './telemetry.js';
|
|
18
|
+
function fileMatchesGlobs(filePath, globs) {
|
|
19
|
+
const hasIncludes = globs.some((g) => !g.startsWith('!'));
|
|
20
|
+
let matched = !hasIncludes;
|
|
21
|
+
for (const glob of globs) {
|
|
22
|
+
if (glob.startsWith('!')) {
|
|
23
|
+
if (matchesGlob(filePath, glob.slice(1)))
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
else if (matchesGlob(filePath, glob)) {
|
|
27
|
+
matched = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return matched;
|
|
31
|
+
}
|
|
32
|
+
export async function applyRulesToAdditionsBounded(ctx, rules, additions, options, onRuleEvent) {
|
|
33
|
+
const violations = [];
|
|
34
|
+
const timeoutOutcomes = [];
|
|
35
|
+
if (additions.length === 0 || rules.length === 0) {
|
|
36
|
+
return { violations, timeoutOutcomes };
|
|
37
|
+
}
|
|
38
|
+
const regexRules = rules.filter((r) => r.engine === 'regex' || !r.engine);
|
|
39
|
+
for (const rule of regexRules) {
|
|
40
|
+
// Partition additions by file so the evaluator can batch one rule per
|
|
41
|
+
// file at a time. File granularity matches the fileGlobs scoping and
|
|
42
|
+
// keeps timeout isolation per rule-file pair.
|
|
43
|
+
const byFile = new Map();
|
|
44
|
+
for (const addition of additions) {
|
|
45
|
+
if (rule.fileGlobs && rule.fileGlobs.length > 0) {
|
|
46
|
+
if (!fileMatchesGlobs(addition.file, rule.fileGlobs))
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const bucket = byFile.get(addition.file) ?? [];
|
|
50
|
+
bucket.push(addition);
|
|
51
|
+
byFile.set(addition.file, bucket);
|
|
52
|
+
}
|
|
53
|
+
for (const [file, fileAdditions] of byFile) {
|
|
54
|
+
const result = await options.evaluator.evaluate({
|
|
55
|
+
ruleHash: rule.lessonHash,
|
|
56
|
+
pattern: rule.pattern,
|
|
57
|
+
flags: '',
|
|
58
|
+
lines: fileAdditions.map((a) => a.line),
|
|
59
|
+
redactedPath: redactPath(file, options.repoRoot),
|
|
60
|
+
});
|
|
61
|
+
if (result.kind === 'error') {
|
|
62
|
+
// Fail loud (matches rule-engine.ts pre-#1641 contract at line
|
|
63
|
+
// 247). An uncompilable compiled rule means the validator was
|
|
64
|
+
// bypassed or the manifest was edited by hand; silently skipping
|
|
65
|
+
// would mark the diff "compliant" while a load-bearing rule is
|
|
66
|
+
// mute.
|
|
67
|
+
throw new TotemParseError(`Rule ${rule.lessonHash} has an invalid regex pattern and cannot be evaluated.`, `Re-run 'totem lesson compile' to regenerate the rule, or archive it via 'totem doctor --pr' if the source lesson cannot produce a valid pattern. Pattern: ${JSON.stringify(rule.pattern)} — worker reported: ${result.message}`);
|
|
68
|
+
}
|
|
69
|
+
if (result.kind === 'timeout') {
|
|
70
|
+
timeoutOutcomes.push({
|
|
71
|
+
ruleHash: rule.lessonHash,
|
|
72
|
+
file,
|
|
73
|
+
elapsedMs: result.elapsedMs,
|
|
74
|
+
mode: options.timeoutMode,
|
|
75
|
+
});
|
|
76
|
+
onRuleEvent?.('failure', rule.lessonHash, {
|
|
77
|
+
file,
|
|
78
|
+
line: 0,
|
|
79
|
+
failureReason: `timeout after ${result.elapsedMs}ms (mode: ${options.timeoutMode})`,
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const matchedIndex of result.matchedIndices) {
|
|
84
|
+
const addition = fileAdditions[matchedIndex];
|
|
85
|
+
if (!addition)
|
|
86
|
+
continue;
|
|
87
|
+
if (isSuppressed(ctx, addition.line, addition.precedingLine)) {
|
|
88
|
+
onRuleEvent?.('suppress', rule.lessonHash, {
|
|
89
|
+
file: addition.file,
|
|
90
|
+
line: addition.lineNumber,
|
|
91
|
+
justification: extractJustification(ctx, addition.line, addition.precedingLine),
|
|
92
|
+
immutable: rule.immutable,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
onRuleEvent?.('trigger', rule.lessonHash, {
|
|
97
|
+
file: addition.file,
|
|
98
|
+
line: addition.lineNumber,
|
|
99
|
+
astContext: addition.astContext,
|
|
100
|
+
});
|
|
101
|
+
if (!addition.astContext || addition.astContext === 'code') {
|
|
102
|
+
violations.push({
|
|
103
|
+
rule,
|
|
104
|
+
file: addition.file,
|
|
105
|
+
line: addition.line,
|
|
106
|
+
lineNumber: addition.lineNumber,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { violations, timeoutOutcomes };
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=apply-rules-bounded.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-rules-bounded.js","sourceRoot":"","sources":["../../src/regex-safety/apply-rules-bounded.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-rules-bounded.test.d.ts","sourceRoot":"","sources":["../../src/regex-safety/apply-rules-bounded.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { makeRuleEngineCtx } from '../test-utils.js';
|
|
3
|
+
import { applyRulesToAdditionsBounded } from './apply-rules-bounded.js';
|
|
4
|
+
import { RegexEvaluator } from './evaluator.js';
|
|
5
|
+
function addition(file, line, lineNumber) {
|
|
6
|
+
return { file, line, lineNumber, precedingLine: null };
|
|
7
|
+
}
|
|
8
|
+
function regexRule(lessonHash, pattern, engine = 'regex') {
|
|
9
|
+
return {
|
|
10
|
+
lessonHash,
|
|
11
|
+
lessonHeading: `rule ${lessonHash}`,
|
|
12
|
+
pattern,
|
|
13
|
+
message: `violation for ${lessonHash}`,
|
|
14
|
+
engine,
|
|
15
|
+
compiledAt: '2026-04-23T00:00:00Z',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('applyRulesToAdditionsBounded — happy path', () => {
|
|
19
|
+
it('flags matching additions under a sync evaluator', async () => {
|
|
20
|
+
const evaluator = new RegexEvaluator();
|
|
21
|
+
try {
|
|
22
|
+
const rules = [regexRule('h1', 'console\\.log')];
|
|
23
|
+
const additions = [
|
|
24
|
+
addition('foo.ts', 'console.log("a")', 10),
|
|
25
|
+
addition('foo.ts', 'logger.info("b")', 11),
|
|
26
|
+
];
|
|
27
|
+
const result = await applyRulesToAdditionsBounded(makeRuleEngineCtx(), rules, additions, {
|
|
28
|
+
evaluator,
|
|
29
|
+
timeoutMode: 'strict',
|
|
30
|
+
repoRoot: '/tmp/repo',
|
|
31
|
+
});
|
|
32
|
+
expect(result.violations).toHaveLength(1);
|
|
33
|
+
expect(result.violations[0]?.rule.lessonHash).toBe('h1');
|
|
34
|
+
expect(result.violations[0]?.lineNumber).toBe(10);
|
|
35
|
+
expect(result.timeoutOutcomes).toEqual([]);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await evaluator.dispose();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
it('respects fileGlobs when evaluating additions', async () => {
|
|
42
|
+
const evaluator = new RegexEvaluator();
|
|
43
|
+
try {
|
|
44
|
+
const rule = {
|
|
45
|
+
...regexRule('scoped', 'foo'),
|
|
46
|
+
fileGlobs: ['**/*.md'],
|
|
47
|
+
};
|
|
48
|
+
const additions = [
|
|
49
|
+
addition('readme.md', 'foo', 1),
|
|
50
|
+
addition('app.ts', 'foo', 1),
|
|
51
|
+
];
|
|
52
|
+
const result = await applyRulesToAdditionsBounded(makeRuleEngineCtx(), [rule], additions, {
|
|
53
|
+
evaluator,
|
|
54
|
+
timeoutMode: 'strict',
|
|
55
|
+
repoRoot: '/tmp/repo',
|
|
56
|
+
});
|
|
57
|
+
expect(result.violations).toHaveLength(1);
|
|
58
|
+
expect(result.violations[0]?.file).toBe('readme.md');
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await evaluator.dispose();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('applyRulesToAdditionsBounded — timeout strict', () => {
|
|
66
|
+
it('returns a RuleTimeoutOutcome and excludes violations for the timing-out rule', async () => {
|
|
67
|
+
const evaluator = new RegexEvaluator({ timeoutMs: 150, softWarningMs: 50 });
|
|
68
|
+
try {
|
|
69
|
+
const rules = [regexRule('redos', '(a+)+b'), regexRule('healthy', 'foo')];
|
|
70
|
+
const additions = [
|
|
71
|
+
addition('a.ts', 'a'.repeat(50000) + 'c', 1),
|
|
72
|
+
addition('b.ts', 'foo', 2),
|
|
73
|
+
];
|
|
74
|
+
const result = await applyRulesToAdditionsBounded(makeRuleEngineCtx(), rules, additions, {
|
|
75
|
+
evaluator,
|
|
76
|
+
timeoutMode: 'strict',
|
|
77
|
+
repoRoot: '/tmp/repo',
|
|
78
|
+
});
|
|
79
|
+
const timeoutHashes = result.timeoutOutcomes.map((o) => o.ruleHash);
|
|
80
|
+
expect(timeoutHashes).toContain('redos');
|
|
81
|
+
// Healthy rule should still produce its violation.
|
|
82
|
+
const healthyViolation = result.violations.find((v) => v.rule.lessonHash === 'healthy');
|
|
83
|
+
expect(healthyViolation).toBeDefined();
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await evaluator.dispose();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('applyRulesToAdditionsBounded — timeout lenient', () => {
|
|
91
|
+
it('records timeout outcomes but does not differ from strict at the engine layer', async () => {
|
|
92
|
+
// The strict/lenient semantics are enforced at the CLI layer based on
|
|
93
|
+
// the timeoutOutcomes array this function returns. The engine itself
|
|
94
|
+
// is policy-free — it records the outcome and lets the caller decide
|
|
95
|
+
// the exit-code effect. Locking this in prevents future drift where
|
|
96
|
+
// the engine starts making policy decisions instead of surfacing them.
|
|
97
|
+
const evaluator = new RegexEvaluator({ timeoutMs: 150, softWarningMs: 50 });
|
|
98
|
+
try {
|
|
99
|
+
const rules = [regexRule('redos', '(a+)+b')];
|
|
100
|
+
const additions = [addition('a.ts', 'a'.repeat(50000) + 'c', 1)];
|
|
101
|
+
const strict = await applyRulesToAdditionsBounded(makeRuleEngineCtx(), rules, additions, {
|
|
102
|
+
evaluator,
|
|
103
|
+
timeoutMode: 'strict',
|
|
104
|
+
repoRoot: '/tmp/repo',
|
|
105
|
+
});
|
|
106
|
+
const lenient = await applyRulesToAdditionsBounded(makeRuleEngineCtx(), rules, additions, {
|
|
107
|
+
evaluator,
|
|
108
|
+
timeoutMode: 'lenient',
|
|
109
|
+
repoRoot: '/tmp/repo',
|
|
110
|
+
});
|
|
111
|
+
expect(strict.timeoutOutcomes).toHaveLength(1);
|
|
112
|
+
expect(lenient.timeoutOutcomes).toHaveLength(1);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await evaluator.dispose();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('applyRulesToAdditionsBounded — invalid pattern', () => {
|
|
120
|
+
it('fails loud on a compiled rule with an invalid regex (matches existing rule-engine contract)', async () => {
|
|
121
|
+
const evaluator = new RegexEvaluator();
|
|
122
|
+
try {
|
|
123
|
+
const rules = [regexRule('broken', '(unclosed')];
|
|
124
|
+
const additions = [addition('a.ts', 'foo', 1)];
|
|
125
|
+
await expect(applyRulesToAdditionsBounded(makeRuleEngineCtx(), rules, additions, {
|
|
126
|
+
evaluator,
|
|
127
|
+
timeoutMode: 'strict',
|
|
128
|
+
repoRoot: '/tmp/repo',
|
|
129
|
+
})).rejects.toThrow(/invalid regex|cannot be evaluated/i);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await evaluator.dispose();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=apply-rules-bounded.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply-rules-bounded.test.js","sourceRoot":"","sources":["../../src/regex-safety/apply-rules-bounded.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent-worker regex evaluator with per-batch timeout (mmnto-ai/totem#1641).
|
|
3
|
+
*
|
|
4
|
+
* Spawns one Node worker thread on construction, serializes batches onto
|
|
5
|
+
* it, and enforces a main-thread timeout. If a pattern catastrophic-
|
|
6
|
+
* backtracks inside the worker, the main-thread timer fires, calls
|
|
7
|
+
* `worker.terminate()`, and respawns a fresh worker for the next batch.
|
|
8
|
+
* Every evaluation resolves with one of three outcomes — `ok` (matched
|
|
9
|
+
* indices + elapsed + softWarning flag), `timeout` (the worker was
|
|
10
|
+
* terminated), or `error` (the pattern was syntactically invalid; the
|
|
11
|
+
* worker is still alive). The caller decides strict vs lenient handling.
|
|
12
|
+
*
|
|
13
|
+
* Invariants:
|
|
14
|
+
* - At most one `Worker` alive per evaluator instance.
|
|
15
|
+
* - `pending` never holds stale entries past a batch's terminal state.
|
|
16
|
+
* - Batches are serialized (one in-flight at a time) — no multiplexing.
|
|
17
|
+
* - Telemetry is emitted on every terminal outcome via the
|
|
18
|
+
* `onTelemetry` callback the caller supplies; if absent, telemetry
|
|
19
|
+
* is silently dropped (safe — the evaluator itself never fails on
|
|
20
|
+
* telemetry-sink failure).
|
|
21
|
+
*/
|
|
22
|
+
import type { RegexTelemetry } from './telemetry.js';
|
|
23
|
+
export interface RegexEvaluatorConfig {
|
|
24
|
+
/** Hard timeout per batch (ms). Exceeded batches terminate the worker. */
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
/** Soft-warning threshold (ms). Sub-timeout but slow; sets the flag on telemetry. */
|
|
27
|
+
softWarningMs: number;
|
|
28
|
+
}
|
|
29
|
+
export interface EvaluateInput {
|
|
30
|
+
ruleHash: string;
|
|
31
|
+
pattern: string;
|
|
32
|
+
flags: string;
|
|
33
|
+
lines: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
export type EvaluateResult = {
|
|
36
|
+
kind: 'ok';
|
|
37
|
+
matchedIndices: number[];
|
|
38
|
+
elapsedMs: number;
|
|
39
|
+
softWarningTriggered: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'timeout';
|
|
42
|
+
elapsedMs: number;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'error';
|
|
45
|
+
message: string;
|
|
46
|
+
elapsedMs: number;
|
|
47
|
+
};
|
|
48
|
+
export declare class RegexEvaluator {
|
|
49
|
+
private worker;
|
|
50
|
+
private readonly pending;
|
|
51
|
+
private readonly config;
|
|
52
|
+
private readonly onTelemetry;
|
|
53
|
+
private queue;
|
|
54
|
+
private disposed;
|
|
55
|
+
/**
|
|
56
|
+
* Coalesces concurrent respawn requests (mmnto-ai/totem#1641 Shield review
|
|
57
|
+
* round-1). Without this, a timeout event firing at roughly the same
|
|
58
|
+
* moment as a worker `error` event can call `spawnWorker()` twice,
|
|
59
|
+
* leaking a thread. `evaluate()` also awaits this promise before
|
|
60
|
+
* `postMessage` so a batch scheduled during a respawn waits for the
|
|
61
|
+
* new worker instead of silently dropping against a null handle.
|
|
62
|
+
*/
|
|
63
|
+
private respawnPromise;
|
|
64
|
+
/**
|
|
65
|
+
* Worker-online gate (mmnto-ai/totem#1641, CI round-1 fix). The Node
|
|
66
|
+
* `Worker` constructor returns before the thread is actually running
|
|
67
|
+
* (thread-spawn takes ~30-50ms). If `evaluate()` starts its timeout
|
|
68
|
+
* timer before the worker is online, a slow CI box can trip a
|
|
69
|
+
* spurious timeout on the first batch. Gate postMessage on this
|
|
70
|
+
* promise so cold-start cost never counts against the budget.
|
|
71
|
+
*/
|
|
72
|
+
private workerReady;
|
|
73
|
+
/**
|
|
74
|
+
* Consecutive-respawn counter (Shield review round-1). If the worker
|
|
75
|
+
* keeps dying at spawn time (missing worker.js, syntax error in the
|
|
76
|
+
* worker script, etc.), unbounded respawn becomes a CPU-pegging loop.
|
|
77
|
+
* The counter increments on each respawn, resets on every successful
|
|
78
|
+
* evaluation, and flips `permanentlyFailed` once the budget is spent.
|
|
79
|
+
*/
|
|
80
|
+
private consecutiveRespawns;
|
|
81
|
+
private permanentlyFailed;
|
|
82
|
+
private static readonly MAX_CONSECUTIVE_RESPAWNS;
|
|
83
|
+
constructor(config?: Partial<RegexEvaluatorConfig>, onTelemetry?: (record: RegexTelemetry) => void);
|
|
84
|
+
evaluate(input: EvaluateInput & {
|
|
85
|
+
redactedPath?: string;
|
|
86
|
+
}): Promise<EvaluateResult>;
|
|
87
|
+
dispose(): Promise<void>;
|
|
88
|
+
private evaluateOnce;
|
|
89
|
+
private spawnWorker;
|
|
90
|
+
private respawnWorker;
|
|
91
|
+
private handleMessage;
|
|
92
|
+
private rejectAllPendingAsCrash;
|
|
93
|
+
private emitTelemetry;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../src/regex-safety/evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGrD,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,oBAAoB,EAAE,OAAO,CAAA;CAAE,GAC1F;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAkC1D,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiD;IAC7E,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,QAAQ,CAAS;IACzB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc,CAA8B;IACpD;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW,CAAoC;IACvD;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAK;gBAGnD,MAAM,GAAE,OAAO,CAAC,oBAAoB,CAAM,EAC1C,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI;IAO1C,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,cAAc,CAAC;IA6CnF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAa9B,OAAO,CAAC,YAAY;IAgDpB,OAAO,CAAC,WAAW;YAsCL,aAAa;IAkC3B,OAAO,CAAC,aAAa;IAkCrB,OAAO,CAAC,uBAAuB;IAiB/B,OAAO,CAAC,aAAa;CAQtB"}
|