@gobing-ai/ts-rule-engine 0.3.20 → 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.
Files changed (71) hide show
  1. package/dist/config/loader.d.ts +3 -0
  2. package/dist/config/loader.d.ts.map +1 -1
  3. package/dist/config/loader.js +3 -4
  4. package/dist/engine.d.ts +8 -1
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +6 -3
  7. package/dist/evaluators/config-access.d.ts +38 -0
  8. package/dist/evaluators/config-access.d.ts.map +1 -0
  9. package/dist/evaluators/config-access.js +60 -0
  10. package/dist/evaluators/coverage-gate-evaluator.d.ts +1 -2
  11. package/dist/evaluators/coverage-gate-evaluator.d.ts.map +1 -1
  12. package/dist/evaluators/coverage-gate-evaluator.js +7 -6
  13. package/dist/evaluators/file-discovery.d.ts +72 -0
  14. package/dist/evaluators/file-discovery.d.ts.map +1 -0
  15. package/dist/evaluators/file-discovery.js +63 -0
  16. package/dist/evaluators/file-utils.d.ts +11 -118
  17. package/dist/evaluators/file-utils.d.ts.map +1 -1
  18. package/dist/evaluators/file-utils.js +11 -177
  19. package/dist/evaluators/forbidden-import-evaluator.d.ts +1 -0
  20. package/dist/evaluators/forbidden-import-evaluator.d.ts.map +1 -1
  21. package/dist/evaluators/forbidden-import-evaluator.js +9 -1
  22. package/dist/evaluators/glob-match.d.ts +34 -0
  23. package/dist/evaluators/glob-match.d.ts.map +1 -0
  24. package/dist/evaluators/glob-match.js +78 -0
  25. package/dist/evaluators/import-boundary-evaluator.d.ts.map +1 -1
  26. package/dist/evaluators/import-boundary-evaluator.js +3 -1
  27. package/dist/evaluators/path-evaluator.d.ts +1 -2
  28. package/dist/evaluators/path-evaluator.d.ts.map +1 -1
  29. package/dist/evaluators/path-evaluator.js +8 -6
  30. package/dist/evaluators/regex-evaluator.d.ts.map +1 -1
  31. package/dist/evaluators/regex-evaluator.js +3 -0
  32. package/dist/evaluators/schema-artifact-evaluator.d.ts +1 -2
  33. package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -1
  34. package/dist/evaluators/schema-artifact-evaluator.js +8 -7
  35. package/dist/evaluators/secrets-scanner-evaluator.d.ts.map +1 -1
  36. package/dist/evaluators/secrets-scanner-evaluator.js +3 -1
  37. package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
  38. package/dist/evaluators/test-location-evaluator.js +3 -1
  39. package/dist/evaluators/tsdoc-export-evaluator.d.ts.map +1 -1
  40. package/dist/evaluators/tsdoc-export-evaluator.js +3 -1
  41. package/dist/fixers/fixers.d.ts +2 -4
  42. package/dist/fixers/fixers.d.ts.map +1 -1
  43. package/dist/fixers/fixers.js +9 -11
  44. package/dist/fixers/test-stub-fixer.d.ts +1 -1
  45. package/dist/fixers/test-stub-fixer.d.ts.map +1 -1
  46. package/dist/fixers/test-stub-fixer.js +2 -2
  47. package/dist/host/bundled-rules.d.ts +2 -2
  48. package/dist/host/bundled-rules.d.ts.map +1 -1
  49. package/dist/host/bundled-rules.js +24 -19
  50. package/dist/types.d.ts +8 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +5 -5
  53. package/src/config/loader.ts +14 -6
  54. package/src/engine.ts +12 -4
  55. package/src/evaluators/config-access.ts +76 -0
  56. package/src/evaluators/coverage-gate-evaluator.ts +7 -8
  57. package/src/evaluators/file-discovery.ts +132 -0
  58. package/src/evaluators/file-utils.ts +29 -258
  59. package/src/evaluators/forbidden-import-evaluator.ts +10 -2
  60. package/src/evaluators/glob-match.ts +76 -0
  61. package/src/evaluators/import-boundary-evaluator.ts +3 -1
  62. package/src/evaluators/path-evaluator.ts +8 -7
  63. package/src/evaluators/regex-evaluator.ts +3 -0
  64. package/src/evaluators/schema-artifact-evaluator.ts +9 -8
  65. package/src/evaluators/secrets-scanner-evaluator.ts +3 -1
  66. package/src/evaluators/test-location-evaluator.ts +5 -2
  67. package/src/evaluators/tsdoc-export-evaluator.ts +3 -1
  68. package/src/fixers/fixers.ts +11 -12
  69. package/src/fixers/test-stub-fixer.ts +3 -3
  70. package/src/host/bundled-rules.ts +23 -19
  71. package/src/types.ts +8 -0
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @module rule-engine/fixers
6
6
  */
7
- import { NodeFileSystem, type ProcessExecutor } from '@gobing-ai/ts-runtime';
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?: NodeFileSystem): Promise<FixApplicationResult>;
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,EAGH,cAAc,EACd,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,cAAqC,GAC1C,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,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAwB;IAE3C,kDAAkD;IAC5C,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAkCtF;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,iBAAiB;IACvD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAwB;IAE3C,0DAA0D;IACpD,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CAoBtF"}
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"}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @module rule-engine/fixers
6
6
  */
7
- import { isAbsolutePath, joinPath, NodeFileSystem, relativePath, resolvePath, } from '@gobing-ai/ts-runtime';
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 = new NodeFileSystem()) {
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.unlink(absPath);
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 this.fs.exists(absPath)))
145
+ if (!(await fs.exists(absPath)))
146
146
  continue;
147
- const source = await this.fs.readFile(absPath);
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 (!isInsideWorkdir(context.workdir, absPath))
186
+ if (!(await fs.exists(absPath)))
187
187
  continue;
188
- if (!(await this.fs.exists(absPath)))
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,EAA4C,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACvG,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,CAAC,OAAO,CAAC,CAAC;CAC1C;AAED,6EAA6E;AAC7E,eAAO,MAAM,cAAc,EAAE,kBAAyC,CAAC;AAEvE,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"}
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, NodeFileSystem } from '@gobing-ai/ts-runtime';
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 = new NodeFileSystem();
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":"AAaA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAoBD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAI/C"}
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, NodeSyncFileSystem } from '@gobing-ai/ts-runtime';
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
- const defaultFs = new NodeSyncFileSystem();
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
- if (fs.stat(candidate)?.isDirectory() === true) {
37
- cachedRoot = candidate;
38
- return cachedRoot;
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
- cachedRoot = null;
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(defaultFs, root, '').sort();
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
- if (fs.stat(abs)?.isDirectory() === true) {
69
- acc.push(...walk(fs, abs, rel));
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 {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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.20",
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.20",
53
- "@gobing-ai/ts-infra": "^0.3.20",
54
- "@gobing-ai/ts-runtime": "^0.3.20",
55
- "@gobing-ai/ts-db": "^0.3.20",
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
  },
@@ -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(options.roots.map((root) => resolvePath(root)));
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(roots: readonly string[]): Promise<MergedRoots> {
209
- const fs = new NodeFileSystem();
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: NodeFileSystem, dir: string, depth = 0): Promise<string[]> {
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: NodeFileSystem, dir: string): Promise<string[]> {
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 { ProcessExecutor } from '@gobing-ai/ts-runtime';
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 { isAbsolutePath, NodeFileSystem, relativePath, resolvePath } from '@gobing-ai/ts-runtime';
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
- private readonly fs: NodeFileSystem;
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
- this.fs = new NodeFileSystem();
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 this.fs.exists(lcovPath))) {
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 = [];