@gobing-ai/ts-rule-engine 0.4.28 → 0.4.30

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.
@@ -2,14 +2,22 @@ import type { Logger } from '@gobing-ai/ts-infra';
2
2
  import type { RuleEngineHost } from '../host/rule-engine-host';
3
3
  /** A capability kind a preset extension can contribute. */
4
4
  export type ExtensionKind = 'resolvers' | 'evaluators' | 'fixers' | 'formatters';
5
- /** A single extension module reference, resolved to an absolute path. */
5
+ /**
6
+ * A single extension module reference: the authored relative path plus the
7
+ * declaring directory (task 0060 C2). Keeping the authored string means the
8
+ * shared loader's `assertRelativeExtensionPath` sees the real declaration — a
9
+ * `..` segment or absolute path is rejected there, not after a basename smash
10
+ * has already stripped it.
11
+ */
6
12
  export interface ExtensionRef {
7
13
  /** Capability registry the module registers into. */
8
14
  readonly kind: ExtensionKind;
9
- /** Absolute path to the module to import. */
10
- readonly absPath: string;
15
+ /** Relative path as authored (e.g. `./exts/foo.ts`). */
16
+ readonly path: string;
17
+ /** Absolute directory the authored `path` is resolved against. */
18
+ readonly baseDir: string;
11
19
  /** Name of the preset that declared this extension (for diagnostics). */
12
- readonly presetName: string;
20
+ readonly sourceName: string;
13
21
  }
14
22
  /** Options controlling preset-extension loading. */
15
23
  export interface LoadExtensionsOptions {
@@ -37,9 +45,11 @@ export interface LoadExtensionsOptions {
37
45
  /**
38
46
  * Collect extension refs declared by a preset's or rule file's `extensions` block.
39
47
  *
40
- * Paths are resolved relative to the declaring file's directory. Use the returned
41
- * refs with {@link loadExtensionsIntoHost}. Rule files and presets are treated
42
- * identically — both flow through the same trust gate at load time.
48
+ * Paths are kept **as authored** and paired with the declaring file's directory as
49
+ * `baseDir` — no eager resolution, so the shared loader's traversal guard sees the
50
+ * real declaration (task 0060 C2). Use the returned refs with
51
+ * {@link loadExtensionsIntoHost}. Rule files and presets are treated identically —
52
+ * both flow through the same trust gate at load time.
43
53
  */
44
54
  export declare function collectExtensions(sourceName: string, sourceDir: string, extensions: Partial<Record<ExtensionKind, string[] | undefined>> | undefined): ExtensionRef[];
45
55
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/config/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAclD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEjF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IACzB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrE;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1C;AAUD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,GAC7E,YAAY,EAAE,CAShB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sBAAsB,CACxC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GACpC,OAAO,CAAC,IAAI,CAAC,CAiEf"}
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/config/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAOlD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEjF;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IACzB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrE;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1C;AAUD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,GAC7E,YAAY,EAAE,CAShB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sBAAsB,CACxC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GACpC,OAAO,CAAC,IAAI,CAAC,CA2Cf"}
@@ -1,4 +1,4 @@
1
- import { basenamePath, createNodeFileSystem, dirnamePath, fileUrlToPath, normalizeSeparators, resolvePath, } from '@gobing-ai/ts-runtime';
1
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
2
2
  import { loadExtensionModules } from '@gobing-ai/ts-runtime/extension';
3
3
  /** Host registries that can receive extension capabilities (all four kinds). */
4
4
  const HOST_REGISTRY_BY_KIND = {
@@ -10,9 +10,11 @@ const HOST_REGISTRY_BY_KIND = {
10
10
  /**
11
11
  * Collect extension refs declared by a preset's or rule file's `extensions` block.
12
12
  *
13
- * Paths are resolved relative to the declaring file's directory. Use the returned
14
- * refs with {@link loadExtensionsIntoHost}. Rule files and presets are treated
15
- * identically — both flow through the same trust gate at load time.
13
+ * Paths are kept **as authored** and paired with the declaring file's directory as
14
+ * `baseDir` — no eager resolution, so the shared loader's traversal guard sees the
15
+ * real declaration (task 0060 C2). Use the returned refs with
16
+ * {@link loadExtensionsIntoHost}. Rule files and presets are treated identically —
17
+ * both flow through the same trust gate at load time.
16
18
  */
17
19
  export function collectExtensions(sourceName, sourceDir, extensions) {
18
20
  if (extensions === undefined)
@@ -20,7 +22,7 @@ export function collectExtensions(sourceName, sourceDir, extensions) {
20
22
  const refs = [];
21
23
  for (const kind of ['resolvers', 'evaluators', 'fixers', 'formatters']) {
22
24
  for (const path of extensions[kind] ?? []) {
23
- refs.push({ kind, presetName: sourceName, absPath: resolvePath(sourceDir, path) });
25
+ refs.push({ kind, sourceName, path, baseDir: sourceDir });
24
26
  }
25
27
  }
26
28
  return refs;
@@ -44,33 +46,15 @@ export function collectExtensions(sourceName, sourceDir, extensions) {
44
46
  export async function loadExtensionsIntoHost(host, refs, options = {}) {
45
47
  if (refs.length === 0)
46
48
  return;
47
- // Normalize file:// URLs to file paths so dirname/basename produce valid
48
- // path components that isAbsolute() accepts (e.g. import.meta.url in tests).
49
- const toFilePath = (p) => (p.startsWith('file://') ? fileUrlToPath(p) : p);
50
- // Defense-in-depth: pre-validate `..` traversal in caller-supplied absPath
51
- // before the basename adaptation strips it. The shared loader's
52
- // assertRelativeExtensionPath also runs on the derived (basename) path.
53
- for (const ref of refs) {
54
- // Split on normalized separators: fileUrlToPath returns forward slashes on
55
- // every platform, so a platform SEP split would miss segments on Windows.
56
- const segments = normalizeSeparators(toFilePath(ref.absPath)).split('/');
57
- if (segments.includes('..')) {
58
- throw new Error(`extension path "${ref.absPath}" declared by "${ref.presetName}" must not contain ".." traversal`);
59
- }
60
- }
61
- // Adapt rule-engine ExtensionRef → shared ExtensionRef so the generic loader
62
- // governs every import. The shared loader resolves (baseDir, path) →
63
- // absPath internally; we supply dirname/basename so the resolved path
64
- // reconstructs the caller's original absPath.
65
- const sharedRefs = refs.map((ref) => {
66
- const fp = toFilePath(ref.absPath);
67
- return {
68
- kind: ref.kind,
69
- path: `./${basenamePath(fp)}`,
70
- baseDir: dirnamePath(fp),
71
- sourceName: ref.presetName,
72
- };
73
- });
49
+ // Map 1:1 onto the shared ref shape — no dirname/basename smash, so the shared
50
+ // loader's assertRelativeExtensionPath + realPath confinement govern the authored
51
+ // path exactly as declared (task 0060 C2).
52
+ const sharedRefs = refs.map((ref) => ({
53
+ kind: ref.kind,
54
+ path: ref.path,
55
+ baseDir: ref.baseDir,
56
+ sourceName: ref.sourceName,
57
+ }));
74
58
  const moduleLoader = options.moduleLoader ?? defaultModuleLoader;
75
59
  // Real import policy gets real confinement by default (ADR-022 addendum): a
76
60
  // caller relying on the default dynamic-import loader that has not set
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAgB,QAAQ,EAAyB,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAwB,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAIH,KAAK,oBAAoB,EAE5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAqB,cAAc,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGhH,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAC9B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACpC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAC5C,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,4EAA4E;AAC5E,qBAAa,UAAU;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAE9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;gBAE5B,OAAO,GAAE,iBAAsB;IAc3C,wCAAwC;IACxC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAI/D;;;;;;;OAOG;IACG,QAAQ,CACV,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAI5B;;;;;;;;;;;;;;;;;;OAkBG;IACG,iBAAiB,CACnB,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,OAAgB,EAC5B,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAmK5B;;;;;;;OAOG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,EAAE,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAG1G"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAgB,QAAQ,EAAyB,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAwB,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAIH,KAAK,oBAAoB,EAE5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,KAAK,EAAqB,cAAc,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGhH,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAC9B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,mCAAmC;IACnC,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAC;IACpC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC;IAC5C,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,4EAA4E;AAC5E,qBAAa,UAAU;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAE9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqC;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;gBAE5B,OAAO,GAAE,iBAAsB;IAc3C,wCAAwC;IACxC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAI/D;;;;;;;OAOG;IACG,QAAQ,CACV,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAI5B;;;;;;;;;;;;;;;;;;OAkBG;IACG,iBAAiB,CACnB,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,OAAgB,EAC5B,WAAW,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IAkL5B;;;;;;;OAOG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,EAAE,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAG1G"}
package/dist/engine.js CHANGED
@@ -81,7 +81,11 @@ export class RuleEngine {
81
81
  return await traceAsync('rule.run', async () => {
82
82
  this.logger.info('rule run started', { enabled: enabledRules.length, total: rules.length });
83
83
  addSpanEvent('rule.run.start', { rules: enabledRules.length, total: rules.length });
84
- void this.events?.emit('rule.run.start', { rules: enabledRules.length, total: rules.length });
84
+ void this.events?.emit('rule.run.start', {
85
+ rules: enabledRules.length,
86
+ total: rules.length,
87
+ severity: 'info',
88
+ });
85
89
  const findings = [];
86
90
  const fixes = [];
87
91
  let index = 0;
@@ -101,7 +105,12 @@ export class RuleEngine {
101
105
  const evalStartMs = Date.now();
102
106
  this.logger.debug('eval start', { ruleId: rule.id, index, total: enabledRules.length });
103
107
  addSpanEvent('rule.eval.start', { ruleId: rule.id, index, total: enabledRules.length });
104
- void this.events?.emit('rule.eval.start', { ruleId: rule.id, index, total: enabledRules.length });
108
+ void this.events?.emit('rule.eval.start', {
109
+ ruleId: rule.id,
110
+ index,
111
+ total: enabledRules.length,
112
+ severity: 'info',
113
+ });
105
114
  let ruleFindings = [];
106
115
  let ruleEvalFixes = [];
107
116
  let evalError;
@@ -121,7 +130,11 @@ export class RuleEngine {
121
130
  }),
122
131
  ];
123
132
  addSpanEvent('rule.eval.error', { ruleId: rule.id, error: evalError });
124
- void this.events?.emit('rule.eval.error', { ruleId: rule.id, error: evalError });
133
+ void this.events?.emit('rule.eval.error', {
134
+ ruleId: rule.id,
135
+ error: evalError,
136
+ severity: 'error',
137
+ });
125
138
  }
126
139
  const durationMs = Date.now() - evalStartMs;
127
140
  addSpanEvent('rule.eval.done', {
@@ -134,6 +147,7 @@ export class RuleEngine {
134
147
  findings: ruleFindings.length,
135
148
  durationMs,
136
149
  details: ruleFindings,
150
+ severity: 'info',
137
151
  });
138
152
  // Persist eval completion.
139
153
  await this.persistence?.updateEvalRun({
@@ -190,6 +204,7 @@ export class RuleEngine {
190
204
  findings: findings.length,
191
205
  durationMs: runDurationMs,
192
206
  stoppedEarly: stoppedEarlyLocal,
207
+ severity: 'info',
193
208
  });
194
209
  // Finalize the run row.
195
210
  await this.persistence?.updateRunStatus(this.runId, 'done', findings.length, fixes.length, 0, // appliedFixCount — set by the caller after applying fixes
@@ -1 +1 @@
1
- {"version":3,"file":"file-discovery.d.ts","sourceRoot":"","sources":["../../src/evaluators/file-discovery.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGH,KAAK,UAAU,EAIlB,MAAM,uBAAuB,CAAC;AAG/B,yCAAyC;AACzC,MAAM,WAAW,sBAAsB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,EAAE,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,aAAgF,CAAC;AAE9G,qFAAqF;AACrF,wBAAsB,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWtF;AAED,gDAAgD;AAChD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,CAErH;AAED,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IACxB,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B;AAED,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7C,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC7B,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;OAQG;IACH,SAAS,EAAE,aAAa,CAAC;IACzB,0BAA0B;IAC1B,EAAE,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAWjF;AAeD,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD"}
1
+ {"version":3,"file":"file-discovery.d.ts","sourceRoot":"","sources":["../../src/evaluators/file-discovery.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGH,KAAK,UAAU,EAIlB,MAAM,uBAAuB,CAAC;AAG/B,yCAAyC;AACzC,MAAM,WAAW,sBAAsB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,EAAE,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,aAAgF,CAAC;AAE9G,qFAAqF;AACrF,wBAAsB,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWtF;AAED,gDAAgD;AAChD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,CAErH;AAED,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IACxB,6BAA6B;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B;AASD,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7C,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC7B,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;OAQG;IACH,SAAS,EAAE,aAAa,CAAC;IACzB,0BAA0B;IAC1B,EAAE,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAcjF;AAeD,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD"}
@@ -25,6 +25,12 @@ export async function discoverFiles(options) {
25
25
  export async function readWorkdirFile(workdir, filePath, fs = createNodeFileSystem()) {
26
26
  return await fs.readFile(resolvePath(workdir, filePath));
27
27
  }
28
+ /**
29
+ * Per-file byte cap for {@link scanFiles} (task 0060 R15). Larger files are skipped
30
+ * with no content buffer so a single oversized artifact cannot balloon memory while
31
+ * a rule corpus is assembled; evaluators still see every smaller file unchanged.
32
+ */
33
+ const MAX_SCANNED_FILE_BYTES = 2_000_000;
28
34
  /**
29
35
  * Discover in-scope files and read each once — the shared scaffolding behind the
30
36
  * line-scanning evaluators. Owns discovery, scope filtering (per `matchMode`), and
@@ -41,6 +47,10 @@ export async function scanFiles(options) {
41
47
  : await discoverFilesByGlob(options.workdir, options.include, options.exclude, fs);
42
48
  const scanned = [];
43
49
  for (const file of files) {
50
+ // Skip oversized files instead of buffering them (task 0060 R15).
51
+ const stat = await fs.stat(resolvePath(options.workdir, file));
52
+ if (stat !== null && stat.size > MAX_SCANNED_FILE_BYTES)
53
+ continue;
44
54
  scanned.push({ file, content: await readWorkdirFile(options.workdir, file, fs) });
45
55
  }
46
56
  return scanned;
package/dist/events.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { EventSeverity } from '@gobing-ai/ts-infra';
1
2
  import type { ConstraintFinding } from './types';
2
3
  /** Typed event map for rule-engine run observability. All events prefixed `rule.`. */
3
4
  export type RuleEngineEvents = {
@@ -5,12 +6,14 @@ export type RuleEngineEvents = {
5
6
  'rule.run.start': (data: {
6
7
  rules: number;
7
8
  total: number;
9
+ severity: EventSeverity;
8
10
  }) => void;
9
11
  /** Emitted immediately before a single rule's evaluator is invoked. */
10
12
  'rule.eval.start': (data: {
11
13
  ruleId: string;
12
14
  index: number;
13
15
  total: number;
16
+ severity: EventSeverity;
14
17
  }) => void;
15
18
  /**
16
19
  * Emitted after a single rule evaluation finishes successfully.
@@ -23,11 +26,13 @@ export type RuleEngineEvents = {
23
26
  findings: number;
24
27
  durationMs: number;
25
28
  details: ConstraintFinding[];
29
+ severity: EventSeverity;
26
30
  }) => void;
27
31
  /** Emitted when a rule evaluator throws (in addition to the `kind:'error'` finding). */
28
32
  'rule.eval.error': (data: {
29
33
  ruleId: string;
30
34
  error: string;
35
+ severity: EventSeverity;
31
36
  }) => void;
32
37
  /** Emitted after the last rule finishes (or was short-circuited). */
33
38
  'rule.run.done': (data: {
@@ -35,6 +40,7 @@ export type RuleEngineEvents = {
35
40
  findings: number;
36
41
  durationMs: number;
37
42
  stoppedEarly: boolean;
43
+ severity: EventSeverity;
38
44
  }) => void;
39
45
  };
40
46
  //# sourceMappingURL=events.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,GAAG;IAC3B,kDAAkD;IAClD,gBAAgB,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnE,uEAAuE;IACvE,iBAAiB,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACpF;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,iBAAiB,EAAE,CAAC;KAChC,KAAK,IAAI,CAAC;IACX,wFAAwF;IACxF,iBAAiB,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,qEAAqE;IACrE,eAAe,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACnH,CAAC"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,GAAG;IAC3B,kDAAkD;IAClD,gBAAgB,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,KAAK,IAAI,CAAC;IAC5F,uEAAuE;IACvE,iBAAiB,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7G;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,iBAAiB,EAAE,CAAC;QAC7B,QAAQ,EAAE,aAAa,CAAC;KAC3B,KAAK,IAAI,CAAC;IACX,wFAAwF;IACxF,iBAAiB,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,KAAK,IAAI,CAAC;IAC9F,qEAAqE;IACrE,eAAe,EAAE,CAAC,IAAI,EAAE;QACpB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,OAAO,CAAC;QACtB,QAAQ,EAAE,aAAa,CAAC;KAC3B,KAAK,IAAI,CAAC;CACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-rule-engine",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
4
4
  "description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -49,10 +49,10 @@
49
49
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-rule-engine-v<version> && git push --tags' && exit 1"
50
50
  },
51
51
  "dependencies": {
52
- "@gobing-ai/ts-ai-runner": "^0.4.28",
53
- "@gobing-ai/ts-infra": "^0.4.28",
54
- "@gobing-ai/ts-runtime": "^0.4.28",
55
- "@gobing-ai/ts-db": "^0.4.28",
52
+ "@gobing-ai/ts-ai-runner": "^0.4.30",
53
+ "@gobing-ai/ts-infra": "^0.4.30",
54
+ "@gobing-ai/ts-runtime": "^0.4.30",
55
+ "@gobing-ai/ts-db": "^0.4.30",
56
56
  "yaml": "^2.7.0",
57
57
  "zod": "^4.1.0"
58
58
  },
@@ -1,12 +1,5 @@
1
1
  import type { Logger } from '@gobing-ai/ts-infra';
2
- import {
3
- basenamePath,
4
- createNodeFileSystem,
5
- dirnamePath,
6
- fileUrlToPath,
7
- normalizeSeparators,
8
- resolvePath,
9
- } from '@gobing-ai/ts-runtime';
2
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
10
3
  import type {
11
4
  ExtensionRef as SharedExtensionRef,
12
5
  LoadExtensionsOptions as SharedLoadExtensionsOptions,
@@ -16,14 +9,22 @@ import type { RuleEngineHost } from '../host/rule-engine-host';
16
9
  /** A capability kind a preset extension can contribute. */
17
10
  export type ExtensionKind = 'resolvers' | 'evaluators' | 'fixers' | 'formatters';
18
11
 
19
- /** A single extension module reference, resolved to an absolute path. */
12
+ /**
13
+ * A single extension module reference: the authored relative path plus the
14
+ * declaring directory (task 0060 C2). Keeping the authored string means the
15
+ * shared loader's `assertRelativeExtensionPath` sees the real declaration — a
16
+ * `..` segment or absolute path is rejected there, not after a basename smash
17
+ * has already stripped it.
18
+ */
20
19
  export interface ExtensionRef {
21
20
  /** Capability registry the module registers into. */
22
21
  readonly kind: ExtensionKind;
23
- /** Absolute path to the module to import. */
24
- readonly absPath: string;
22
+ /** Relative path as authored (e.g. `./exts/foo.ts`). */
23
+ readonly path: string;
24
+ /** Absolute directory the authored `path` is resolved against. */
25
+ readonly baseDir: string;
25
26
  /** Name of the preset that declared this extension (for diagnostics). */
26
- readonly presetName: string;
27
+ readonly sourceName: string;
27
28
  }
28
29
 
29
30
  /** Options controlling preset-extension loading. */
@@ -61,9 +62,11 @@ const HOST_REGISTRY_BY_KIND: Record<ExtensionKind, 'resolvers' | 'evaluators' |
61
62
  /**
62
63
  * Collect extension refs declared by a preset's or rule file's `extensions` block.
63
64
  *
64
- * Paths are resolved relative to the declaring file's directory. Use the returned
65
- * refs with {@link loadExtensionsIntoHost}. Rule files and presets are treated
66
- * identically — both flow through the same trust gate at load time.
65
+ * Paths are kept **as authored** and paired with the declaring file's directory as
66
+ * `baseDir` — no eager resolution, so the shared loader's traversal guard sees the
67
+ * real declaration (task 0060 C2). Use the returned refs with
68
+ * {@link loadExtensionsIntoHost}. Rule files and presets are treated identically —
69
+ * both flow through the same trust gate at load time.
67
70
  */
68
71
  export function collectExtensions(
69
72
  sourceName: string,
@@ -74,7 +77,7 @@ export function collectExtensions(
74
77
  const refs: ExtensionRef[] = [];
75
78
  for (const kind of ['resolvers', 'evaluators', 'fixers', 'formatters'] as ExtensionKind[]) {
76
79
  for (const path of extensions[kind] ?? []) {
77
- refs.push({ kind, presetName: sourceName, absPath: resolvePath(sourceDir, path) });
80
+ refs.push({ kind, sourceName, path, baseDir: sourceDir });
78
81
  }
79
82
  }
80
83
  return refs;
@@ -103,37 +106,15 @@ export async function loadExtensionsIntoHost(
103
106
  ): Promise<void> {
104
107
  if (refs.length === 0) return;
105
108
 
106
- // Normalize file:// URLs to file paths so dirname/basename produce valid
107
- // path components that isAbsolute() accepts (e.g. import.meta.url in tests).
108
- const toFilePath = (p: string): string => (p.startsWith('file://') ? fileUrlToPath(p) : p);
109
-
110
- // Defense-in-depth: pre-validate `..` traversal in caller-supplied absPath
111
- // before the basename adaptation strips it. The shared loader's
112
- // assertRelativeExtensionPath also runs on the derived (basename) path.
113
- for (const ref of refs) {
114
- // Split on normalized separators: fileUrlToPath returns forward slashes on
115
- // every platform, so a platform SEP split would miss segments on Windows.
116
- const segments = normalizeSeparators(toFilePath(ref.absPath)).split('/');
117
- if (segments.includes('..')) {
118
- throw new Error(
119
- `extension path "${ref.absPath}" declared by "${ref.presetName}" must not contain ".." traversal`,
120
- );
121
- }
122
- }
123
-
124
- // Adapt rule-engine ExtensionRef → shared ExtensionRef so the generic loader
125
- // governs every import. The shared loader resolves (baseDir, path) →
126
- // absPath internally; we supply dirname/basename so the resolved path
127
- // reconstructs the caller's original absPath.
128
- const sharedRefs: SharedExtensionRef<ExtensionKind>[] = refs.map((ref) => {
129
- const fp = toFilePath(ref.absPath);
130
- return {
131
- kind: ref.kind,
132
- path: `./${basenamePath(fp)}`,
133
- baseDir: dirnamePath(fp),
134
- sourceName: ref.presetName,
135
- };
136
- });
109
+ // Map 1:1 onto the shared ref shape — no dirname/basename smash, so the shared
110
+ // loader's assertRelativeExtensionPath + realPath confinement govern the authored
111
+ // path exactly as declared (task 0060 C2).
112
+ const sharedRefs: SharedExtensionRef<ExtensionKind>[] = refs.map((ref) => ({
113
+ kind: ref.kind,
114
+ path: ref.path,
115
+ baseDir: ref.baseDir,
116
+ sourceName: ref.sourceName,
117
+ }));
137
118
 
138
119
  const moduleLoader = options.moduleLoader ?? defaultModuleLoader;
139
120
  // Real import policy gets real confinement by default (ADR-022 addendum): a
package/src/engine.ts CHANGED
@@ -141,7 +141,11 @@ export class RuleEngine {
141
141
  async () => {
142
142
  this.logger.info('rule run started', { enabled: enabledRules.length, total: rules.length });
143
143
  addSpanEvent('rule.run.start', { rules: enabledRules.length, total: rules.length });
144
- void this.events?.emit('rule.run.start', { rules: enabledRules.length, total: rules.length });
144
+ void this.events?.emit('rule.run.start', {
145
+ rules: enabledRules.length,
146
+ total: rules.length,
147
+ severity: 'info',
148
+ });
145
149
 
146
150
  const findings: ConstraintFinding[] = [];
147
151
  const fixes: Fix[] = [];
@@ -164,7 +168,12 @@ export class RuleEngine {
164
168
  const evalStartMs = Date.now();
165
169
  this.logger.debug('eval start', { ruleId: rule.id, index, total: enabledRules.length });
166
170
  addSpanEvent('rule.eval.start', { ruleId: rule.id, index, total: enabledRules.length });
167
- void this.events?.emit('rule.eval.start', { ruleId: rule.id, index, total: enabledRules.length });
171
+ void this.events?.emit('rule.eval.start', {
172
+ ruleId: rule.id,
173
+ index,
174
+ total: enabledRules.length,
175
+ severity: 'info',
176
+ });
168
177
 
169
178
  let ruleFindings: ConstraintFinding[] = [];
170
179
  let ruleEvalFixes: Fix[] = [];
@@ -184,7 +193,11 @@ export class RuleEngine {
184
193
  }),
185
194
  ];
186
195
  addSpanEvent('rule.eval.error', { ruleId: rule.id, error: evalError });
187
- void this.events?.emit('rule.eval.error', { ruleId: rule.id, error: evalError });
196
+ void this.events?.emit('rule.eval.error', {
197
+ ruleId: rule.id,
198
+ error: evalError,
199
+ severity: 'error',
200
+ });
188
201
  }
189
202
 
190
203
  const durationMs = Date.now() - evalStartMs;
@@ -198,6 +211,7 @@ export class RuleEngine {
198
211
  findings: ruleFindings.length,
199
212
  durationMs,
200
213
  details: ruleFindings,
214
+ severity: 'info',
201
215
  });
202
216
 
203
217
  // Persist eval completion.
@@ -262,6 +276,7 @@ export class RuleEngine {
262
276
  findings: findings.length,
263
277
  durationMs: runDurationMs,
264
278
  stoppedEarly: stoppedEarlyLocal,
279
+ severity: 'info',
265
280
  });
266
281
 
267
282
  // Finalize the run row.
@@ -60,6 +60,13 @@ export interface ScannedFile {
60
60
  readonly content: string;
61
61
  }
62
62
 
63
+ /**
64
+ * Per-file byte cap for {@link scanFiles} (task 0060 R15). Larger files are skipped
65
+ * with no content buffer so a single oversized artifact cannot balloon memory while
66
+ * a rule corpus is assembled; evaluators still see every smaller file unchanged.
67
+ */
68
+ const MAX_SCANNED_FILE_BYTES = 2_000_000;
69
+
63
70
  /** How `scanFiles` matches `include` / `exclude` against discovered paths. */
64
71
  export type ScanMatchMode = 'loose' | 'glob';
65
72
 
@@ -102,6 +109,9 @@ export async function scanFiles(options: ScanFilesOptions): Promise<ScannedFile[
102
109
  : await discoverFilesByGlob(options.workdir, options.include, options.exclude, fs);
103
110
  const scanned: ScannedFile[] = [];
104
111
  for (const file of files) {
112
+ // Skip oversized files instead of buffering them (task 0060 R15).
113
+ const stat = await fs.stat(resolvePath(options.workdir, file));
114
+ if (stat !== null && stat.size > MAX_SCANNED_FILE_BYTES) continue;
105
115
  scanned.push({ file, content: await readWorkdirFile(options.workdir, file, fs) });
106
116
  }
107
117
  return scanned;
package/src/events.ts CHANGED
@@ -1,11 +1,12 @@
1
+ import type { EventSeverity } from '@gobing-ai/ts-infra';
1
2
  import type { ConstraintFinding } from './types';
2
3
 
3
4
  /** Typed event map for rule-engine run observability. All events prefixed `rule.`. */
4
5
  export type RuleEngineEvents = {
5
6
  /** Emitted before the first rule is evaluated. */
6
- 'rule.run.start': (data: { rules: number; total: number }) => void;
7
+ 'rule.run.start': (data: { rules: number; total: number; severity: EventSeverity }) => void;
7
8
  /** Emitted immediately before a single rule's evaluator is invoked. */
8
- 'rule.eval.start': (data: { ruleId: string; index: number; total: number }) => void;
9
+ 'rule.eval.start': (data: { ruleId: string; index: number; total: number; severity: EventSeverity }) => void;
9
10
  /**
10
11
  * Emitted after a single rule evaluation finishes successfully.
11
12
  * `findings` is the count; `details` carries the actual finding objects so
@@ -17,9 +18,16 @@ export type RuleEngineEvents = {
17
18
  findings: number;
18
19
  durationMs: number;
19
20
  details: ConstraintFinding[];
21
+ severity: EventSeverity;
20
22
  }) => void;
21
23
  /** Emitted when a rule evaluator throws (in addition to the `kind:'error'` finding). */
22
- 'rule.eval.error': (data: { ruleId: string; error: string }) => void;
24
+ 'rule.eval.error': (data: { ruleId: string; error: string; severity: EventSeverity }) => void;
23
25
  /** Emitted after the last rule finishes (or was short-circuited). */
24
- 'rule.run.done': (data: { rules: number; findings: number; durationMs: number; stoppedEarly: boolean }) => void;
26
+ 'rule.run.done': (data: {
27
+ rules: number;
28
+ findings: number;
29
+ durationMs: number;
30
+ stoppedEarly: boolean;
31
+ severity: EventSeverity;
32
+ }) => void;
25
33
  };