@gobing-ai/ts-rule-engine 0.3.19 → 0.3.21
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/dist/config/loader.d.ts +3 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +3 -4
- package/dist/engine.d.ts +8 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +6 -3
- package/dist/evaluators/config-access.d.ts +38 -0
- package/dist/evaluators/config-access.d.ts.map +1 -0
- package/dist/evaluators/config-access.js +60 -0
- package/dist/evaluators/coverage-gate-evaluator.d.ts +1 -2
- package/dist/evaluators/coverage-gate-evaluator.d.ts.map +1 -1
- package/dist/evaluators/coverage-gate-evaluator.js +7 -6
- package/dist/evaluators/file-discovery.d.ts +72 -0
- package/dist/evaluators/file-discovery.d.ts.map +1 -0
- package/dist/evaluators/file-discovery.js +63 -0
- package/dist/evaluators/file-utils.d.ts +11 -118
- package/dist/evaluators/file-utils.d.ts.map +1 -1
- package/dist/evaluators/file-utils.js +11 -177
- package/dist/evaluators/forbidden-import-evaluator.d.ts +1 -0
- package/dist/evaluators/forbidden-import-evaluator.d.ts.map +1 -1
- package/dist/evaluators/forbidden-import-evaluator.js +9 -1
- package/dist/evaluators/glob-match.d.ts +34 -0
- package/dist/evaluators/glob-match.d.ts.map +1 -0
- package/dist/evaluators/glob-match.js +78 -0
- package/dist/evaluators/import-boundary-evaluator.d.ts.map +1 -1
- package/dist/evaluators/import-boundary-evaluator.js +3 -1
- package/dist/evaluators/path-evaluator.d.ts +1 -2
- package/dist/evaluators/path-evaluator.d.ts.map +1 -1
- package/dist/evaluators/path-evaluator.js +8 -6
- package/dist/evaluators/regex-evaluator.d.ts.map +1 -1
- package/dist/evaluators/regex-evaluator.js +3 -0
- package/dist/evaluators/schema-artifact-evaluator.d.ts +1 -2
- package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -1
- package/dist/evaluators/schema-artifact-evaluator.js +8 -7
- package/dist/evaluators/secrets-scanner-evaluator.d.ts.map +1 -1
- package/dist/evaluators/secrets-scanner-evaluator.js +3 -1
- package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
- package/dist/evaluators/test-location-evaluator.js +3 -1
- package/dist/evaluators/tsdoc-export-evaluator.d.ts.map +1 -1
- package/dist/evaluators/tsdoc-export-evaluator.js +3 -1
- package/dist/fixers/fixers.d.ts +2 -4
- package/dist/fixers/fixers.d.ts.map +1 -1
- package/dist/fixers/fixers.js +9 -11
- 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 +2 -2
- package/dist/host/bundled-rules.d.ts +2 -2
- package/dist/host/bundled-rules.d.ts.map +1 -1
- package/dist/host/bundled-rules.js +24 -19
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/config/loader.ts +14 -6
- package/src/engine.ts +12 -4
- package/src/evaluators/config-access.ts +76 -0
- package/src/evaluators/coverage-gate-evaluator.ts +7 -8
- package/src/evaluators/file-discovery.ts +132 -0
- package/src/evaluators/file-utils.ts +29 -258
- package/src/evaluators/forbidden-import-evaluator.ts +10 -2
- package/src/evaluators/glob-match.ts +76 -0
- package/src/evaluators/import-boundary-evaluator.ts +3 -1
- package/src/evaluators/path-evaluator.ts +8 -7
- package/src/evaluators/regex-evaluator.ts +3 -0
- package/src/evaluators/schema-artifact-evaluator.ts +9 -8
- package/src/evaluators/secrets-scanner-evaluator.ts +3 -1
- package/src/evaluators/test-location-evaluator.ts +5 -2
- package/src/evaluators/tsdoc-export-evaluator.ts +3 -1
- package/src/fixers/fixers.ts +11 -12
- package/src/fixers/test-stub-fixer.ts +3 -3
- package/src/host/bundled-rules.ts +23 -19
- package/src/types.ts +8 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
1
2
|
import {
|
|
2
3
|
type ConstraintRule,
|
|
3
4
|
createFinding,
|
|
@@ -61,7 +62,8 @@ export class SecretsScannerEvaluator implements RuleEvaluator {
|
|
|
61
62
|
const scope = config.scope as { include?: unknown; exclude?: unknown } | undefined;
|
|
62
63
|
const include = stringArray(scope?.include) ?? rule.include;
|
|
63
64
|
const exclude = stringArray(scope?.exclude) ?? rule.exclude;
|
|
64
|
-
const
|
|
65
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
66
|
+
const files = await scanFiles({ workdir: context.workdir, include, exclude, matchMode: 'loose', fs });
|
|
65
67
|
|
|
66
68
|
const findings = [];
|
|
67
69
|
for (const { file, content } of files) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
1
2
|
import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/extension';
|
|
2
3
|
import { type TestPathResolver, TypeScriptTestPathResolver } from '../resolvers/test-path-resolver';
|
|
3
4
|
import {
|
|
5
|
+
type ConstraintFinding,
|
|
4
6
|
type ConstraintRule,
|
|
5
7
|
createFinding,
|
|
6
8
|
type RuleContext,
|
|
@@ -45,8 +47,9 @@ export class TestLocationEvaluator implements RuleEvaluator {
|
|
|
45
47
|
const forbid = config.forbid ?? [];
|
|
46
48
|
const include = rule.include ?? ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'];
|
|
47
49
|
const exclude = rule.exclude ?? [];
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
+
const findings: ConstraintFinding[] = [];
|
|
51
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
52
|
+
const allFiles = await discoverFiles({ workdir: context.workdir, fs });
|
|
50
53
|
|
|
51
54
|
const testPatterns = config.requireCorrespondingTest ? [expected] : include;
|
|
52
55
|
const testFiles = allFiles.filter((file) => testPatterns.some((pattern) => matchesGlob(file, pattern)));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
1
2
|
import {
|
|
2
3
|
type ConstraintRule,
|
|
3
4
|
createFinding,
|
|
@@ -54,7 +55,8 @@ export class TsdocExportEvaluator implements RuleEvaluator {
|
|
|
54
55
|
const exclude = rule.exclude ?? [];
|
|
55
56
|
// Single, strict glob scoping — collapses the previous double-scoping (loose
|
|
56
57
|
// discoverFiles prefilter + per-file matchesGlob) into one pass.
|
|
57
|
-
const
|
|
58
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
59
|
+
const files = await scanFiles({ workdir: context.workdir, include, exclude, matchMode: 'glob', fs });
|
|
58
60
|
|
|
59
61
|
const findings = [];
|
|
60
62
|
for (const { file, content } of files) {
|
package/src/fixers/fixers.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
createNodeFileSystem,
|
|
10
|
+
type FileSystem,
|
|
9
11
|
isAbsolutePath,
|
|
10
12
|
joinPath,
|
|
11
|
-
NodeFileSystem,
|
|
12
13
|
type ProcessExecutor,
|
|
13
14
|
relativePath,
|
|
14
15
|
resolvePath,
|
|
@@ -66,7 +67,7 @@ export async function applyFixes(
|
|
|
66
67
|
workdir: string,
|
|
67
68
|
fixes: readonly Fix[],
|
|
68
69
|
dryRun = false,
|
|
69
|
-
fs:
|
|
70
|
+
fs: FileSystem = createNodeFileSystem(),
|
|
70
71
|
): Promise<FixApplicationResult> {
|
|
71
72
|
const byFile = new Map<string, Fix[]>();
|
|
72
73
|
for (const fix of fixes) {
|
|
@@ -117,7 +118,7 @@ export async function applyFixes(
|
|
|
117
118
|
const isFullDeletion =
|
|
118
119
|
next.length === 0 && selected.applied.some((fix) => fix.start === 0 && fix.end === original.length);
|
|
119
120
|
if (isFullDeletion) {
|
|
120
|
-
await fs.
|
|
121
|
+
await fs.deleteFile(absPath);
|
|
121
122
|
} else {
|
|
122
123
|
await fs.writeFile(absPath, next);
|
|
123
124
|
}
|
|
@@ -173,10 +174,10 @@ function isValidRange(start: number, end: number, contentLength: number): boolea
|
|
|
173
174
|
* replacement string (supports `$1` / `$&` back-references).
|
|
174
175
|
*/
|
|
175
176
|
export class RegexFixerProvider implements RuleFixerProvider {
|
|
176
|
-
private readonly fs = new NodeFileSystem();
|
|
177
|
-
|
|
178
177
|
/** Produce fixes for regex-evaluator findings. */
|
|
179
178
|
async createFixes({ rule, context, findings, fix }: RuleFixerInput): Promise<Fix[]> {
|
|
179
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
180
|
+
|
|
180
181
|
if (fix.mode === 'none' || fix.replacement === undefined) return [];
|
|
181
182
|
// After the guard above, mode is guaranteed to not be 'none'.
|
|
182
183
|
const fixMode = fix.mode as Exclude<FixMode, 'none'>;
|
|
@@ -191,8 +192,8 @@ export class RegexFixerProvider implements RuleFixerProvider {
|
|
|
191
192
|
if (!finding.filePath) continue;
|
|
192
193
|
const absPath = resolveWorkdirPath(context.workdir, finding.filePath);
|
|
193
194
|
if (!isInsideWorkdir(context.workdir, absPath) || finding.line == null) continue;
|
|
194
|
-
if (!(await
|
|
195
|
-
const source = await
|
|
195
|
+
if (!(await fs.exists(absPath))) continue;
|
|
196
|
+
const source = await fs.readFile(absPath);
|
|
196
197
|
const line = getLineRange(source, finding.line);
|
|
197
198
|
if (!line) continue;
|
|
198
199
|
for (const match of source.slice(line.start, line.end).matchAll(regex)) {
|
|
@@ -220,18 +221,16 @@ export class RegexFixerProvider implements RuleFixerProvider {
|
|
|
220
221
|
* `applyFixes` interprets a zero-length result spanning the full file as a deletion.
|
|
221
222
|
*/
|
|
222
223
|
export class PathFixerProvider implements RuleFixerProvider {
|
|
223
|
-
private readonly fs = new NodeFileSystem();
|
|
224
|
-
|
|
225
224
|
/** Produce deletion fixes for path-evaluator findings. */
|
|
226
225
|
async createFixes({ rule, context, findings, fix }: RuleFixerInput): Promise<Fix[]> {
|
|
226
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
227
227
|
if (fix.mode !== 'auto' || rule.evaluator.config?.must !== 'absent') return [];
|
|
228
228
|
const fixes: Fix[] = [];
|
|
229
229
|
for (const finding of findings) {
|
|
230
230
|
if (!finding.filePath) continue;
|
|
231
231
|
const absPath = resolveWorkdirPath(context.workdir, finding.filePath);
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
const content = await this.fs.readFile(absPath);
|
|
232
|
+
if (!(await fs.exists(absPath))) continue;
|
|
233
|
+
const content = await fs.readFile(absPath);
|
|
235
234
|
fixes.push({
|
|
236
235
|
ruleId: rule.id,
|
|
237
236
|
filePath: finding.filePath,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module rule-engine/fixers/test-stub-fixer
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { isAbsolutePath, joinPath,
|
|
14
|
+
import { createNodeFileSystem, isAbsolutePath, joinPath, type ProcessExecutor } from '@gobing-ai/ts-runtime';
|
|
15
15
|
import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/extension';
|
|
16
16
|
import type { TestPathResolver } from '../resolvers/test-path-resolver';
|
|
17
17
|
import type { Fix } from '../types';
|
|
@@ -20,11 +20,11 @@ import type { RuleFixerInput, RuleFixerProvider } from './fixers';
|
|
|
20
20
|
/** File-system seam for dependency injection in tests. */
|
|
21
21
|
export interface TestStubFileSystem {
|
|
22
22
|
/** Resolve to true if the path exists on disk. */
|
|
23
|
-
exists(path: string): Promise<boolean>;
|
|
23
|
+
exists(path: string): boolean | Promise<boolean>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/** Real file-system implementation backed by the runtime FileSystem seam. */
|
|
27
|
-
export const realFileSystem: TestStubFileSystem =
|
|
27
|
+
export const realFileSystem: TestStubFileSystem = createNodeFileSystem();
|
|
28
28
|
|
|
29
29
|
/** Dependencies injected into {@link TestStubFixer}. */
|
|
30
30
|
export interface TestStubFixerDeps {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createNodeFileSystem, dirnamePath, type FileSystem, joinPath } from '@gobing-ai/ts-runtime';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Directory name holding the rule presets and category folders bundled with this
|
|
@@ -9,7 +9,8 @@ const BUNDLED_RULES_DIR = 'rules';
|
|
|
9
9
|
|
|
10
10
|
/** Memoized result so the upward filesystem walk runs at most once per process. */
|
|
11
11
|
let cachedRoot: string | null | undefined;
|
|
12
|
-
|
|
12
|
+
/** In-flight resolution to prevent racing concurrent calls. */
|
|
13
|
+
let resolving: Promise<string | null> | undefined;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Resolve the absolute path to the rule presets bundled with
|
|
@@ -26,26 +27,28 @@ const defaultFs = new NodeSyncFileSystem();
|
|
|
26
27
|
* which makes it robust to the build's `src`→`dist` layout shift. Returns `null`
|
|
27
28
|
* if the directory is absent (e.g. a partial install that excluded the assets).
|
|
28
29
|
*/
|
|
29
|
-
export function bundledRulesRoot(): string | null {
|
|
30
|
-
|
|
30
|
+
export async function bundledRulesRoot(): Promise<string | null> {
|
|
31
|
+
if (cachedRoot !== undefined) return cachedRoot;
|
|
32
|
+
if (resolving !== undefined) return resolving;
|
|
33
|
+
resolving = bundledRulesRootWithFs(createNodeFileSystem());
|
|
34
|
+
cachedRoot = await resolving;
|
|
35
|
+
resolving = undefined;
|
|
36
|
+
return cachedRoot;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
function bundledRulesRootWithFs(fs:
|
|
34
|
-
if (cachedRoot !== undefined) return cachedRoot;
|
|
39
|
+
async function bundledRulesRootWithFs(fs: FileSystem): Promise<string | null> {
|
|
35
40
|
let dir = import.meta.dirname;
|
|
36
|
-
// Walk to filesystem root at most; the package root is only a few levels up.
|
|
37
41
|
while (true) {
|
|
38
42
|
const candidate = joinPath(dir, BUNDLED_RULES_DIR);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return
|
|
43
|
+
const st = await fs.stat(candidate);
|
|
44
|
+
if (st?.isDirectory() === true) {
|
|
45
|
+
return candidate;
|
|
42
46
|
}
|
|
43
47
|
const parent = dirnamePath(dir);
|
|
44
48
|
if (parent === dir) break;
|
|
45
49
|
dir = parent;
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
return cachedRoot;
|
|
51
|
+
return null;
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/**
|
|
@@ -56,20 +59,21 @@ function bundledRulesRootWithFs(fs: SyncFileSystem): string | null {
|
|
|
56
59
|
* (e.g. a per-user global rules directory) on first run. Returns an empty array
|
|
57
60
|
* when no bundled directory is present.
|
|
58
61
|
*/
|
|
59
|
-
export function listBundledRuleFiles(): string[] {
|
|
60
|
-
const root = bundledRulesRoot();
|
|
62
|
+
export async function listBundledRuleFiles(): Promise<string[]> {
|
|
63
|
+
const root = await bundledRulesRoot();
|
|
61
64
|
if (root === null) return [];
|
|
62
|
-
return walk(
|
|
65
|
+
return (await walk(createNodeFileSystem(), root, '')).sort();
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
/** Recursively collect YAML/JSON files under `dir`, returning paths relative to the walk origin. */
|
|
66
|
-
function walk(fs:
|
|
69
|
+
async function walk(fs: FileSystem, dir: string, relPrefix: string): Promise<string[]> {
|
|
67
70
|
const acc: string[] = [];
|
|
68
|
-
for (const entry of fs.readDir(dir)) {
|
|
71
|
+
for (const entry of await fs.readDir(dir)) {
|
|
69
72
|
const abs = joinPath(dir, entry);
|
|
70
73
|
const rel = relPrefix.length > 0 ? `${relPrefix}/${entry}` : entry;
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const st = await fs.stat(abs);
|
|
75
|
+
if (st?.isDirectory() === true) {
|
|
76
|
+
acc.push(...(await walk(fs, abs, rel)));
|
|
73
77
|
} else if (/\.(ya?ml|json)$/i.test(entry)) {
|
|
74
78
|
acc.push(rel);
|
|
75
79
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FileSystem } from '@gobing-ai/ts-runtime';
|
|
1
2
|
import { assertRelativeExtensionPath } from '@gobing-ai/ts-runtime/extension';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
|
|
@@ -156,6 +157,13 @@ export interface RuleContext {
|
|
|
156
157
|
workdir: string;
|
|
157
158
|
/** Rule being evaluated. */
|
|
158
159
|
rule: ConstraintRule;
|
|
160
|
+
/**
|
|
161
|
+
* Filesystem port (ADR-011 union-return {@link FileSystem}); defaults to
|
|
162
|
+
* `createNodeFileSystem()` when constructed by `RuleEngine`. Evaluators and
|
|
163
|
+
* fixer providers MUST read files through this port rather than constructing
|
|
164
|
+
* their own filesystem instance, so a host can inject a virtual fs in tests.
|
|
165
|
+
*/
|
|
166
|
+
fileSystem?: FileSystem;
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
/** Evaluator implementation contract. */
|