@mmnto/totem 1.15.1 → 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 +59 -4
- package/dist/compile-lesson.js.map +1 -1
- package/dist/compile-lesson.test.js +174 -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,314 @@
|
|
|
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 * as crypto from 'node:crypto';
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { Worker } from 'node:worker_threads';
|
|
27
|
+
import { TotemError } from '../errors.js';
|
|
28
|
+
const DEFAULT_CONFIG = {
|
|
29
|
+
timeoutMs: 100,
|
|
30
|
+
softWarningMs: 50,
|
|
31
|
+
};
|
|
32
|
+
function resolveWorkerPath() {
|
|
33
|
+
// In the built bundle, this module compiles to dist/regex-safety/evaluator.js
|
|
34
|
+
// sitting next to dist/regex-safety/worker.js — the relative lookup
|
|
35
|
+
// resolves directly. In vitest/dev the current module is the .ts source
|
|
36
|
+
// file under src/regex-safety/ with no sibling worker.js; fall back to
|
|
37
|
+
// the built dist path so tests can exercise the real worker without
|
|
38
|
+
// requiring a loader shim.
|
|
39
|
+
const here = fileURLToPath(import.meta.url);
|
|
40
|
+
const dir = path.dirname(here);
|
|
41
|
+
const siblingJs = path.join(dir, 'worker.js');
|
|
42
|
+
if (fs.existsSync(siblingJs))
|
|
43
|
+
return siblingJs;
|
|
44
|
+
const distFallback = path.resolve(dir, '..', '..', 'dist', 'regex-safety', 'worker.js');
|
|
45
|
+
if (fs.existsSync(distFallback))
|
|
46
|
+
return distFallback;
|
|
47
|
+
// Last resort: return the expected sibling path. Worker() will throw
|
|
48
|
+
// a descriptive MODULE_NOT_FOUND the caller can surface.
|
|
49
|
+
return siblingJs;
|
|
50
|
+
}
|
|
51
|
+
export class RegexEvaluator {
|
|
52
|
+
worker = null;
|
|
53
|
+
pending = new Map();
|
|
54
|
+
config;
|
|
55
|
+
onTelemetry;
|
|
56
|
+
queue = Promise.resolve();
|
|
57
|
+
disposed = false;
|
|
58
|
+
/**
|
|
59
|
+
* Coalesces concurrent respawn requests (mmnto-ai/totem#1641 Shield review
|
|
60
|
+
* round-1). Without this, a timeout event firing at roughly the same
|
|
61
|
+
* moment as a worker `error` event can call `spawnWorker()` twice,
|
|
62
|
+
* leaking a thread. `evaluate()` also awaits this promise before
|
|
63
|
+
* `postMessage` so a batch scheduled during a respawn waits for the
|
|
64
|
+
* new worker instead of silently dropping against a null handle.
|
|
65
|
+
*/
|
|
66
|
+
respawnPromise = null;
|
|
67
|
+
/**
|
|
68
|
+
* Worker-online gate (mmnto-ai/totem#1641, CI round-1 fix). The Node
|
|
69
|
+
* `Worker` constructor returns before the thread is actually running
|
|
70
|
+
* (thread-spawn takes ~30-50ms). If `evaluate()` starts its timeout
|
|
71
|
+
* timer before the worker is online, a slow CI box can trip a
|
|
72
|
+
* spurious timeout on the first batch. Gate postMessage on this
|
|
73
|
+
* promise so cold-start cost never counts against the budget.
|
|
74
|
+
*/
|
|
75
|
+
workerReady = Promise.resolve();
|
|
76
|
+
/**
|
|
77
|
+
* Consecutive-respawn counter (Shield review round-1). If the worker
|
|
78
|
+
* keeps dying at spawn time (missing worker.js, syntax error in the
|
|
79
|
+
* worker script, etc.), unbounded respawn becomes a CPU-pegging loop.
|
|
80
|
+
* The counter increments on each respawn, resets on every successful
|
|
81
|
+
* evaluation, and flips `permanentlyFailed` once the budget is spent.
|
|
82
|
+
*/
|
|
83
|
+
consecutiveRespawns = 0;
|
|
84
|
+
permanentlyFailed = false;
|
|
85
|
+
static MAX_CONSECUTIVE_RESPAWNS = 3;
|
|
86
|
+
constructor(config = {}, onTelemetry) {
|
|
87
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
88
|
+
this.onTelemetry = onTelemetry;
|
|
89
|
+
this.spawnWorker();
|
|
90
|
+
}
|
|
91
|
+
async evaluate(input) {
|
|
92
|
+
if (this.disposed) {
|
|
93
|
+
throw new TotemError('CHECK_FAILED', 'RegexEvaluator has been disposed', 'Construct a new RegexEvaluator before calling evaluate(). The dispose() method releases the worker and marks the instance unusable (mmnto-ai/totem#1641).');
|
|
94
|
+
}
|
|
95
|
+
if (this.permanentlyFailed) {
|
|
96
|
+
throw new TotemError('CHECK_FAILED', `RegexEvaluator exhausted ${RegexEvaluator.MAX_CONSECUTIVE_RESPAWNS} consecutive respawn attempts without a successful evaluation`, 'The regex worker script likely failed to initialize (missing worker.js, syntax error in the worker module, or a Node version that cannot load it). Inspect the worker script at packages/core/src/regex-safety/worker.ts and rebuild @mmnto/totem.');
|
|
97
|
+
}
|
|
98
|
+
// Serialize: the next batch waits for the current one to finish.
|
|
99
|
+
// Single-worker invariant — no batch multiplexing.
|
|
100
|
+
const previous = this.queue;
|
|
101
|
+
let release;
|
|
102
|
+
const gate = new Promise((resolve) => {
|
|
103
|
+
release = resolve;
|
|
104
|
+
});
|
|
105
|
+
this.queue = previous.then(() => gate);
|
|
106
|
+
await previous;
|
|
107
|
+
// Wait for any in-flight respawn so postMessage does not race a
|
|
108
|
+
// null `this.worker` handle (Shield review round-1 race fix).
|
|
109
|
+
if (this.respawnPromise) {
|
|
110
|
+
await this.respawnPromise;
|
|
111
|
+
}
|
|
112
|
+
// Wait for the worker thread to finish spawning before posting.
|
|
113
|
+
// Cold-start (thread spawn + module load) is ~30-50ms and must not
|
|
114
|
+
// count against the batch timeout budget, otherwise a slow CI box
|
|
115
|
+
// trips a spurious timeout on the first batch (CI round-1 fix).
|
|
116
|
+
await this.workerReady;
|
|
117
|
+
try {
|
|
118
|
+
return await this.evaluateOnce(input);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
release();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async dispose() {
|
|
125
|
+
this.disposed = true;
|
|
126
|
+
await this.queue;
|
|
127
|
+
if (this.worker) {
|
|
128
|
+
await this.worker.terminate();
|
|
129
|
+
this.worker = null;
|
|
130
|
+
}
|
|
131
|
+
for (const entry of this.pending.values()) {
|
|
132
|
+
clearTimeout(entry.timer);
|
|
133
|
+
}
|
|
134
|
+
this.pending.clear();
|
|
135
|
+
}
|
|
136
|
+
evaluateOnce(input) {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const id = crypto.randomBytes(8).toString('hex');
|
|
139
|
+
const startedAt = Date.now();
|
|
140
|
+
const inputSize = input.lines.reduce((acc, line) => acc + line.length, 0);
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
const entry = this.pending.get(id);
|
|
143
|
+
if (!entry)
|
|
144
|
+
return;
|
|
145
|
+
this.pending.delete(id);
|
|
146
|
+
const elapsedMs = Date.now() - startedAt;
|
|
147
|
+
this.emitTelemetry({
|
|
148
|
+
ruleHash: entry.ruleHash,
|
|
149
|
+
redactedPath: entry.redactedPath,
|
|
150
|
+
matchedInputSize: entry.inputSize,
|
|
151
|
+
elapsedTimeMs: elapsedMs,
|
|
152
|
+
timeoutTriggered: true,
|
|
153
|
+
softWarningTriggered: false,
|
|
154
|
+
});
|
|
155
|
+
// Terminate worker — the regex is hung on a line we can't interrupt.
|
|
156
|
+
// Await respawn before resolving so the next queued evaluate() does
|
|
157
|
+
// not race a not-yet-spawned worker and get a second timeout.
|
|
158
|
+
void this.respawnWorker().finally(() => {
|
|
159
|
+
entry.resolve({ kind: 'timeout', elapsedMs });
|
|
160
|
+
});
|
|
161
|
+
}, this.config.timeoutMs);
|
|
162
|
+
this.pending.set(id, {
|
|
163
|
+
resolve,
|
|
164
|
+
timer,
|
|
165
|
+
ruleHash: input.ruleHash,
|
|
166
|
+
startedAt,
|
|
167
|
+
inputSize,
|
|
168
|
+
redactedPath: input.redactedPath ?? '<unknown>',
|
|
169
|
+
});
|
|
170
|
+
const request = {
|
|
171
|
+
id,
|
|
172
|
+
pattern: input.pattern,
|
|
173
|
+
flags: input.flags,
|
|
174
|
+
lines: input.lines,
|
|
175
|
+
};
|
|
176
|
+
this.worker?.postMessage(request);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
spawnWorker() {
|
|
180
|
+
const worker = new Worker(resolveWorkerPath());
|
|
181
|
+
this.worker = worker;
|
|
182
|
+
this.workerReady = new Promise((resolve) => {
|
|
183
|
+
worker.once('online', () => resolve());
|
|
184
|
+
});
|
|
185
|
+
worker.on('message', (response) => this.handleMessage(response));
|
|
186
|
+
worker.on('error', () => {
|
|
187
|
+
// Worker crashed outside of a normal message flow (e.g., an
|
|
188
|
+
// internal error). The queued-evaluate lock only releases when
|
|
189
|
+
// the in-flight promise resolves, so we must await respawn before
|
|
190
|
+
// resolving pending entries — otherwise the next evaluate() races
|
|
191
|
+
// a null `this.worker` and postMessage silently drops (spurious
|
|
192
|
+
// timeout on the next batch). Same invariant the timeout path
|
|
193
|
+
// already relies on (see evaluateOnce timer callback above).
|
|
194
|
+
void this.respawnWorker().finally(() => {
|
|
195
|
+
this.rejectAllPendingAsCrash();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
worker.on('exit', (code) => {
|
|
199
|
+
// GCA PR #1644 round-1 — handle unexpected worker exits (OOM kill,
|
|
200
|
+
// internal Node crash) that do not surface through the `error`
|
|
201
|
+
// event. Skip respawn on graceful exit (code 0) and on explicit
|
|
202
|
+
// dispose (terminate() drives exit with non-zero, but we only
|
|
203
|
+
// spin up the respawn after checking the flag). Skip if this
|
|
204
|
+
// handle is no longer the active worker — `respawnWorker` calls
|
|
205
|
+
// `terminate()` on the prior worker before setting a new one, and
|
|
206
|
+
// the exit event for the old handle fires after the field has
|
|
207
|
+
// moved on; respawning again would leak a thread.
|
|
208
|
+
if (this.disposed)
|
|
209
|
+
return;
|
|
210
|
+
if (code === 0)
|
|
211
|
+
return;
|
|
212
|
+
if (this.worker !== worker)
|
|
213
|
+
return;
|
|
214
|
+
void this.respawnWorker().finally(() => {
|
|
215
|
+
this.rejectAllPendingAsCrash();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async respawnWorker() {
|
|
220
|
+
// Coalesce concurrent respawn calls (Shield review round-1). If two
|
|
221
|
+
// events (timeout + error) both request a respawn, they share the
|
|
222
|
+
// same in-flight promise instead of spawning two workers.
|
|
223
|
+
if (this.respawnPromise)
|
|
224
|
+
return this.respawnPromise;
|
|
225
|
+
if (this.disposed)
|
|
226
|
+
return;
|
|
227
|
+
this.respawnPromise = (async () => {
|
|
228
|
+
try {
|
|
229
|
+
this.consecutiveRespawns += 1;
|
|
230
|
+
if (this.consecutiveRespawns > RegexEvaluator.MAX_CONSECUTIVE_RESPAWNS) {
|
|
231
|
+
this.permanentlyFailed = true;
|
|
232
|
+
this.rejectAllPendingAsCrash();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const old = this.worker;
|
|
236
|
+
this.worker = null;
|
|
237
|
+
if (old) {
|
|
238
|
+
try {
|
|
239
|
+
await old.terminate(); // totem-context: intentional best-effort cleanup — terminate() on an already-dead or still-initializing worker can throw, no recovery path at this layer; the new worker spawn is the load-bearing step.
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// No-op (see totem-context on the terminate() call above).
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (this.disposed)
|
|
246
|
+
return;
|
|
247
|
+
this.spawnWorker();
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
this.respawnPromise = null;
|
|
251
|
+
}
|
|
252
|
+
})();
|
|
253
|
+
return this.respawnPromise;
|
|
254
|
+
}
|
|
255
|
+
handleMessage(response) {
|
|
256
|
+
const entry = this.pending.get(response.id);
|
|
257
|
+
if (!entry)
|
|
258
|
+
return;
|
|
259
|
+
this.pending.delete(response.id);
|
|
260
|
+
clearTimeout(entry.timer);
|
|
261
|
+
// Any successful round-trip resets the respawn-failure counter.
|
|
262
|
+
// Persistent spawn failures only matter when they fire back-to-back
|
|
263
|
+
// with no intervening successful evaluation (Shield review round-1).
|
|
264
|
+
this.consecutiveRespawns = 0;
|
|
265
|
+
const elapsedMs = Date.now() - entry.startedAt;
|
|
266
|
+
const softWarningTriggered = elapsedMs >= this.config.softWarningMs;
|
|
267
|
+
this.emitTelemetry({
|
|
268
|
+
ruleHash: entry.ruleHash,
|
|
269
|
+
redactedPath: entry.redactedPath,
|
|
270
|
+
matchedInputSize: entry.inputSize,
|
|
271
|
+
elapsedTimeMs: elapsedMs,
|
|
272
|
+
timeoutTriggered: false,
|
|
273
|
+
softWarningTriggered: response.kind === 'ok' ? softWarningTriggered : false,
|
|
274
|
+
});
|
|
275
|
+
if (response.kind === 'ok') {
|
|
276
|
+
entry.resolve({
|
|
277
|
+
kind: 'ok',
|
|
278
|
+
matchedIndices: response.matchedIndices,
|
|
279
|
+
elapsedMs,
|
|
280
|
+
softWarningTriggered,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
entry.resolve({ kind: 'error', message: response.message, elapsedMs });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
rejectAllPendingAsCrash() {
|
|
288
|
+
for (const [id, entry] of this.pending.entries()) {
|
|
289
|
+
clearTimeout(entry.timer);
|
|
290
|
+
const elapsedMs = Date.now() - entry.startedAt;
|
|
291
|
+
this.emitTelemetry({
|
|
292
|
+
ruleHash: entry.ruleHash,
|
|
293
|
+
redactedPath: entry.redactedPath,
|
|
294
|
+
matchedInputSize: entry.inputSize,
|
|
295
|
+
elapsedTimeMs: elapsedMs,
|
|
296
|
+
timeoutTriggered: true,
|
|
297
|
+
softWarningTriggered: false,
|
|
298
|
+
});
|
|
299
|
+
entry.resolve({ kind: 'timeout', elapsedMs });
|
|
300
|
+
this.pending.delete(id);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
emitTelemetry(record) {
|
|
304
|
+
if (!this.onTelemetry)
|
|
305
|
+
return;
|
|
306
|
+
try {
|
|
307
|
+
this.onTelemetry(record); // totem-context: intentional best-effort telemetry — the evaluator's correctness contract is regex matches, not telemetry delivery; sink failures (bad callback, disk full, permission error) must not interfere with match results.
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// No-op (see totem-context on the onTelemetry call above).
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=evaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.js","sourceRoot":"","sources":["../../src/regex-safety/evaluator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.test.d.ts","sourceRoot":"","sources":["../../src/regex-safety/evaluator.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { RegexEvaluator } from './evaluator.js';
|
|
3
|
+
describe('RegexEvaluator — happy path', () => {
|
|
4
|
+
it('returns matched line indices for a simple pattern', async () => {
|
|
5
|
+
const evaluator = new RegexEvaluator();
|
|
6
|
+
try {
|
|
7
|
+
const result = await evaluator.evaluate({
|
|
8
|
+
ruleHash: 'h1',
|
|
9
|
+
pattern: 'console\\.log',
|
|
10
|
+
flags: '',
|
|
11
|
+
lines: ['console.log("a")', 'logger.info("b")', 'console.log("c")'],
|
|
12
|
+
});
|
|
13
|
+
expect(result.kind).toBe('ok');
|
|
14
|
+
if (result.kind === 'ok') {
|
|
15
|
+
expect(result.matchedIndices).toEqual([0, 2]);
|
|
16
|
+
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await evaluator.dispose();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
it('returns an empty match list when no line matches', async () => {
|
|
24
|
+
const evaluator = new RegexEvaluator();
|
|
25
|
+
try {
|
|
26
|
+
const result = await evaluator.evaluate({
|
|
27
|
+
ruleHash: 'h2',
|
|
28
|
+
pattern: 'xyz\\d+',
|
|
29
|
+
flags: '',
|
|
30
|
+
lines: ['foo', 'bar', 'baz'],
|
|
31
|
+
});
|
|
32
|
+
expect(result.kind).toBe('ok');
|
|
33
|
+
if (result.kind === 'ok') {
|
|
34
|
+
expect(result.matchedIndices).toEqual([]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await evaluator.dispose();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('RegexEvaluator — timeout', () => {
|
|
43
|
+
it('aborts catastrophic backtracking within the configured timeout', async () => {
|
|
44
|
+
const evaluator = new RegexEvaluator({ timeoutMs: 150, softWarningMs: 50 });
|
|
45
|
+
try {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
const result = await evaluator.evaluate({
|
|
48
|
+
ruleHash: 'redos',
|
|
49
|
+
pattern: '(a+)+b',
|
|
50
|
+
flags: '',
|
|
51
|
+
lines: ['a'.repeat(50000) + 'c'],
|
|
52
|
+
});
|
|
53
|
+
const elapsed = Date.now() - start;
|
|
54
|
+
expect(result.kind).toBe('timeout');
|
|
55
|
+
// Main thread must return in roughly the budget; allow generous
|
|
56
|
+
// tolerance for worker respawn and test runner overhead.
|
|
57
|
+
expect(elapsed).toBeLessThan(2000);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await evaluator.dispose();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
it('respawns the worker after a timeout and accepts subsequent evaluations', async () => {
|
|
64
|
+
const evaluator = new RegexEvaluator({ timeoutMs: 150, softWarningMs: 50 });
|
|
65
|
+
try {
|
|
66
|
+
const bad = await evaluator.evaluate({
|
|
67
|
+
ruleHash: 'redos',
|
|
68
|
+
pattern: '(a+)+b',
|
|
69
|
+
flags: '',
|
|
70
|
+
lines: ['a'.repeat(50000) + 'c'],
|
|
71
|
+
});
|
|
72
|
+
expect(bad.kind).toBe('timeout');
|
|
73
|
+
// After respawn, a normal evaluation must succeed.
|
|
74
|
+
const good = await evaluator.evaluate({
|
|
75
|
+
ruleHash: 'after-redos',
|
|
76
|
+
pattern: 'foo',
|
|
77
|
+
flags: '',
|
|
78
|
+
lines: ['foo', 'bar'],
|
|
79
|
+
});
|
|
80
|
+
expect(good.kind).toBe('ok');
|
|
81
|
+
if (good.kind === 'ok') {
|
|
82
|
+
expect(good.matchedIndices).toEqual([0]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await evaluator.dispose();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
it('emits softWarningTriggered for evaluations between softWarningMs and timeoutMs', async () => {
|
|
90
|
+
// Simulate a slow-but-not-pathological pattern by using a moderately
|
|
91
|
+
// backtracking regex with bounded input. Actual wall-clock depends
|
|
92
|
+
// on the host, so we pick a pattern that reliably falls in the
|
|
93
|
+
// soft-warning window and adjust thresholds low enough to trip it.
|
|
94
|
+
const evaluator = new RegexEvaluator({ timeoutMs: 500, softWarningMs: 1 });
|
|
95
|
+
try {
|
|
96
|
+
const result = await evaluator.evaluate({
|
|
97
|
+
ruleHash: 'slow',
|
|
98
|
+
pattern: 'foo',
|
|
99
|
+
flags: '',
|
|
100
|
+
// Many lines guarantee the total evaluation takes > 1ms.
|
|
101
|
+
lines: new Array(1000).fill('foo'),
|
|
102
|
+
});
|
|
103
|
+
expect(result.kind).toBe('ok');
|
|
104
|
+
if (result.kind === 'ok') {
|
|
105
|
+
// With softWarningMs = 1 and 1000 lines, elapsed should exceed 1ms
|
|
106
|
+
// on any reasonable host and softWarning should trip.
|
|
107
|
+
expect(result.softWarningTriggered).toBe(true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await evaluator.dispose();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('RegexEvaluator — error cases', () => {
|
|
116
|
+
it('reports an invalid regex as an error (no worker termination)', async () => {
|
|
117
|
+
const evaluator = new RegexEvaluator();
|
|
118
|
+
try {
|
|
119
|
+
const result = await evaluator.evaluate({
|
|
120
|
+
ruleHash: 'bad-pattern',
|
|
121
|
+
pattern: '(unclosed',
|
|
122
|
+
flags: '',
|
|
123
|
+
lines: ['anything'],
|
|
124
|
+
});
|
|
125
|
+
expect(result.kind).toBe('error');
|
|
126
|
+
if (result.kind === 'error') {
|
|
127
|
+
expect(result.message.toLowerCase()).toMatch(/invalid|syntax|unterminated/);
|
|
128
|
+
}
|
|
129
|
+
// Worker should still be alive for the next evaluation.
|
|
130
|
+
const next = await evaluator.evaluate({
|
|
131
|
+
ruleHash: 'after-bad',
|
|
132
|
+
pattern: 'foo',
|
|
133
|
+
flags: '',
|
|
134
|
+
lines: ['foo'],
|
|
135
|
+
});
|
|
136
|
+
expect(next.kind).toBe('ok');
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await evaluator.dispose();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('RegexEvaluator — worker exit recovery (mmnto-ai/totem#1641 GCA round-1)', () => {
|
|
144
|
+
it('respawns after an unexpected non-zero exit and accepts subsequent evaluations', async () => {
|
|
145
|
+
// Opt into the worker's test-only crash hook; child worker threads
|
|
146
|
+
// inherit the parent process env so the gate fires inside the
|
|
147
|
+
// worker. Restored in the `finally` block after dispose.
|
|
148
|
+
const prior = process.env.TOTEM_TEST_WORKER_CRASH_HOOK;
|
|
149
|
+
process.env.TOTEM_TEST_WORKER_CRASH_HOOK = '1';
|
|
150
|
+
const evaluator = new RegexEvaluator({ timeoutMs: 2000, softWarningMs: 100 });
|
|
151
|
+
try {
|
|
152
|
+
// Fire a crash-signal batch. The worker's test-only hook calls
|
|
153
|
+
// process.exit(1), which surfaces to the evaluator as an `exit`
|
|
154
|
+
// event with a non-zero code (no `error` event fires on OOM /
|
|
155
|
+
// internal crash paths). The exit handler respawns the worker
|
|
156
|
+
// and calls rejectAllPendingAsCrash(), which resolves the batch
|
|
157
|
+
// as a timeout before the main-thread timer fires. The test
|
|
158
|
+
// asserts that a follow-up evaluate() against the fresh worker
|
|
159
|
+
// succeeds — the exit handler is the only path that would have
|
|
160
|
+
// triggered the respawn on an exit-without-error crash.
|
|
161
|
+
const crashBatch = evaluator.evaluate({
|
|
162
|
+
ruleHash: 'crash',
|
|
163
|
+
pattern: '__TOTEM_TEST_CRASH__',
|
|
164
|
+
flags: '',
|
|
165
|
+
lines: ['trigger'],
|
|
166
|
+
});
|
|
167
|
+
await crashBatch;
|
|
168
|
+
// After the exit handler respawns, a normal evaluate must succeed.
|
|
169
|
+
const next = await evaluator.evaluate({
|
|
170
|
+
ruleHash: 'after-crash',
|
|
171
|
+
pattern: 'foo',
|
|
172
|
+
flags: '',
|
|
173
|
+
lines: ['foo', 'bar'],
|
|
174
|
+
});
|
|
175
|
+
expect(next.kind).toBe('ok');
|
|
176
|
+
if (next.kind === 'ok') {
|
|
177
|
+
expect(next.matchedIndices).toEqual([0]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await evaluator.dispose();
|
|
182
|
+
if (prior === undefined) {
|
|
183
|
+
delete process.env.TOTEM_TEST_WORKER_CRASH_HOOK;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
process.env.TOTEM_TEST_WORKER_CRASH_HOOK = prior;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
describe('RegexEvaluator — serialization', () => {
|
|
192
|
+
it('serializes concurrent evaluate() calls onto the single worker', async () => {
|
|
193
|
+
// Two in-flight evaluations must queue, not overlap. The second
|
|
194
|
+
// evaluation does not start until the first resolves. This protects
|
|
195
|
+
// the single-worker invariant (no multiplexing of batches onto one
|
|
196
|
+
// worker message round-trip).
|
|
197
|
+
const evaluator = new RegexEvaluator();
|
|
198
|
+
try {
|
|
199
|
+
const a = evaluator.evaluate({
|
|
200
|
+
ruleHash: 'concurrent-a',
|
|
201
|
+
pattern: 'foo',
|
|
202
|
+
flags: '',
|
|
203
|
+
lines: ['foo', 'bar'],
|
|
204
|
+
});
|
|
205
|
+
const b = evaluator.evaluate({
|
|
206
|
+
ruleHash: 'concurrent-b',
|
|
207
|
+
pattern: 'bar',
|
|
208
|
+
flags: '',
|
|
209
|
+
lines: ['foo', 'bar'],
|
|
210
|
+
});
|
|
211
|
+
const [resA, resB] = await Promise.all([a, b]);
|
|
212
|
+
expect(resA.kind).toBe('ok');
|
|
213
|
+
expect(resB.kind).toBe('ok');
|
|
214
|
+
if (resA.kind === 'ok')
|
|
215
|
+
expect(resA.matchedIndices).toEqual([0]);
|
|
216
|
+
if (resB.kind === 'ok')
|
|
217
|
+
expect(resB.matchedIndices).toEqual([1]);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
await evaluator.dispose();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
//# sourceMappingURL=evaluator.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.test.js","sourceRoot":"","sources":["../../src/regex-safety/evaluator.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* One record per rule-batch evaluation emitted by `RegexEvaluator` to the
|
|
4
|
+
* Totem telemetry sink (mmnto-ai/totem#1641). Crosses the disk /
|
|
5
|
+
* observability boundary, so validated with Zod rather than a plain TS
|
|
6
|
+
* interface: malformed telemetry should fail loud at write rather than
|
|
7
|
+
* silently polluting the sink that downstream tooling will parse.
|
|
8
|
+
*
|
|
9
|
+
* Path-redaction discipline lives in `redactPath` below; raw absolute
|
|
10
|
+
* paths only appear when the operator opts in via `--telemetry-full-paths`
|
|
11
|
+
* (the evaluator passes the redacted or raw path to this schema; the
|
|
12
|
+
* schema itself does not enforce which variant is recorded).
|
|
13
|
+
*/
|
|
14
|
+
export declare const RegexTelemetrySchema: z.ZodObject<{
|
|
15
|
+
ruleHash: z.ZodString;
|
|
16
|
+
redactedPath: z.ZodString;
|
|
17
|
+
matchedInputSize: z.ZodNumber;
|
|
18
|
+
elapsedTimeMs: z.ZodNumber;
|
|
19
|
+
timeoutTriggered: z.ZodBoolean;
|
|
20
|
+
softWarningTriggered: z.ZodBoolean;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
ruleHash: string;
|
|
23
|
+
redactedPath: string;
|
|
24
|
+
matchedInputSize: number;
|
|
25
|
+
elapsedTimeMs: number;
|
|
26
|
+
timeoutTriggered: boolean;
|
|
27
|
+
softWarningTriggered: boolean;
|
|
28
|
+
}, {
|
|
29
|
+
ruleHash: string;
|
|
30
|
+
redactedPath: string;
|
|
31
|
+
matchedInputSize: number;
|
|
32
|
+
elapsedTimeMs: number;
|
|
33
|
+
timeoutTriggered: boolean;
|
|
34
|
+
softWarningTriggered: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
export type RegexTelemetry = z.infer<typeof RegexTelemetrySchema>;
|
|
37
|
+
/**
|
|
38
|
+
* Normalize a file path into a redaction-safe form for telemetry.
|
|
39
|
+
*
|
|
40
|
+
* Paths inside the repo root are returned as repo-relative; paths outside
|
|
41
|
+
* the repo root collapse to `<extern:<sha256-12>>` so `/tmp/foo`,
|
|
42
|
+
* `C:\Users\alice\secret.ts`, or a sibling-repo path cannot leak into the
|
|
43
|
+
* telemetry sink unintentionally. The extern hash is stable so deduping
|
|
44
|
+
* and pattern-spotting still work across runs.
|
|
45
|
+
*
|
|
46
|
+
* Callers that want raw absolute paths must opt in explicitly at the
|
|
47
|
+
* evaluator layer (e.g., `--telemetry-full-paths`) and bypass this helper.
|
|
48
|
+
*/
|
|
49
|
+
export declare function redactPath(absOrRelPath: string, repoRoot: string): string;
|
|
50
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/regex-safety/telemetry.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;EAO/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiBzE"}
|