@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.
Files changed (52) hide show
  1. package/dist/compile-lesson.d.ts.map +1 -1
  2. package/dist/compile-lesson.js +59 -4
  3. package/dist/compile-lesson.js.map +1 -1
  4. package/dist/compile-lesson.test.js +174 -0
  5. package/dist/compile-lesson.test.js.map +1 -1
  6. package/dist/compiler-schema.d.ts +58 -16
  7. package/dist/compiler-schema.d.ts.map +1 -1
  8. package/dist/compiler-schema.js +79 -0
  9. package/dist/compiler-schema.js.map +1 -1
  10. package/dist/compiler-schema.test.js +160 -1
  11. package/dist/compiler-schema.test.js.map +1 -1
  12. package/dist/compiler.d.ts +1 -1
  13. package/dist/compiler.d.ts.map +1 -1
  14. package/dist/compiler.js +1 -1
  15. package/dist/compiler.js.map +1 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/regex-safety/apply-rules-bounded.d.ts +35 -0
  21. package/dist/regex-safety/apply-rules-bounded.d.ts.map +1 -0
  22. package/dist/regex-safety/apply-rules-bounded.js +114 -0
  23. package/dist/regex-safety/apply-rules-bounded.js.map +1 -0
  24. package/dist/regex-safety/apply-rules-bounded.test.d.ts +2 -0
  25. package/dist/regex-safety/apply-rules-bounded.test.d.ts.map +1 -0
  26. package/dist/regex-safety/apply-rules-bounded.test.js +136 -0
  27. package/dist/regex-safety/apply-rules-bounded.test.js.map +1 -0
  28. package/dist/regex-safety/evaluator.d.ts +95 -0
  29. package/dist/regex-safety/evaluator.d.ts.map +1 -0
  30. package/dist/regex-safety/evaluator.js +314 -0
  31. package/dist/regex-safety/evaluator.js.map +1 -0
  32. package/dist/regex-safety/evaluator.test.d.ts +2 -0
  33. package/dist/regex-safety/evaluator.test.d.ts.map +1 -0
  34. package/dist/regex-safety/evaluator.test.js +224 -0
  35. package/dist/regex-safety/evaluator.test.js.map +1 -0
  36. package/dist/regex-safety/telemetry.d.ts +50 -0
  37. package/dist/regex-safety/telemetry.d.ts.map +1 -0
  38. package/dist/regex-safety/telemetry.js +50 -0
  39. package/dist/regex-safety/telemetry.js.map +1 -0
  40. package/dist/regex-safety/telemetry.test.d.ts +2 -0
  41. package/dist/regex-safety/telemetry.test.d.ts.map +1 -0
  42. package/dist/regex-safety/telemetry.test.js +82 -0
  43. package/dist/regex-safety/telemetry.test.js.map +1 -0
  44. package/dist/regex-safety/worker.d.ts +31 -0
  45. package/dist/regex-safety/worker.d.ts.map +1 -0
  46. package/dist/regex-safety/worker.js +51 -0
  47. package/dist/regex-safety/worker.js.map +1 -0
  48. package/dist/rule-engine.d.ts +1 -0
  49. package/dist/rule-engine.d.ts.map +1 -1
  50. package/dist/rule-engine.js +1 -1
  51. package/dist/rule-engine.js.map +1 -1
  52. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=evaluator.test.d.ts.map
@@ -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"}