@gobing-ai/ts-rule-engine 0.3.0 → 0.3.2

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 (82) 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/rule-engine-host.d.ts +1 -1
  47. package/dist/host/rule-engine-host.d.ts.map +1 -1
  48. package/dist/host/rule-engine-host.js +1 -1
  49. package/dist/index.d.ts +3 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +7 -1
  52. package/dist/types.d.ts +2 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +20 -6
  55. package/package.json +4 -5
  56. package/rules/spur-dev.yaml +1 -1
  57. package/src/config/extensions.ts +58 -29
  58. package/src/config/loader.ts +27 -12
  59. package/src/engine.ts +132 -47
  60. package/src/evaluators/agent-detection-evaluator.ts +2 -8
  61. package/src/evaluators/coverage-gate-evaluator.ts +4 -5
  62. package/src/evaluators/exit-code-evaluator.ts +6 -23
  63. package/src/evaluators/file-utils.ts +70 -8
  64. package/src/evaluators/forbidden-import-evaluator.ts +2 -9
  65. package/src/evaluators/path-evaluator.ts +5 -18
  66. package/src/evaluators/regex-evaluator.ts +4 -11
  67. package/src/evaluators/ripgrep-evaluator.ts +167 -0
  68. package/src/evaluators/schema-artifact-evaluator.ts +3 -8
  69. package/src/evaluators/sg-evaluator.ts +21 -4
  70. package/src/evaluators/test-location-evaluator.ts +3 -16
  71. package/src/events.ts +13 -0
  72. package/src/fixers/fixers.ts +12 -6
  73. package/src/fixers/test-stub-fixer.ts +4 -5
  74. package/src/host/builtins.ts +5 -3
  75. package/src/host/rule-engine-host.ts +1 -1
  76. package/src/index.ts +8 -1
  77. package/src/types.ts +20 -6
  78. package/dist/host/capability-registry.d.ts +0 -24
  79. package/dist/host/capability-registry.d.ts.map +0 -1
  80. package/dist/host/capability-registry.js +0 -28
  81. package/src/host/capability-registry.ts +0 -41
  82. /package/rules/{typescript → quality}/tsdoc-exports.yaml +0 -0
@@ -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');
@@ -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
@@ -1,8 +1,16 @@
1
+ import { assertRelativeExtensionPath } from '@gobing-ai/ts-runtime/plugin';
1
2
  import { z } from 'zod';
2
3
 
3
4
  /** Finding severity emitted by the rule engine. */
4
5
  export type RuleSeverity = 'error' | 'warning' | 'info';
5
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
+
6
14
  /** Fix authority level for candidate fixes. */
7
15
  export type FixMode = 'none' | 'suggest' | 'auto';
8
16
 
@@ -218,16 +226,22 @@ export const ConstraintRuleSchema = z.object({
218
226
  *
219
227
  * Rejects absolute paths and `..` traversal: extension declarations are data, and a
220
228
  * path that escapes the declaring file's directory is a trust-boundary violation even
221
- * when extension loading is explicitly allowed.
229
+ * when extension loading is explicitly allowed. The actual guard is the shared
230
+ * `assertRelativeExtensionPath` (ADR-010) so schema-time validation here and load-time
231
+ * validation in the shared loader use one source of truth.
222
232
  */
223
233
  const relativeExtensionPath = z
224
234
  .string()
225
235
  .min(1)
226
- .refine((value) => !/^([/\\]|[A-Za-z]:[/\\])/.test(value), {
227
- message: 'extension path must be relative (no absolute paths)',
228
- })
229
- .refine((value) => !value.split(/[/\\]/).includes('..'), {
230
- message: 'extension path must not contain ".." traversal',
236
+ .superRefine((value, ctx) => {
237
+ try {
238
+ assertRelativeExtensionPath(value);
239
+ } catch (error) {
240
+ ctx.addIssue({
241
+ code: 'custom',
242
+ message: error instanceof Error ? error.message : 'invalid extension path',
243
+ });
244
+ }
231
245
  });
232
246
 
233
247
  /**
@@ -1,24 +0,0 @@
1
- /** Registry origin for a host capability. */
2
- export type CapabilityOrigin = 'builtin' | 'extension';
3
- /** Registry entry metadata. */
4
- export interface CapabilityEntry<TCapability> {
5
- /** Capability implementation. */
6
- capability: TCapability;
7
- /** Registration origin. */
8
- origin: CapabilityOrigin;
9
- }
10
- /** Typed registry used by the rule engine host. */
11
- export declare class CapabilityRegistry<TCapability> {
12
- private readonly kind;
13
- private readonly capabilities;
14
- constructor(kind: string);
15
- /** Register or replace a capability. */
16
- register(name: string, capability: TCapability, origin?: CapabilityOrigin): void;
17
- /** Return true when a capability exists. */
18
- has(name: string): boolean;
19
- /** Get a registered capability or throw a clear error. */
20
- get(name: string): TCapability;
21
- /** List registered capability names. */
22
- list(): string[];
23
- }
24
- //# 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,6CAA6C;AAC7C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,+BAA+B;AAC/B,MAAM,WAAW,eAAe,CAAC,WAAW;IACxC,iCAAiC;IACjC,UAAU,EAAE,WAAW,CAAC;IACxB,2BAA2B;IAC3B,MAAM,EAAE,gBAAgB,CAAC;CAC5B;AAED,mDAAmD;AACnD,qBAAa,kBAAkB,CAAC,WAAW;IAG3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmD;gBAEnD,IAAI,EAAE,MAAM;IAEzC,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAE,gBAA8B,GAAG,IAAI;IAI7F,4CAA4C;IAC5C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,0DAA0D;IAC1D,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW;IAQ9B,wCAAwC;IACxC,IAAI,IAAI,MAAM,EAAE;CAGnB"}
@@ -1,28 +0,0 @@
1
- /** Typed registry used by the rule engine host. */
2
- export class CapabilityRegistry {
3
- kind;
4
- capabilities = new Map();
5
- constructor(kind) {
6
- this.kind = kind;
7
- }
8
- /** Register or replace a capability. */
9
- register(name, capability, origin = 'extension') {
10
- this.capabilities.set(name, { capability, origin });
11
- }
12
- /** Return true when a capability exists. */
13
- has(name) {
14
- return this.capabilities.has(name);
15
- }
16
- /** Get a registered capability or throw a clear error. */
17
- get(name) {
18
- const entry = this.capabilities.get(name);
19
- if (entry === undefined) {
20
- throw new Error(`Unknown ${this.kind}: ${name}`);
21
- }
22
- return entry.capability;
23
- }
24
- /** List registered capability names. */
25
- list() {
26
- return [...this.capabilities.keys()];
27
- }
28
- }
@@ -1,41 +0,0 @@
1
- /** Registry origin for a host capability. */
2
- export type CapabilityOrigin = 'builtin' | 'extension';
3
-
4
- /** Registry entry metadata. */
5
- export interface CapabilityEntry<TCapability> {
6
- /** Capability implementation. */
7
- capability: TCapability;
8
- /** Registration origin. */
9
- origin: CapabilityOrigin;
10
- }
11
-
12
- /** Typed registry used by the rule engine host. */
13
- export class CapabilityRegistry<TCapability> {
14
- private readonly capabilities = new Map<string, CapabilityEntry<TCapability>>();
15
-
16
- constructor(private readonly kind: string) {}
17
-
18
- /** Register or replace a capability. */
19
- register(name: string, capability: TCapability, origin: CapabilityOrigin = 'extension'): void {
20
- this.capabilities.set(name, { capability, origin });
21
- }
22
-
23
- /** Return true when a capability exists. */
24
- has(name: string): boolean {
25
- return this.capabilities.has(name);
26
- }
27
-
28
- /** Get a registered capability or throw a clear error. */
29
- get(name: string): TCapability {
30
- const entry = this.capabilities.get(name);
31
- if (entry === undefined) {
32
- throw new Error(`Unknown ${this.kind}: ${name}`);
33
- }
34
- return entry.capability;
35
- }
36
-
37
- /** List registered capability names. */
38
- list(): string[] {
39
- return [...this.capabilities.keys()];
40
- }
41
- }