@mmnto/totem 1.15.2 → 1.15.4
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 +35 -0
- package/dist/compile-lesson.d.ts.map +1 -1
- package/dist/compile-lesson.js +80 -39
- package/dist/compile-lesson.js.map +1 -1
- package/dist/compile-lesson.test.js +229 -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 +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/lesson-pattern.d.ts +20 -0
- package/dist/lesson-pattern.d.ts.map +1 -1
- package/dist/lesson-pattern.js +38 -0
- package/dist/lesson-pattern.js.map +1 -1
- package/dist/lesson-pattern.test.js +94 -1
- package/dist/lesson-pattern.test.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,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"}
|
|
@@ -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":""}
|