@gobing-ai/ts-rule-engine 0.3.1 → 0.3.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 (86) hide show
  1. package/README.md +328 -58
  2. package/dist/config/extensions.d.ts +10 -7
  3. package/dist/config/extensions.d.ts.map +1 -1
  4. package/dist/config/extensions.js +48 -23
  5. package/dist/config/loader.d.ts +7 -0
  6. package/dist/config/loader.d.ts.map +1 -1
  7. package/dist/config/loader.js +17 -12
  8. package/dist/engine.d.ts +13 -2
  9. package/dist/engine.d.ts.map +1 -1
  10. package/dist/engine.js +107 -45
  11. package/dist/evaluators/agent-detection-evaluator.d.ts.map +1 -1
  12. package/dist/evaluators/agent-detection-evaluator.js +2 -9
  13. package/dist/evaluators/coverage-gate-evaluator.d.ts.map +1 -1
  14. package/dist/evaluators/coverage-gate-evaluator.js +4 -5
  15. package/dist/evaluators/exit-code-evaluator.d.ts.map +1 -1
  16. package/dist/evaluators/exit-code-evaluator.js +6 -23
  17. package/dist/evaluators/file-utils.d.ts +25 -1
  18. package/dist/evaluators/file-utils.d.ts.map +1 -1
  19. package/dist/evaluators/file-utils.js +48 -8
  20. package/dist/evaluators/forbidden-import-evaluator.js +2 -10
  21. package/dist/evaluators/path-evaluator.d.ts.map +1 -1
  22. package/dist/evaluators/path-evaluator.js +5 -18
  23. package/dist/evaluators/regex-evaluator.js +3 -11
  24. package/dist/evaluators/ripgrep-evaluator.d.ts +50 -0
  25. package/dist/evaluators/ripgrep-evaluator.d.ts.map +1 -0
  26. package/dist/evaluators/ripgrep-evaluator.js +145 -0
  27. package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -1
  28. package/dist/evaluators/schema-artifact-evaluator.js +3 -7
  29. package/dist/evaluators/sg-evaluator.d.ts +10 -2
  30. package/dist/evaluators/sg-evaluator.d.ts.map +1 -1
  31. package/dist/evaluators/sg-evaluator.js +21 -4
  32. package/dist/evaluators/test-location-evaluator.d.ts +2 -2
  33. package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
  34. package/dist/evaluators/test-location-evaluator.js +2 -15
  35. package/dist/events.d.ts +33 -0
  36. package/dist/events.d.ts.map +1 -0
  37. package/dist/events.js +0 -0
  38. package/dist/fixers/fixers.d.ts +1 -1
  39. package/dist/fixers/fixers.d.ts.map +1 -1
  40. package/dist/fixers/fixers.js +4 -5
  41. package/dist/fixers/test-stub-fixer.d.ts +1 -1
  42. package/dist/fixers/test-stub-fixer.d.ts.map +1 -1
  43. package/dist/fixers/test-stub-fixer.js +3 -4
  44. package/dist/host/builtins.d.ts.map +1 -1
  45. package/dist/host/builtins.js +5 -3
  46. package/dist/host/bundled-rules.d.ts +1 -1
  47. package/dist/host/bundled-rules.js +1 -1
  48. package/dist/host/rule-engine-host.d.ts +1 -1
  49. package/dist/host/rule-engine-host.d.ts.map +1 -1
  50. package/dist/host/rule-engine-host.js +1 -1
  51. package/dist/index.d.ts +3 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +7 -1
  54. package/dist/types.d.ts +2 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/types.js +6 -0
  57. package/package.json +4 -5
  58. package/rules/example.yaml +13 -0
  59. package/src/config/extensions.ts +58 -29
  60. package/src/config/loader.ts +27 -12
  61. package/src/engine.ts +132 -47
  62. package/src/evaluators/agent-detection-evaluator.ts +2 -8
  63. package/src/evaluators/coverage-gate-evaluator.ts +4 -5
  64. package/src/evaluators/exit-code-evaluator.ts +6 -23
  65. package/src/evaluators/file-utils.ts +70 -8
  66. package/src/evaluators/forbidden-import-evaluator.ts +2 -9
  67. package/src/evaluators/path-evaluator.ts +5 -18
  68. package/src/evaluators/regex-evaluator.ts +4 -11
  69. package/src/evaluators/ripgrep-evaluator.ts +167 -0
  70. package/src/evaluators/schema-artifact-evaluator.ts +3 -8
  71. package/src/evaluators/sg-evaluator.ts +21 -4
  72. package/src/evaluators/test-location-evaluator.ts +3 -16
  73. package/src/events.ts +13 -0
  74. package/src/fixers/fixers.ts +12 -6
  75. package/src/fixers/test-stub-fixer.ts +4 -5
  76. package/src/host/builtins.ts +5 -3
  77. package/src/host/bundled-rules.ts +1 -1
  78. package/src/host/rule-engine-host.ts +1 -1
  79. package/src/index.ts +8 -1
  80. package/src/types.ts +7 -0
  81. package/dist/host/capability-registry.d.ts +0 -10
  82. package/dist/host/capability-registry.d.ts.map +0 -1
  83. package/dist/host/capability-registry.js +0 -9
  84. package/rules/recommended.yaml +0 -10
  85. package/rules/spur-dev.yaml +0 -6
  86. package/src/host/capability-registry.ts +0 -9
@@ -0,0 +1,167 @@
1
+ import { NodeProcessExecutor, type ProcessExecutor } from '@gobing-ai/ts-runtime';
2
+ import {
3
+ type ConstraintRule,
4
+ createFinding,
5
+ type RuleContext,
6
+ type RuleEvaluationResult,
7
+ type RuleEvaluator,
8
+ } from '../types';
9
+ import { configString, DEFAULT_EXCLUDES } from './file-utils';
10
+
11
+ /**
12
+ * Evaluates source files against a regex using the `rg` (ripgrep) CLI.
13
+ *
14
+ * This is the real ripgrep-backed engine registered under the `rg` rule type. Unlike
15
+ * the {@link import('./regex-evaluator').RegexEvaluator} (`regex` type, JS `RegExp`),
16
+ * it runs ripgrep's linear-time Rust regex engine: ReDoS-immune, parallel, and pruning
17
+ * heavy trees during traversal. The dialect differs from JS `RegExp` — no lookbehind or
18
+ * backreferences — so rule patterns must be ripgrep-compatible (enforced by the
19
+ * `rg-dialect` spur rule; see {@link isRipgrepCompatiblePattern}).
20
+ *
21
+ * ## Options (in `evaluator.config`)
22
+ * - `pattern` — ripgrep regex to search for (required).
23
+ * - `mode` — `forbid` (default): each match is a finding. `require`: a finding per file
24
+ * that lacks the pattern.
25
+ * - `multiline` — when `true`, patterns may span lines (`rg -U --multiline-dotall`).
26
+ *
27
+ * Inline flags like `(?i)` are passed through to ripgrep, which supports them natively.
28
+ *
29
+ * The rule's `include` globs and `exclude` globs (plus {@link DEFAULT_EXCLUDES}) are
30
+ * forwarded as `--glob` / `--glob '!…'` so ripgrep prunes during traversal rather than
31
+ * walking everything — the same skip-list the in-process discovery path uses, applied
32
+ * regardless of whether the workspace is a git repo or has a `.gitignore`.
33
+ */
34
+ export class RipgrepEvaluator implements RuleEvaluator {
35
+ private readonly executor: ProcessExecutor;
36
+
37
+ constructor(executor: ProcessExecutor = new NodeProcessExecutor()) {
38
+ this.executor = executor;
39
+ }
40
+
41
+ /** Run ripgrep and emit findings for matches (forbid) or absent files (require). */
42
+ async evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult> {
43
+ const config = rule.evaluator.config ?? {};
44
+ const pattern = configString(config, 'pattern', undefined, { evaluator: 'rg' });
45
+ const mode = configString(config, 'mode', 'forbid');
46
+ const multiline = config.multiline === true;
47
+
48
+ const args = buildArgs(pattern, mode, multiline, rule.include ?? [], rule.exclude ?? []);
49
+ const result = await this.executor.run({
50
+ command: 'rg',
51
+ args,
52
+ cwd: context.workdir,
53
+ timeout: 60_000,
54
+ rejectOnError: false,
55
+ label: 'rg',
56
+ });
57
+
58
+ // ripgrep exits 0 with matches, 1 when none, 2 on error. Treat 2 (or any non-0/1
59
+ // with stderr) as a hard failure so a broken pattern or missing `rg` fails loud.
60
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
61
+ const detail = result.stderr.trim();
62
+ throw new Error(`rg failed (exit ${result.exitCode})${detail.length > 0 ? `: ${detail}` : ''}`);
63
+ }
64
+
65
+ return mode === 'require'
66
+ ? { findings: requireFindings(rule, pattern, result.stdout), fixes: [] }
67
+ : { findings: forbidFindings(rule, pattern, result.stdout), fixes: [] };
68
+ }
69
+ }
70
+
71
+ /** Build the ripgrep argument list for the given mode and scope. */
72
+ function buildArgs(pattern: string, mode: string, multiline: boolean, include: string[], exclude: string[]): string[] {
73
+ const args: string[] = [];
74
+ if (multiline) args.push('-U', '--multiline-dotall');
75
+
76
+ if (mode === 'require') {
77
+ // Files lacking the pattern, one path per line.
78
+ args.push('--files-without-match');
79
+ } else {
80
+ // Structured events carrying file + line_number for precise findings.
81
+ args.push('--json');
82
+ }
83
+
84
+ // Scope: includes as positive globs, DEFAULT_EXCLUDES + rule excludes as negated globs
85
+ // so ripgrep prunes during traversal (not after) regardless of .gitignore presence.
86
+ for (const glob of include) args.push('--glob', glob);
87
+ for (const dir of DEFAULT_EXCLUDES) args.push('--glob', `!**/${dir}/**`);
88
+ for (const glob of exclude) args.push('--glob', `!${glob}`);
89
+
90
+ // `--` guards against a pattern that begins with `-`.
91
+ args.push('--', pattern);
92
+ return args;
93
+ }
94
+
95
+ /** Parse `rg --json` match events into one finding per matched line. */
96
+ function forbidFindings(rule: ConstraintRule, pattern: string, stdout: string): ReturnType<typeof createFinding>[] {
97
+ const findings: ReturnType<typeof createFinding>[] = [];
98
+ for (const line of stdout.split('\n')) {
99
+ const trimmed = line.trim();
100
+ if (trimmed.length === 0) continue;
101
+ let event: RipgrepEvent;
102
+ try {
103
+ event = JSON.parse(trimmed) as RipgrepEvent;
104
+ } catch {
105
+ continue; // Skip non-JSON noise.
106
+ }
107
+ if (event.type !== 'match') continue;
108
+ const file = event.data?.path?.text;
109
+ const lineNumber = event.data?.line_number;
110
+ if (typeof file !== 'string' || typeof lineNumber !== 'number') continue;
111
+ findings.push(
112
+ createFinding(rule, `forbidden pattern found: ${pattern}`, file, {
113
+ line: lineNumber,
114
+ code: 'rg:found',
115
+ }),
116
+ );
117
+ }
118
+ return findings;
119
+ }
120
+
121
+ /** Build one finding per file path printed by `rg --files-without-match`. */
122
+ function requireFindings(rule: ConstraintRule, pattern: string, stdout: string): ReturnType<typeof createFinding>[] {
123
+ const findings: ReturnType<typeof createFinding>[] = [];
124
+ for (const line of stdout.split('\n')) {
125
+ const file = line.trim();
126
+ if (file.length === 0) continue;
127
+ findings.push(createFinding(rule, `required pattern not found: ${pattern}`, file, { code: 'rg:missing' }));
128
+ }
129
+ return findings;
130
+ }
131
+
132
+ /** Minimal shape of a `rg --json` event (only the fields this evaluator reads). */
133
+ interface RipgrepEvent {
134
+ type: string;
135
+ data?: {
136
+ path?: { text?: string };
137
+ line_number?: number;
138
+ };
139
+ }
140
+
141
+ /** JS-`RegExp`-only constructs that ripgrep's Rust regex engine does not support. */
142
+ const JS_ONLY_REGEX_FEATURES: { readonly name: string; readonly test: RegExp }[] = [
143
+ { name: 'lookbehind', test: /\(\?<[=!]/ },
144
+ { name: 'backreference', test: /\\[1-9]/ },
145
+ { name: 'named backreference', test: /\\k<[^>]+>/ },
146
+ ];
147
+
148
+ /**
149
+ * Report whether a regex `pattern` is safe to run under ripgrep's engine.
150
+ *
151
+ * ripgrep's Rust `regex` crate is linear-time and therefore omits features that require
152
+ * backtracking — lookbehind and backreferences. A pattern using them works under the JS
153
+ * `regex` evaluator but fails to compile under `rg`. The `rg-dialect` spur rule and the
154
+ * downstream rule-file converter use this to keep incompatible patterns on the `regex`
155
+ * type instead of silently breaking them on `rg`.
156
+ *
157
+ * @returns `{ compatible: true }` or `{ compatible: false, feature }` naming the first
158
+ * unsupported construct found.
159
+ */
160
+ export function isRipgrepCompatiblePattern(
161
+ pattern: string,
162
+ ): { compatible: true } | { compatible: false; feature: string } {
163
+ for (const { name, test } of JS_ONLY_REGEX_FEATURES) {
164
+ if (test.test(pattern)) return { compatible: false, feature: name };
165
+ }
166
+ return { compatible: true };
167
+ }
@@ -1,5 +1,4 @@
1
- import { resolve } from 'node:path';
2
- import { NodeFileSystem } from '@gobing-ai/ts-runtime';
1
+ import { joinPath, NodeFileSystem } from '@gobing-ai/ts-runtime';
3
2
  import {
4
3
  type ConstraintRule,
5
4
  createFinding,
@@ -7,6 +6,7 @@ import {
7
6
  type RuleEvaluationResult,
8
7
  type RuleEvaluator,
9
8
  } from '../types';
9
+ import { stringArray } from './file-utils';
10
10
 
11
11
  /**
12
12
  * Evaluates JSON schema artifact files for structural integrity.
@@ -42,7 +42,7 @@ export class SchemaArtifactEvaluator implements RuleEvaluator {
42
42
  const requireRequiredArray = config.requireRequiredArray === true;
43
43
 
44
44
  // Check existence.
45
- const absolutePath = resolve(context.workdir, file);
45
+ const absolutePath = joinPath(context.workdir, file);
46
46
  const exists = await this.fs.exists(absolutePath);
47
47
  if (!exists) {
48
48
  return {
@@ -127,8 +127,3 @@ export class SchemaArtifactEvaluator implements RuleEvaluator {
127
127
  return { findings, fixes: [] };
128
128
  }
129
129
  }
130
-
131
- /** Return a string array if value is a string array, otherwise undefined. */
132
- function stringArray(value: unknown): string[] | undefined {
133
- return Array.isArray(value) && value.every((item) => typeof item === 'string') ? (value as string[]) : undefined;
134
- }
@@ -6,7 +6,7 @@ import {
6
6
  type RuleEvaluationResult,
7
7
  type RuleEvaluator,
8
8
  } from '../types';
9
- import { matchesGlob } from './file-utils';
9
+ import { DEFAULT_EXCLUDES, matchesGlob } from './file-utils';
10
10
 
11
11
  /**
12
12
  * Evaluates source code against an ast-grep pattern using the `sg` CLI.
@@ -15,8 +15,16 @@ import { matchesGlob } from './file-utils';
15
15
  * - `pattern` — ast-grep pattern to search for (required).
16
16
  * - `language` — language for ast-grep parsing (default: `typescript`).
17
17
  *
18
- * Include globs from the rule are forwarded to sg via `--glob` arguments.
19
- * Exclude globs from the rule are applied in-process after parsing sg output.
18
+ * Scope is forwarded to `sg` via `--globs` so the subprocess **prunes during traversal**
19
+ * rather than walking everything and filtering after:
20
+ * - the rule's `include` globs become positive `--globs` patterns;
21
+ * - {@link DEFAULT_EXCLUDES} (`node_modules`, `dist`, …) and the rule's `exclude` globs
22
+ * become negated `--globs '!…'` patterns, so heavy generated trees are never descended
23
+ * into even when a rule forgets to exclude them. ast-grep takes the later glob on
24
+ * precedence, so exclusions are appended after includes.
25
+ *
26
+ * The rule's `exclude` is also re-applied in-process as a belt-and-suspenders against any
27
+ * difference between ast-grep's glob semantics and {@link matchesGlob}.
20
28
  */
21
29
  export class SgEvaluator implements RuleEvaluator {
22
30
  private readonly executor: ProcessExecutor;
@@ -38,8 +46,17 @@ export class SgEvaluator implements RuleEvaluator {
38
46
  const exclude = rule.exclude ?? [];
39
47
 
40
48
  const args: string[] = ['run', '--pattern', pattern, '--lang', language, '--json'];
49
+ // Positive include globs first.
41
50
  for (const glob of include) {
42
- args.push(`--glob=${glob}`);
51
+ args.push('--globs', glob);
52
+ }
53
+ // Negated globs prune at traversal time. Default excludes go first so a rule's own
54
+ // exclude (later → higher precedence in ast-grep) can still re-include if intended.
55
+ for (const dir of DEFAULT_EXCLUDES) {
56
+ args.push('--globs', `!**/${dir}/**`);
57
+ }
58
+ for (const glob of exclude) {
59
+ args.push('--globs', `!${glob}`);
43
60
  }
44
61
 
45
62
  const result = await this.executor.run({
@@ -1,5 +1,5 @@
1
- import type { CapabilityRegistry } from '../host/capability-registry';
2
- import type { TestPathResolver } from '../resolvers/test-path-resolver';
1
+ import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
2
+ import { type TestPathResolver, TypeScriptTestPathResolver } from '../resolvers/test-path-resolver';
3
3
  import {
4
4
  type ConstraintRule,
5
5
  createFinding,
@@ -111,17 +111,4 @@ export class TestLocationEvaluator implements RuleEvaluator {
111
111
  }
112
112
 
113
113
  /** Built-in TypeScript convention used when no resolver registry is injected. */
114
- const TYPESCRIPT_FALLBACK: TestPathResolver = {
115
- name: 'typescript',
116
- resolveTestPath(srcRelPath: string): string {
117
- if (srcRelPath.includes('.test.') || srcRelPath.includes('.spec.')) return srcRelPath;
118
- const srcIdx = srcRelPath.indexOf('/src/');
119
- if (srcIdx !== -1) {
120
- const pkg = srcRelPath.slice(0, srcIdx);
121
- const rel = srcRelPath.slice(srcIdx + '/src/'.length).replace(/\.(ts|tsx|js|jsx)$/, '.test.ts');
122
- return `${pkg}/tests/${rel}`;
123
- }
124
- const withoutExt = srcRelPath.replace(/\.(ts|tsx|js|jsx)$/, '');
125
- return `tests/${withoutExt.replace(/^src\//, '')}.test.ts`;
126
- },
127
- };
114
+ const TYPESCRIPT_FALLBACK: TestPathResolver = new TypeScriptTestPathResolver();
package/src/events.ts ADDED
@@ -0,0 +1,13 @@
1
+ /** Typed event map for rule-engine run observability. All events prefixed `rule.`. */
2
+ export type RuleEngineEvents = {
3
+ /** Emitted before the first rule is evaluated. */
4
+ 'rule.run.start': (data: { rules: number; total: number }) => void;
5
+ /** Emitted immediately before a single rule's evaluator is invoked. */
6
+ 'rule.eval.start': (data: { ruleId: string; index: number; total: number }) => void;
7
+ /** Emitted after a single rule evaluation finishes successfully. */
8
+ 'rule.eval.done': (data: { ruleId: string; findings: number; durationMs: number }) => void;
9
+ /** Emitted when a rule evaluator throws (in addition to the `kind:'error'` finding). */
10
+ 'rule.eval.error': (data: { ruleId: string; error: string }) => void;
11
+ /** Emitted after the last rule finishes (or was short-circuited). */
12
+ 'rule.run.done': (data: { rules: number; findings: number; durationMs: number; stoppedEarly: boolean }) => void;
13
+ };
@@ -5,9 +5,15 @@
5
5
  * @module rule-engine/fixers
6
6
  */
7
7
 
8
- import { isAbsolute, join, relative, resolve } from 'node:path';
9
- import { NodeFileSystem, type ProcessExecutor } from '@gobing-ai/ts-runtime';
10
- import type { CapabilityRegistry } from '../host/capability-registry';
8
+ import {
9
+ isAbsolutePath,
10
+ joinPath,
11
+ NodeFileSystem,
12
+ type ProcessExecutor,
13
+ relativePath,
14
+ resolvePath,
15
+ } from '@gobing-ai/ts-runtime';
16
+ import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
11
17
  import type { TestPathResolver } from '../resolvers/test-path-resolver';
12
18
  import type { ConstraintFinding, ConstraintRule, Fix, FixMode, RuleContext } from '../types';
13
19
  import { TestStubFixer } from './test-stub-fixer';
@@ -83,7 +89,7 @@ export function builtInFixers(host?: BuiltInFixersDeps, exec?: ProcessExecutor):
83
89
 
84
90
  /** Resolve a workdir-relative or absolute path to an absolute path. */
85
91
  export function resolveWorkdirPath(workdir: string, filePath: string): string {
86
- return isAbsolute(filePath) ? filePath : join(workdir, filePath);
92
+ return isAbsolutePath(filePath) ? filePath : joinPath(workdir, filePath);
87
93
  }
88
94
 
89
95
  /** Apply byte-range fixes to files, optionally returning a dry-run diff only. */
@@ -181,8 +187,8 @@ function selectNonOverlappingFixes(fixes: readonly Fix[]): { applied: Fix[]; def
181
187
 
182
188
  /** Return true when absPath is at or below workdir. */
183
189
  function isInsideWorkdir(workdir: string, absPath: string): boolean {
184
- const rel = relative(resolve(workdir), resolve(absPath));
185
- return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
190
+ const rel = relativePath(resolvePath(workdir), resolvePath(absPath));
191
+ return rel === '' || (!rel.startsWith('..') && !isAbsolutePath(rel));
186
192
  }
187
193
 
188
194
  /** Return true when [start, end] is a valid byte range for a string of contentLength bytes. */
@@ -11,9 +11,8 @@
11
11
  * @module rule-engine/fixers/test-stub-fixer
12
12
  */
13
13
 
14
- import { isAbsolute, join } from 'node:path';
15
- import { NodeFileSystem, type ProcessExecutor } from '@gobing-ai/ts-runtime';
16
- import type { CapabilityRegistry } from '../host/capability-registry';
14
+ import { isAbsolutePath, joinPath, NodeFileSystem, type ProcessExecutor } from '@gobing-ai/ts-runtime';
15
+ import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
17
16
  import type { TestPathResolver } from '../resolvers/test-path-resolver';
18
17
  import type { Fix } from '../types';
19
18
  import type { RuleFixerInput, RuleFixerProvider } from './fixers';
@@ -39,7 +38,7 @@ export interface TestStubFixerDeps {
39
38
 
40
39
  /** Normalize a finding path to a repository-relative POSIX path, or return null when invalid. */
41
40
  function normalizeFindingPath(filePath: string): string | null {
42
- if (!filePath || isAbsolute(filePath)) return null;
41
+ if (!filePath || isAbsolutePath(filePath)) return null;
43
42
  const normalized = filePath.replaceAll('\\', '/').replace(/^\.\//, '');
44
43
  if (normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) return null;
45
44
  return normalized;
@@ -94,7 +93,7 @@ export class TestStubFixer implements RuleFixerProvider {
94
93
  continue;
95
94
  }
96
95
 
97
- const absTestPath = join(context.workdir, testRelPath);
96
+ const absTestPath = joinPath(context.workdir, testRelPath);
98
97
  if (await this.fs.exists(absTestPath)) continue;
99
98
 
100
99
  // Export discovery is intentionally omitted — pass empty exports array.
@@ -6,6 +6,7 @@ import { ForbiddenImportEvaluator } from '../evaluators/forbidden-import-evaluat
6
6
  import { ImportBoundaryEvaluator } from '../evaluators/import-boundary-evaluator';
7
7
  import { PathEvaluator } from '../evaluators/path-evaluator';
8
8
  import { RegexEvaluator } from '../evaluators/regex-evaluator';
9
+ import { RipgrepEvaluator } from '../evaluators/ripgrep-evaluator';
9
10
  import { SchemaArtifactEvaluator } from '../evaluators/schema-artifact-evaluator';
10
11
  import { SecretsScannerEvaluator } from '../evaluators/secrets-scanner-evaluator';
11
12
  import { SgEvaluator } from '../evaluators/sg-evaluator';
@@ -23,10 +24,11 @@ import type { RuleEngineHost } from './rule-engine-host';
23
24
 
24
25
  /** Register bundled evaluators and formatters on a host. */
25
26
  export function registerBuiltins(host: RuleEngineHost, executor?: ProcessExecutor): void {
26
- const regex = new RegexEvaluator();
27
27
  const path = new PathEvaluator();
28
- host.evaluators.register('regex', regex, 'builtin');
29
- host.evaluators.register('rg', regex, 'builtin');
28
+ // `regex` = JS RegExp engine; `rg` = real ripgrep engine (ReDoS-immune, prunes during
29
+ // traversal). The two are honestly named distinct engines, not aliases.
30
+ host.evaluators.register('regex', new RegexEvaluator(), 'builtin');
31
+ host.evaluators.register('rg', new RipgrepEvaluator(executor), 'builtin');
30
32
  host.evaluators.register('path', path, 'builtin');
31
33
  host.evaluators.register('file-exist', path, 'builtin');
32
34
  host.evaluators.register('forbidden-import', new ForbiddenImportEvaluator(), 'builtin');
@@ -15,7 +15,7 @@ const defaultFs = new NodeSyncFileSystem();
15
15
  * Resolve the absolute path to the rule presets bundled with
16
16
  * `@gobing-ai/ts-rule-engine`.
17
17
  *
18
- * The directory ships portable presets (`recommended`, `spur-dev`) and category
18
+ * The directory ships a generic example preset and category
19
19
  * folders (`typescript`, `structure`, `quality`) so a consumer gets a working
20
20
  * default ruleset without authoring any files. Pass the returned path as the
21
21
  * lowest-priority entry to {@link loadPreset}'s `roots`, letting project-local
@@ -1,6 +1,6 @@
1
+ import { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
1
2
  import type { TestPathResolver } from '../resolvers/test-path-resolver';
2
3
  import type { ResultFormatter, RuleEvaluator } from '../types';
3
- import { CapabilityRegistry } from './capability-registry';
4
4
 
5
5
  /** Host container for rule-engine capabilities. */
6
6
  export class RuleEngineHost {
package/src/index.ts CHANGED
@@ -1,12 +1,19 @@
1
+ // Public re-export of the shared plugin registry (ADR-010). Sourced directly from
2
+ // the shared plugin core; the former host/capability-registry.ts shim was removed
3
+ // once the one-release back-compat window closed.
4
+ export { type CapabilityEntry, type CapabilityOrigin, CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
1
5
  export * from './config/extensions';
2
6
  export * from './config/loader';
3
7
  export * from './engine';
8
+ // Public dialect helper for the `rg` (ripgrep) evaluator — consumed by the downstream
9
+ // rule-file converter and the rg-dialect rule to keep JS-only patterns off the `rg` type.
10
+ export { isRipgrepCompatiblePattern } from './evaluators/ripgrep-evaluator';
11
+ export type { RuleEngineEvents } from './events';
4
12
  export * from './fixers/fixers';
5
13
  export * from './fixers/test-stub-fixer';
6
14
  export * from './formatters/json';
7
15
  export * from './formatters/text';
8
16
  export * from './host/bundled-rules';
9
- export * from './host/capability-registry';
10
17
  export * from './host/rule-engine-host';
11
18
  export * from './resolvers/test-path-resolver';
12
19
  export * from './types';
package/src/types.ts CHANGED
@@ -4,6 +4,13 @@ import { z } from 'zod';
4
4
  /** Finding severity emitted by the rule engine. */
5
5
  export type RuleSeverity = 'error' | 'warning' | 'info';
6
6
 
7
+ /** Numeric rank for severity comparison; higher = more severe. */
8
+ export const SEVERITY_RANK: Record<RuleSeverity, number> = {
9
+ error: 3,
10
+ warning: 2,
11
+ info: 1,
12
+ };
13
+
7
14
  /** Fix authority level for candidate fixes. */
8
15
  export type FixMode = 'none' | 'suggest' | 'auto';
9
16
 
@@ -1,10 +0,0 @@
1
- /**
2
- * Compatibility re-export (ADR-010 / task 0008).
3
- *
4
- * The capability registry now lives in the shared plugin core
5
- * (`@gobing-ai/ts-runtime/plugin`). This module re-exports it from the original
6
- * path so the public barrel and existing importers keep working unchanged. New
7
- * code should import from `@gobing-ai/ts-runtime/plugin` directly.
8
- */
9
- export { type CapabilityEntry, type CapabilityOrigin, CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
10
- //# sourceMappingURL=capability-registry.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"capability-registry.d.ts","sourceRoot":"","sources":["../../src/host/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
@@ -1,9 +0,0 @@
1
- /**
2
- * Compatibility re-export (ADR-010 / task 0008).
3
- *
4
- * The capability registry now lives in the shared plugin core
5
- * (`@gobing-ai/ts-runtime/plugin`). This module re-exports it from the original
6
- * path so the public barrel and existing importers keep working unchanged. New
7
- * code should import from `@gobing-ai/ts-runtime/plugin` directly.
8
- */
9
- export { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
@@ -1,10 +0,0 @@
1
- # Recommended preset — portable rule categories for TypeScript projects.
2
- # Use with: spur rule run --preset recommended
3
- #
4
- # Bundled with @gobing-ai/ts-rule-engine. A project may override individual rule
5
- # files by placing same-named files under its local .spur/rules/ root.
6
- name: recommended
7
- extends:
8
- - typescript
9
- - structure
10
- - quality
@@ -1,6 +0,0 @@
1
- # Development preset — stricter rules for development workflow.
2
- # Use with: spur rule run --preset spur-dev --rule coverage-gate --fail-on warning
3
- name: spur-dev
4
- extends:
5
- - typescript
6
- - quality
@@ -1,9 +0,0 @@
1
- /**
2
- * Compatibility re-export (ADR-010 / task 0008).
3
- *
4
- * The capability registry now lives in the shared plugin core
5
- * (`@gobing-ai/ts-runtime/plugin`). This module re-exports it from the original
6
- * path so the public barrel and existing importers keep working unchanged. New
7
- * code should import from `@gobing-ai/ts-runtime/plugin` directly.
8
- */
9
- export { type CapabilityEntry, type CapabilityOrigin, CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';