@gobing-ai/ts-rule-engine 0.3.1 → 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.
- package/README.md +328 -58
- package/dist/config/extensions.d.ts +10 -7
- package/dist/config/extensions.d.ts.map +1 -1
- package/dist/config/extensions.js +48 -23
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +17 -12
- package/dist/engine.d.ts +13 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +107 -45
- package/dist/evaluators/agent-detection-evaluator.d.ts.map +1 -1
- package/dist/evaluators/agent-detection-evaluator.js +2 -9
- package/dist/evaluators/coverage-gate-evaluator.d.ts.map +1 -1
- package/dist/evaluators/coverage-gate-evaluator.js +4 -5
- package/dist/evaluators/exit-code-evaluator.d.ts.map +1 -1
- package/dist/evaluators/exit-code-evaluator.js +6 -23
- package/dist/evaluators/file-utils.d.ts +25 -1
- package/dist/evaluators/file-utils.d.ts.map +1 -1
- package/dist/evaluators/file-utils.js +48 -8
- package/dist/evaluators/forbidden-import-evaluator.js +2 -10
- package/dist/evaluators/path-evaluator.d.ts.map +1 -1
- package/dist/evaluators/path-evaluator.js +5 -18
- package/dist/evaluators/regex-evaluator.js +3 -11
- package/dist/evaluators/ripgrep-evaluator.d.ts +50 -0
- package/dist/evaluators/ripgrep-evaluator.d.ts.map +1 -0
- package/dist/evaluators/ripgrep-evaluator.js +145 -0
- package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -1
- package/dist/evaluators/schema-artifact-evaluator.js +3 -7
- package/dist/evaluators/sg-evaluator.d.ts +10 -2
- package/dist/evaluators/sg-evaluator.d.ts.map +1 -1
- package/dist/evaluators/sg-evaluator.js +21 -4
- package/dist/evaluators/test-location-evaluator.d.ts +2 -2
- package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
- package/dist/evaluators/test-location-evaluator.js +2 -15
- package/dist/events.d.ts +33 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +0 -0
- package/dist/fixers/fixers.d.ts +1 -1
- package/dist/fixers/fixers.d.ts.map +1 -1
- package/dist/fixers/fixers.js +4 -5
- package/dist/fixers/test-stub-fixer.d.ts +1 -1
- package/dist/fixers/test-stub-fixer.d.ts.map +1 -1
- package/dist/fixers/test-stub-fixer.js +3 -4
- package/dist/host/builtins.d.ts.map +1 -1
- package/dist/host/builtins.js +5 -3
- package/dist/host/rule-engine-host.d.ts +1 -1
- package/dist/host/rule-engine-host.d.ts.map +1 -1
- package/dist/host/rule-engine-host.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/package.json +4 -5
- package/src/config/extensions.ts +58 -29
- package/src/config/loader.ts +27 -12
- package/src/engine.ts +132 -47
- package/src/evaluators/agent-detection-evaluator.ts +2 -8
- package/src/evaluators/coverage-gate-evaluator.ts +4 -5
- package/src/evaluators/exit-code-evaluator.ts +6 -23
- package/src/evaluators/file-utils.ts +70 -8
- package/src/evaluators/forbidden-import-evaluator.ts +2 -9
- package/src/evaluators/path-evaluator.ts +5 -18
- package/src/evaluators/regex-evaluator.ts +4 -11
- package/src/evaluators/ripgrep-evaluator.ts +167 -0
- package/src/evaluators/schema-artifact-evaluator.ts +3 -8
- package/src/evaluators/sg-evaluator.ts +21 -4
- package/src/evaluators/test-location-evaluator.ts +3 -16
- package/src/events.ts +13 -0
- package/src/fixers/fixers.ts +12 -6
- package/src/fixers/test-stub-fixer.ts +4 -5
- package/src/host/builtins.ts +5 -3
- package/src/host/rule-engine-host.ts +1 -1
- package/src/index.ts +8 -1
- package/src/types.ts +7 -0
- package/dist/host/capability-registry.d.ts +0 -10
- package/dist/host/capability-registry.d.ts.map +0 -1
- package/dist/host/capability-registry.js +0 -9
- 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 {
|
|
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 =
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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(
|
|
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 '
|
|
2
|
-
import type
|
|
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
|
+
};
|
package/src/fixers/fixers.ts
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
* @module rule-engine/fixers
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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 =
|
|
185
|
-
return rel === '' || (!rel.startsWith('..') && !
|
|
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 {
|
|
15
|
-
import {
|
|
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 ||
|
|
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 =
|
|
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.
|
package/src/host/builtins.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
@@ -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,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';
|