@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
package/dist/fixers/fixers.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module rule-engine/fixers
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { type FileSystem, type ProcessExecutor } from '@gobing-ai/ts-runtime';
|
|
8
8
|
import type { RuleEngineHost } from '../host/rule-engine-host';
|
|
9
9
|
import type { Fix, FixMode, RuleFixerInput, RuleFixerProvider } from '../types';
|
|
10
10
|
export type { EffectiveFix, RuleFixerInput, RuleFixerProvider } from '../types';
|
|
@@ -26,7 +26,7 @@ export declare function registerBuiltinFixers(host: RuleEngineHost, exec?: Proce
|
|
|
26
26
|
/** Resolve a workdir-relative or absolute path to an absolute path. */
|
|
27
27
|
export declare function resolveWorkdirPath(workdir: string, filePath: string): string;
|
|
28
28
|
/** Apply byte-range fixes to files, optionally returning a dry-run diff only. */
|
|
29
|
-
export declare function applyFixes(workdir: string, fixes: readonly Fix[], dryRun?: boolean, fs?:
|
|
29
|
+
export declare function applyFixes(workdir: string, fixes: readonly Fix[], dryRun?: boolean, fs?: FileSystem): Promise<FixApplicationResult>;
|
|
30
30
|
/** Create a coarse unified diff string for dry-run CLI output. */
|
|
31
31
|
export declare function createUnifiedDiff(filePath: string, before: string, after: string): string;
|
|
32
32
|
/**
|
|
@@ -37,7 +37,6 @@ export declare function createUnifiedDiff(filePath: string, before: string, afte
|
|
|
37
37
|
* replacement string (supports `$1` / `$&` back-references).
|
|
38
38
|
*/
|
|
39
39
|
export declare class RegexFixerProvider implements RuleFixerProvider {
|
|
40
|
-
private readonly fs;
|
|
41
40
|
/** Produce fixes for regex-evaluator findings. */
|
|
42
41
|
createFixes({ rule, context, findings, fix }: RuleFixerInput): Promise<Fix[]>;
|
|
43
42
|
}
|
|
@@ -49,7 +48,6 @@ export declare class RegexFixerProvider implements RuleFixerProvider {
|
|
|
49
48
|
* `applyFixes` interprets a zero-length result spanning the full file as a deletion.
|
|
50
49
|
*/
|
|
51
50
|
export declare class PathFixerProvider implements RuleFixerProvider {
|
|
52
|
-
private readonly fs;
|
|
53
51
|
/** Produce deletion fixes for path-evaluator findings. */
|
|
54
52
|
createFixes({ rule, context, findings, fix }: RuleFixerInput): Promise<Fix[]>;
|
|
55
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixers.d.ts","sourceRoot":"","sources":["../../src/fixers/fixers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"fixers.d.ts","sourceRoot":"","sources":["../../src/fixers/fixers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEH,KAAK,UAAU,EAGf,KAAK,eAAe,EAGvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAkB,GAAG,EAAE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAIhG,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAEhF,6EAA6E;AAC7E,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAIjD,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACjC,0CAA0C;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,kCAAkC;IAClC,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB;AAED,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,IAAI,CAcxF;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,SAAS,GAAG,EAAE,EACrB,MAAM,UAAQ,EACd,EAAE,GAAE,UAAmC,GACxC,OAAO,CAAC,oBAAoB,CAAC,CA0D/B;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOzF;AA8BD;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,iBAAiB;IACxD,kDAAkD;IAC5C,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAoCtF;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,iBAAiB;IACvD,0DAA0D;IACpD,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAoBtF"}
|
package/dist/fixers/fixers.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module rule-engine/fixers
|
|
6
6
|
*/
|
|
7
|
-
import { isAbsolutePath, joinPath,
|
|
7
|
+
import { createNodeFileSystem, isAbsolutePath, joinPath, relativePath, resolvePath, } from '@gobing-ai/ts-runtime';
|
|
8
8
|
import { TestStubFixer } from './test-stub-fixer.js';
|
|
9
9
|
/** Numeric ordering for fix authority; higher means more write authority. */
|
|
10
10
|
export const FIX_MODE_RANK = {
|
|
@@ -29,7 +29,7 @@ export function resolveWorkdirPath(workdir, filePath) {
|
|
|
29
29
|
return isAbsolutePath(filePath) ? filePath : joinPath(workdir, filePath);
|
|
30
30
|
}
|
|
31
31
|
/** Apply byte-range fixes to files, optionally returning a dry-run diff only. */
|
|
32
|
-
export async function applyFixes(workdir, fixes, dryRun = false, fs =
|
|
32
|
+
export async function applyFixes(workdir, fixes, dryRun = false, fs = createNodeFileSystem()) {
|
|
33
33
|
const byFile = new Map();
|
|
34
34
|
for (const fix of fixes) {
|
|
35
35
|
const list = byFile.get(fix.filePath) ?? [];
|
|
@@ -71,7 +71,7 @@ export async function applyFixes(workdir, fixes, dryRun = false, fs = new NodeFi
|
|
|
71
71
|
if (!dryRun) {
|
|
72
72
|
const isFullDeletion = next.length === 0 && selected.applied.some((fix) => fix.start === 0 && fix.end === original.length);
|
|
73
73
|
if (isFullDeletion) {
|
|
74
|
-
await fs.
|
|
74
|
+
await fs.deleteFile(absPath);
|
|
75
75
|
}
|
|
76
76
|
else {
|
|
77
77
|
await fs.writeFile(absPath, next);
|
|
@@ -122,9 +122,9 @@ function isValidRange(start, end, contentLength) {
|
|
|
122
122
|
* replacement string (supports `$1` / `$&` back-references).
|
|
123
123
|
*/
|
|
124
124
|
export class RegexFixerProvider {
|
|
125
|
-
fs = new NodeFileSystem();
|
|
126
125
|
/** Produce fixes for regex-evaluator findings. */
|
|
127
126
|
async createFixes({ rule, context, findings, fix }) {
|
|
127
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
128
128
|
if (fix.mode === 'none' || fix.replacement === undefined)
|
|
129
129
|
return [];
|
|
130
130
|
// After the guard above, mode is guaranteed to not be 'none'.
|
|
@@ -142,9 +142,9 @@ export class RegexFixerProvider {
|
|
|
142
142
|
const absPath = resolveWorkdirPath(context.workdir, finding.filePath);
|
|
143
143
|
if (!isInsideWorkdir(context.workdir, absPath) || finding.line == null)
|
|
144
144
|
continue;
|
|
145
|
-
if (!(await
|
|
145
|
+
if (!(await fs.exists(absPath)))
|
|
146
146
|
continue;
|
|
147
|
-
const source = await
|
|
147
|
+
const source = await fs.readFile(absPath);
|
|
148
148
|
const line = getLineRange(source, finding.line);
|
|
149
149
|
if (!line)
|
|
150
150
|
continue;
|
|
@@ -173,9 +173,9 @@ export class RegexFixerProvider {
|
|
|
173
173
|
* `applyFixes` interprets a zero-length result spanning the full file as a deletion.
|
|
174
174
|
*/
|
|
175
175
|
export class PathFixerProvider {
|
|
176
|
-
fs = new NodeFileSystem();
|
|
177
176
|
/** Produce deletion fixes for path-evaluator findings. */
|
|
178
177
|
async createFixes({ rule, context, findings, fix }) {
|
|
178
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
179
179
|
if (fix.mode !== 'auto' || rule.evaluator.config?.must !== 'absent')
|
|
180
180
|
return [];
|
|
181
181
|
const fixes = [];
|
|
@@ -183,11 +183,9 @@ export class PathFixerProvider {
|
|
|
183
183
|
if (!finding.filePath)
|
|
184
184
|
continue;
|
|
185
185
|
const absPath = resolveWorkdirPath(context.workdir, finding.filePath);
|
|
186
|
-
if (!
|
|
186
|
+
if (!(await fs.exists(absPath)))
|
|
187
187
|
continue;
|
|
188
|
-
|
|
189
|
-
continue;
|
|
190
|
-
const content = await this.fs.readFile(absPath);
|
|
188
|
+
const content = await fs.readFile(absPath);
|
|
191
189
|
fixes.push({
|
|
192
190
|
ruleId: rule.id,
|
|
193
191
|
filePath: finding.filePath,
|
|
@@ -18,7 +18,7 @@ import type { RuleFixerInput, RuleFixerProvider } from './fixers';
|
|
|
18
18
|
/** File-system seam for dependency injection in tests. */
|
|
19
19
|
export interface TestStubFileSystem {
|
|
20
20
|
/** Resolve to true if the path exists on disk. */
|
|
21
|
-
exists(path: string): Promise<boolean>;
|
|
21
|
+
exists(path: string): boolean | Promise<boolean>;
|
|
22
22
|
}
|
|
23
23
|
/** Real file-system implementation backed by the runtime FileSystem seam. */
|
|
24
24
|
export declare const realFileSystem: TestStubFileSystem;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-stub-fixer.d.ts","sourceRoot":"","sources":["../../src/fixers/test-stub-fixer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"test-stub-fixer.d.ts","sourceRoot":"","sources":["../../src/fixers/test-stub-fixer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAkD,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElE,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IAC/B,kDAAkD;IAClD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpD;AAED,6EAA6E;AAC7E,eAAO,MAAM,cAAc,EAAE,kBAA2C,CAAC;AAEzE,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAC9B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;IACzD,+DAA+D;IAC/D,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,iDAAiD;IACjD,QAAQ,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC;CAC5C;AAUD;;;;;;;GAOG;AACH,qBAAa,aAAc,YAAW,iBAAiB;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;IACjE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAqB;gBAE5B,IAAI,EAAE,iBAAiB;IAOnC,wDAAwD;IAClD,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAkD3D"}
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @module rule-engine/fixers/test-stub-fixer
|
|
12
12
|
*/
|
|
13
|
-
import { isAbsolutePath, joinPath
|
|
13
|
+
import { createNodeFileSystem, isAbsolutePath, joinPath } from '@gobing-ai/ts-runtime';
|
|
14
14
|
/** Real file-system implementation backed by the runtime FileSystem seam. */
|
|
15
|
-
export const realFileSystem =
|
|
15
|
+
export const realFileSystem = createNodeFileSystem();
|
|
16
16
|
/** Normalize a finding path to a repository-relative POSIX path, or return null when invalid. */
|
|
17
17
|
function normalizeFindingPath(filePath) {
|
|
18
18
|
if (!filePath || isAbsolutePath(filePath))
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* which makes it robust to the build's `src`→`dist` layout shift. Returns `null`
|
|
14
14
|
* if the directory is absent (e.g. a partial install that excluded the assets).
|
|
15
15
|
*/
|
|
16
|
-
export declare function bundledRulesRoot(): string | null
|
|
16
|
+
export declare function bundledRulesRoot(): Promise<string | null>;
|
|
17
17
|
/**
|
|
18
18
|
* List the relative paths of every bundled rule asset (presets and category rule
|
|
19
19
|
* files), each as a `/`-joined path relative to {@link bundledRulesRoot}.
|
|
@@ -22,5 +22,5 @@ export declare function bundledRulesRoot(): string | null;
|
|
|
22
22
|
* (e.g. a per-user global rules directory) on first run. Returns an empty array
|
|
23
23
|
* when no bundled directory is present.
|
|
24
24
|
*/
|
|
25
|
-
export declare function listBundledRuleFiles(): string[]
|
|
25
|
+
export declare function listBundledRuleFiles(): Promise<string[]>;
|
|
26
26
|
//# sourceMappingURL=bundled-rules.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundled-rules.d.ts","sourceRoot":"","sources":["../../src/host/bundled-rules.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bundled-rules.d.ts","sourceRoot":"","sources":["../../src/host/bundled-rules.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO/D;AAiBD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAI9D"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dirnamePath, joinPath
|
|
1
|
+
import { createNodeFileSystem, dirnamePath, joinPath } from '@gobing-ai/ts-runtime';
|
|
2
2
|
/**
|
|
3
3
|
* Directory name holding the rule presets and category folders bundled with this
|
|
4
4
|
* package. Lives at the package root (sibling to `dist/`), shipped via the
|
|
@@ -7,7 +7,8 @@ import { dirnamePath, joinPath, NodeSyncFileSystem } from '@gobing-ai/ts-runtime
|
|
|
7
7
|
const BUNDLED_RULES_DIR = 'rules';
|
|
8
8
|
/** Memoized result so the upward filesystem walk runs at most once per process. */
|
|
9
9
|
let cachedRoot;
|
|
10
|
-
|
|
10
|
+
/** In-flight resolution to prevent racing concurrent calls. */
|
|
11
|
+
let resolving;
|
|
11
12
|
/**
|
|
12
13
|
* Resolve the absolute path to the rule presets bundled with
|
|
13
14
|
* `@gobing-ai/ts-rule-engine`.
|
|
@@ -23,27 +24,30 @@ const defaultFs = new NodeSyncFileSystem();
|
|
|
23
24
|
* which makes it robust to the build's `src`→`dist` layout shift. Returns `null`
|
|
24
25
|
* if the directory is absent (e.g. a partial install that excluded the assets).
|
|
25
26
|
*/
|
|
26
|
-
export function bundledRulesRoot() {
|
|
27
|
-
return bundledRulesRootWithFs(defaultFs);
|
|
28
|
-
}
|
|
29
|
-
function bundledRulesRootWithFs(fs) {
|
|
27
|
+
export async function bundledRulesRoot() {
|
|
30
28
|
if (cachedRoot !== undefined)
|
|
31
29
|
return cachedRoot;
|
|
30
|
+
if (resolving !== undefined)
|
|
31
|
+
return resolving;
|
|
32
|
+
resolving = bundledRulesRootWithFs(createNodeFileSystem());
|
|
33
|
+
cachedRoot = await resolving;
|
|
34
|
+
resolving = undefined;
|
|
35
|
+
return cachedRoot;
|
|
36
|
+
}
|
|
37
|
+
async function bundledRulesRootWithFs(fs) {
|
|
32
38
|
let dir = import.meta.dirname;
|
|
33
|
-
// Walk to filesystem root at most; the package root is only a few levels up.
|
|
34
39
|
while (true) {
|
|
35
40
|
const candidate = joinPath(dir, BUNDLED_RULES_DIR);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
41
|
+
const st = await fs.stat(candidate);
|
|
42
|
+
if (st?.isDirectory() === true) {
|
|
43
|
+
return candidate;
|
|
39
44
|
}
|
|
40
45
|
const parent = dirnamePath(dir);
|
|
41
46
|
if (parent === dir)
|
|
42
47
|
break;
|
|
43
48
|
dir = parent;
|
|
44
49
|
}
|
|
45
|
-
|
|
46
|
-
return cachedRoot;
|
|
50
|
+
return null;
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
53
|
* List the relative paths of every bundled rule asset (presets and category rule
|
|
@@ -53,20 +57,21 @@ function bundledRulesRootWithFs(fs) {
|
|
|
53
57
|
* (e.g. a per-user global rules directory) on first run. Returns an empty array
|
|
54
58
|
* when no bundled directory is present.
|
|
55
59
|
*/
|
|
56
|
-
export function listBundledRuleFiles() {
|
|
57
|
-
const root = bundledRulesRoot();
|
|
60
|
+
export async function listBundledRuleFiles() {
|
|
61
|
+
const root = await bundledRulesRoot();
|
|
58
62
|
if (root === null)
|
|
59
63
|
return [];
|
|
60
|
-
return walk(
|
|
64
|
+
return (await walk(createNodeFileSystem(), root, '')).sort();
|
|
61
65
|
}
|
|
62
66
|
/** Recursively collect YAML/JSON files under `dir`, returning paths relative to the walk origin. */
|
|
63
|
-
function walk(fs, dir, relPrefix) {
|
|
67
|
+
async function walk(fs, dir, relPrefix) {
|
|
64
68
|
const acc = [];
|
|
65
|
-
for (const entry of fs.readDir(dir)) {
|
|
69
|
+
for (const entry of await fs.readDir(dir)) {
|
|
66
70
|
const abs = joinPath(dir, entry);
|
|
67
71
|
const rel = relPrefix.length > 0 ? `${relPrefix}/${entry}` : entry;
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
const st = await fs.stat(abs);
|
|
73
|
+
if (st?.isDirectory() === true) {
|
|
74
|
+
acc.push(...(await walk(fs, abs, rel)));
|
|
70
75
|
}
|
|
71
76
|
else if (/\.(ya?ml|json)$/i.test(entry)) {
|
|
72
77
|
acc.push(rel);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FileSystem } from '@gobing-ai/ts-runtime';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/** Finding severity emitted by the rule engine. */
|
|
3
4
|
export type RuleSeverity = 'error' | 'warning' | 'info';
|
|
@@ -141,6 +142,13 @@ export interface RuleContext {
|
|
|
141
142
|
workdir: string;
|
|
142
143
|
/** Rule being evaluated. */
|
|
143
144
|
rule: ConstraintRule;
|
|
145
|
+
/**
|
|
146
|
+
* Filesystem port (ADR-011 union-return {@link FileSystem}); defaults to
|
|
147
|
+
* `createNodeFileSystem()` when constructed by `RuleEngine`. Evaluators and
|
|
148
|
+
* fixer providers MUST read files through this port rather than constructing
|
|
149
|
+
* their own filesystem instance, so a host can inject a virtual fs in tests.
|
|
150
|
+
*/
|
|
151
|
+
fileSystem?: FileSystem;
|
|
144
152
|
}
|
|
145
153
|
/** Evaluator implementation contract. */
|
|
146
154
|
export interface RuleEvaluator {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-rule-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-rule-engine-v<version> && git push --tags' && exit 1"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@gobing-ai/ts-ai-runner": "^0.3.
|
|
53
|
-
"@gobing-ai/ts-infra": "^0.3.
|
|
54
|
-
"@gobing-ai/ts-runtime": "^0.3.
|
|
55
|
-
"@gobing-ai/ts-db": "^0.3.
|
|
52
|
+
"@gobing-ai/ts-ai-runner": "^0.3.21",
|
|
53
|
+
"@gobing-ai/ts-infra": "^0.3.21",
|
|
54
|
+
"@gobing-ai/ts-runtime": "^0.3.21",
|
|
55
|
+
"@gobing-ai/ts-db": "^0.3.21",
|
|
56
56
|
"yaml": "^2.7.0",
|
|
57
57
|
"zod": "^4.1.0"
|
|
58
58
|
},
|
package/src/config/loader.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
basenamePath,
|
|
3
|
+
createNodeFileSystem,
|
|
3
4
|
dirnamePath,
|
|
5
|
+
type FileSystem,
|
|
4
6
|
joinPath,
|
|
5
7
|
loadStructuredConfig,
|
|
6
|
-
NodeFileSystem,
|
|
7
8
|
relativePath,
|
|
8
9
|
resolvePath,
|
|
9
10
|
SEP,
|
|
@@ -33,6 +34,8 @@ export interface RuleLoaderOptions {
|
|
|
33
34
|
validateSchema?: boolean;
|
|
34
35
|
/** Optional fetch implementation for remote HTTP(S) schema refs. */
|
|
35
36
|
fetch?: (input: string) => Promise<Response>;
|
|
37
|
+
/** Filesystem for reading roots. Defaults to `createNodeFileSystem()`; inject to test without disk. */
|
|
38
|
+
fileSystem?: FileSystem;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/** Options for loading a single rule file directly from disk. */
|
|
@@ -67,7 +70,10 @@ interface MergedRoots {
|
|
|
67
70
|
* the rest of a preset's categories from the lower-priority roots.
|
|
68
71
|
*/
|
|
69
72
|
export async function loadPreset(name: string, options: RuleLoaderOptions): Promise<LoadedPreset> {
|
|
70
|
-
const merged = await buildMergedRoots(
|
|
73
|
+
const merged = await buildMergedRoots(
|
|
74
|
+
options.roots.map((root) => resolvePath(root)),
|
|
75
|
+
options.fileSystem,
|
|
76
|
+
);
|
|
71
77
|
const presetPath = findMergedPreset(merged, name);
|
|
72
78
|
if (presetPath === null) return { rules: [], extensions: [] };
|
|
73
79
|
const preset = PresetDefinitionSchema.parse(await readStructuredFile(presetPath, options)) as PresetDefinition;
|
|
@@ -205,8 +211,10 @@ async function loadPresetEntry(
|
|
|
205
211
|
* Roots are processed in the order supplied (highest priority first). The first
|
|
206
212
|
* root to provide a given relative path owns that file; later roots are shadowed.
|
|
207
213
|
*/
|
|
208
|
-
async function buildMergedRoots(
|
|
209
|
-
|
|
214
|
+
async function buildMergedRoots(
|
|
215
|
+
roots: readonly string[],
|
|
216
|
+
fs: FileSystem = createNodeFileSystem(),
|
|
217
|
+
): Promise<MergedRoots> {
|
|
210
218
|
const files = new Map<string, string>();
|
|
211
219
|
const categories = new Set<string>();
|
|
212
220
|
for (const root of roots) {
|
|
@@ -258,7 +266,7 @@ function mergedFilesInCategory(merged: MergedRoots, category: string): string[]
|
|
|
258
266
|
}
|
|
259
267
|
|
|
260
268
|
/** Recursively collect YAML/JSON file paths under a directory, skipping root-level `presets/`. */
|
|
261
|
-
async function walkYamlFiles(fs:
|
|
269
|
+
async function walkYamlFiles(fs: FileSystem, dir: string, depth = 0): Promise<string[]> {
|
|
262
270
|
const stat = await fs.stat(dir);
|
|
263
271
|
if (stat === null || !stat.isDirectory()) return [];
|
|
264
272
|
const acc: string[] = [];
|
|
@@ -276,7 +284,7 @@ async function walkYamlFiles(fs: NodeFileSystem, dir: string, depth = 0): Promis
|
|
|
276
284
|
}
|
|
277
285
|
|
|
278
286
|
/** List immediate subdirectory names of a root (excluding `presets`). */
|
|
279
|
-
async function listImmediateDirs(fs:
|
|
287
|
+
async function listImmediateDirs(fs: FileSystem, dir: string): Promise<string[]> {
|
|
280
288
|
const stat = await fs.stat(dir);
|
|
281
289
|
if (stat === null || !stat.isDirectory()) return [];
|
|
282
290
|
const dirs: string[] = [];
|
package/src/engine.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EventBus, Logger } from '@gobing-ai/ts-infra';
|
|
2
2
|
import { addSpanEvent, getLogger, traceAsync } from '@gobing-ai/ts-infra';
|
|
3
|
-
import type
|
|
3
|
+
import { createNodeFileSystem, type FileSystem, type ProcessExecutor } from '@gobing-ai/ts-runtime';
|
|
4
4
|
import type { RuleEngineEvents } from './events';
|
|
5
5
|
import {
|
|
6
6
|
applyFixes as applyFixesImpl,
|
|
@@ -19,6 +19,12 @@ import { createFinding, SEVERITY_RANK } from './types';
|
|
|
19
19
|
export interface RuleEngineOptions {
|
|
20
20
|
/** Optional executor supplied to process-backed evaluators. */
|
|
21
21
|
processExecutor?: ProcessExecutor;
|
|
22
|
+
/**
|
|
23
|
+
* Filesystem port (ADR-011 union-return {@link FileSystem}); defaults to
|
|
24
|
+
* `createNodeFileSystem()`. Threaded through {@link RuleContext} to every
|
|
25
|
+
* evaluator and fixer provider so hosts can inject a virtual fs in tests.
|
|
26
|
+
*/
|
|
27
|
+
fileSystem?: FileSystem;
|
|
22
28
|
/** Optional preconfigured host. */
|
|
23
29
|
host?: RuleEngineHost;
|
|
24
30
|
/** Optional event bus for structured run observability (R-A4). */
|
|
@@ -43,6 +49,7 @@ export class RuleEngine {
|
|
|
43
49
|
private readonly persistence: RulePersistenceAdapter | undefined;
|
|
44
50
|
private readonly runId: string;
|
|
45
51
|
private readonly runMeta: Record<string, unknown> | undefined;
|
|
52
|
+
private readonly fileSystem: FileSystem;
|
|
46
53
|
|
|
47
54
|
constructor(options: RuleEngineOptions = {}) {
|
|
48
55
|
this.host = options.host ?? new RuleEngineHost();
|
|
@@ -53,6 +60,7 @@ export class RuleEngine {
|
|
|
53
60
|
this.persistence = options.persistence;
|
|
54
61
|
this.runId = options.runId ?? crypto.randomUUID();
|
|
55
62
|
this.runMeta = options.runMeta;
|
|
63
|
+
this.fileSystem = options.fileSystem ?? createNodeFileSystem();
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/** Register or replace an evaluator. */
|
|
@@ -155,7 +163,7 @@ export class RuleEngine {
|
|
|
155
163
|
try {
|
|
156
164
|
const result = await this.host.evaluators
|
|
157
165
|
.get(rule.evaluator.type)
|
|
158
|
-
.evaluate(rule, { rule, workdir });
|
|
166
|
+
.evaluate(rule, { rule, workdir, fileSystem: this.fileSystem });
|
|
159
167
|
ruleFindings = result.findings;
|
|
160
168
|
ruleEvalFixes = result.fixes;
|
|
161
169
|
} catch (error) {
|
|
@@ -210,7 +218,7 @@ export class RuleEngine {
|
|
|
210
218
|
};
|
|
211
219
|
const providerFixes = await provider.createFixes({
|
|
212
220
|
rule,
|
|
213
|
-
context: { rule, workdir },
|
|
221
|
+
context: { rule, workdir, fileSystem: this.fileSystem },
|
|
214
222
|
findings: ruleFindings,
|
|
215
223
|
fix: effectiveFix,
|
|
216
224
|
});
|
|
@@ -272,7 +280,7 @@ export class RuleEngine {
|
|
|
272
280
|
* @returns Application details and optional diff.
|
|
273
281
|
*/
|
|
274
282
|
async applyFixes(workdir: string, fixes: readonly Fix[], dryRun = false): Promise<FixApplicationResult> {
|
|
275
|
-
return applyFixesImpl(workdir, fixes, dryRun);
|
|
283
|
+
return applyFixesImpl(workdir, fixes, dryRun, this.fileSystem);
|
|
276
284
|
}
|
|
277
285
|
}
|
|
278
286
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed accessors for a rule's `evaluator.config` bag and inline-flag parsing.
|
|
3
|
+
*
|
|
4
|
+
* Required keys fail loudly (throw, naming the evaluator) so misconfigured rules
|
|
5
|
+
* surface at evaluation time rather than silently scanning the wrong set.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Return the value as a `string[]` when every item is a string, otherwise undefined. */
|
|
9
|
+
export function stringArray(value: unknown): string[] | undefined {
|
|
10
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? (value as string[]) : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Optional accessor context — names the evaluator in "required config" errors for rule authors. */
|
|
14
|
+
export interface ConfigAccessorOptions {
|
|
15
|
+
/** Evaluator name surfaced in the error, e.g. `"regex evaluator requires string config \"pattern\""`. */
|
|
16
|
+
evaluator?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requiredConfigError(kind: string, key: string, evaluator?: string): Error {
|
|
20
|
+
const who = evaluator !== undefined ? `${evaluator} evaluator` : 'evaluator';
|
|
21
|
+
return new Error(`${who} requires ${kind} config "${key}"`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read a string entry from a rule's evaluator config. Returns the value when it is a string,
|
|
26
|
+
* the `fallback` when one is supplied, otherwise throws — so required keys fail loudly.
|
|
27
|
+
*/
|
|
28
|
+
export function configString(
|
|
29
|
+
config: Record<string, unknown>,
|
|
30
|
+
key: string,
|
|
31
|
+
fallback?: string,
|
|
32
|
+
options: ConfigAccessorOptions = {},
|
|
33
|
+
): string {
|
|
34
|
+
const value = config[key];
|
|
35
|
+
if (typeof value === 'string') return value;
|
|
36
|
+
if (fallback !== undefined) return fallback;
|
|
37
|
+
throw requiredConfigError('string', key, options.evaluator);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read a string-array entry from a rule's evaluator config. A bare string is coerced to a
|
|
42
|
+
* single-element array. Returns the `fallback` when one is supplied and the value is absent;
|
|
43
|
+
* otherwise throws — so required keys fail loudly.
|
|
44
|
+
*/
|
|
45
|
+
export function configArray(
|
|
46
|
+
config: Record<string, unknown>,
|
|
47
|
+
key: string,
|
|
48
|
+
fallback?: string[],
|
|
49
|
+
options: ConfigAccessorOptions = {},
|
|
50
|
+
): string[] {
|
|
51
|
+
const value = config[key];
|
|
52
|
+
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) return value as string[];
|
|
53
|
+
if (typeof value === 'string') return [value];
|
|
54
|
+
if (fallback !== undefined) return fallback;
|
|
55
|
+
throw requiredConfigError('string[]', key, options.evaluator);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Read a finite-number entry from a rule's evaluator config, falling back when absent or invalid. */
|
|
59
|
+
export function configNumber(config: Record<string, unknown>, key: string, fallback: number): number {
|
|
60
|
+
const value = config[key];
|
|
61
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Split a leading ripgrep/PCRE-style `(?flags)` inline group off a regex source.
|
|
66
|
+
*
|
|
67
|
+
* Returns the JS-relevant flags found in the group (filtered to `imsu`) and the
|
|
68
|
+
* remaining source with the group removed. When no leading group is present, returns
|
|
69
|
+
* empty flags and the source unchanged. Shared by evaluators that accept inline flags.
|
|
70
|
+
*/
|
|
71
|
+
export function parseInlineFlags(source: string): { flags: string; rest: string } {
|
|
72
|
+
const match = /^\(\?([a-z]+)\)/.exec(source);
|
|
73
|
+
if (!match) return { flags: '', rest: source };
|
|
74
|
+
const flags = [...(match[1] ?? '')].filter((flag) => 'imsu'.includes(flag)).join('');
|
|
75
|
+
return { flags, rest: source.slice(match[0].length) };
|
|
76
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createNodeFileSystem, isAbsolutePath, relativePath, resolvePath } from '@gobing-ai/ts-runtime';
|
|
2
2
|
import {
|
|
3
3
|
type ConstraintRule,
|
|
4
4
|
createFinding,
|
|
@@ -44,20 +44,20 @@ interface CoverageGateConfig {
|
|
|
44
44
|
* error, so the gate degrades gracefully when coverage was not generated.
|
|
45
45
|
*/
|
|
46
46
|
export class CoverageGateEvaluator implements RuleEvaluator {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
/** Evaluate per-file coverage against the configured threshold. */
|
|
48
|
+
// Explicit constructor avoids a V8 function-coverage phantom (synthesized default ctor).
|
|
49
49
|
constructor() {
|
|
50
|
-
|
|
50
|
+
// no-op; all state comes from evaluate() context
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** Evaluate per-file coverage against the configured threshold. */
|
|
54
53
|
async evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult> {
|
|
54
|
+
const fs = context.fileSystem ?? createNodeFileSystem();
|
|
55
55
|
const config = (rule.evaluator.config ?? {}) as CoverageGateConfig;
|
|
56
56
|
const lcovPath = config.lcovPath
|
|
57
57
|
? resolvePath(context.workdir, config.lcovPath)
|
|
58
58
|
: resolvePath(context.workdir, 'coverage', 'lcov.info');
|
|
59
59
|
|
|
60
|
-
if (!(await
|
|
60
|
+
if (!(await fs.exists(lcovPath))) {
|
|
61
61
|
return {
|
|
62
62
|
findings: [
|
|
63
63
|
createFinding(rule, 'No lcov file found — coverage not generated. Skipping gate.', lcovPath, {
|
|
@@ -67,8 +67,7 @@ export class CoverageGateEvaluator implements RuleEvaluator {
|
|
|
67
67
|
fixes: [],
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
const coverage = parseLcov(await this.fs.readFile(lcovPath));
|
|
70
|
+
const coverage = parseLcov(await fs.readFile(lcovPath));
|
|
72
71
|
const threshold = config.threshold ?? 90;
|
|
73
72
|
const exemptions = new Map((config.exemptions ?? []).map((entry) => [entry.path, entry]));
|
|
74
73
|
const findings = [];
|