@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.
Files changed (52) hide show
  1. package/dist/compile-lesson.d.ts.map +1 -1
  2. package/dist/compile-lesson.js +25 -4
  3. package/dist/compile-lesson.js.map +1 -1
  4. package/dist/compile-lesson.test.js +120 -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,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":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,WAAW,GAEZ,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAsB5C,SAAS,gBAAgB,CAAC,QAAgB,EAAE,KAAwB;IAClE,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,IAAI,OAAO,GAAG,CAAC,WAAW,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,IAAI,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;QACzD,CAAC;aAAM,IAAI,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;YACvC,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,GAAsB,EACtB,KAA8B,EAC9B,SAAkC,EAClC,OAA4B,EAC5B,WAA+B;IAE/B,MAAM,UAAU,GAAgB,EAAE,CAAC;IACnC,MAAM,eAAe,GAAyB,EAAE,CAAC;IAEjD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;IACzC,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAE1E,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,sEAAsE;QACtE,qEAAqE;QACrE,8CAA8C;QAC9C,MAAM,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;QACjD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBAAE,SAAS;YACjE,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC/C,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC;QAED,KAAK,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,MAAM,EAAE,CAAC;YAC3C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC;gBAC9C,QAAQ,EAAE,IAAI,CAAC,UAAU;gBACzB,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,KAAK,EAAE,EAAE;gBACT,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBACvC,YAAY,EAAE,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC;aACjD,CAAC,CAAC;YAEH,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC5B,+DAA+D;gBAC/D,8DAA8D;gBAC9D,iEAAiE;gBACjE,+DAA+D;gBAC/D,QAAQ;gBACR,MAAM,IAAI,eAAe,CACvB,QAAQ,IAAI,CAAC,UAAU,wDAAwD,EAC/E,6JAA6J,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,uBAAuB,MAAM,CAAC,OAAO,EAAE,CACjO,CAAC;YACJ,CAAC;YAED,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC9B,eAAe,CAAC,IAAI,CAAC;oBACnB,QAAQ,EAAE,IAAI,CAAC,UAAU;oBACzB,IAAI;oBACJ,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,IAAI,EAAE,OAAO,CAAC,WAAW;iBAC1B,CAAC,CAAC;gBACH,WAAW,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE;oBACxC,IAAI;oBACJ,IAAI,EAAE,CAAC;oBACP,aAAa,EAAE,iBAAiB,MAAM,CAAC,SAAS,aAAa,OAAO,CAAC,WAAW,GAAG;iBACpF,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,KAAK,MAAM,YAAY,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;gBACjD,MAAM,QAAQ,GAAG,aAAa,CAAC,YAAY,CAAC,CAAC;gBAC7C,IAAI,CAAC,QAAQ;oBAAE,SAAS;gBAExB,IAAI,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;oBAC7D,WAAW,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;wBACzC,IAAI,EAAE,QAAQ,CAAC,IAAI;wBACnB,IAAI,EAAE,QAAQ,CAAC,UAAU;wBACzB,aAAa,EAAE,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,aAAa,CAAC;wBAC/E,SAAS,EAAE,IAAI,CAAC,SAAS;qBAC1B,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;gBAED,WAAW,EAAE,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE;oBACxC,IAAI,EAAE,QAAQ,CAAC,IAAI;oBACnB,IAAI,EAAE,QAAQ,CAAC,UAAU;oBACzB,UAAU,EAAE,QAAQ,CAAC,UAAU;iBAChC,CAAC,CAAC;gBAEH,IAAI,CAAC,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC;oBAC3D,UAAU,CAAC,IAAI,CAAC;wBACd,IAAI;wBACJ,IAAI,EAAE,QAAQ,CAAC,IAAI;wBACnB,IAAI,EAAE,QAAQ,CAAC,IAAI;wBACnB,UAAU,EAAE,QAAQ,CAAC,UAAU;qBAChC,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC;AACzC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=apply-rules-bounded.test.d.ts.map
@@ -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"}