@gobing-ai/ts-rule-engine 0.2.6 → 0.2.7

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 (75) hide show
  1. package/dist/config/extensions.d.ts +46 -0
  2. package/dist/config/extensions.d.ts.map +1 -0
  3. package/dist/config/extensions.js +63 -0
  4. package/dist/config/loader.js +13 -3
  5. package/dist/engine.d.ts +26 -1
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +79 -0
  8. package/dist/evaluators/exit-code-evaluator.d.ts +1 -1
  9. package/dist/evaluators/exit-code-evaluator.d.ts.map +1 -1
  10. package/dist/evaluators/exit-code-evaluator.js +22 -9
  11. package/dist/evaluators/forbidden-import-evaluator.d.ts +16 -3
  12. package/dist/evaluators/forbidden-import-evaluator.d.ts.map +1 -1
  13. package/dist/evaluators/forbidden-import-evaluator.js +71 -6
  14. package/dist/evaluators/import-boundary-evaluator.d.ts +19 -0
  15. package/dist/evaluators/import-boundary-evaluator.d.ts.map +1 -0
  16. package/dist/evaluators/import-boundary-evaluator.js +85 -0
  17. package/dist/evaluators/path-evaluator.d.ts +15 -2
  18. package/dist/evaluators/path-evaluator.d.ts.map +1 -1
  19. package/dist/evaluators/path-evaluator.js +49 -3
  20. package/dist/evaluators/regex-evaluator.d.ts.map +1 -1
  21. package/dist/evaluators/regex-evaluator.js +43 -8
  22. package/dist/evaluators/schema-artifact-evaluator.d.ts +21 -0
  23. package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -0
  24. package/dist/evaluators/schema-artifact-evaluator.js +102 -0
  25. package/dist/evaluators/secrets-scanner-evaluator.d.ts +13 -2
  26. package/dist/evaluators/secrets-scanner-evaluator.d.ts.map +1 -1
  27. package/dist/evaluators/secrets-scanner-evaluator.js +72 -9
  28. package/dist/evaluators/sg-evaluator.d.ts +19 -0
  29. package/dist/evaluators/sg-evaluator.d.ts.map +1 -0
  30. package/dist/evaluators/sg-evaluator.js +112 -0
  31. package/dist/evaluators/test-location-evaluator.d.ts +14 -1
  32. package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
  33. package/dist/evaluators/test-location-evaluator.js +42 -22
  34. package/dist/evaluators/tsdoc-export-evaluator.js +19 -5
  35. package/dist/fixers/fixers.d.ts +86 -0
  36. package/dist/fixers/fixers.d.ts.map +1 -0
  37. package/dist/fixers/fixers.js +230 -0
  38. package/dist/fixers/test-stub-fixer.d.ts +49 -0
  39. package/dist/fixers/test-stub-fixer.d.ts.map +1 -0
  40. package/dist/fixers/test-stub-fixer.js +91 -0
  41. package/dist/host/builtins.d.ts.map +1 -1
  42. package/dist/host/builtins.js +12 -1
  43. package/dist/host/rule-engine-host.d.ts +3 -0
  44. package/dist/host/rule-engine-host.d.ts.map +1 -1
  45. package/dist/host/rule-engine-host.js +3 -0
  46. package/dist/index.d.ts +4 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +4 -0
  49. package/dist/resolvers/test-path-resolver.d.ts +72 -0
  50. package/dist/resolvers/test-path-resolver.d.ts.map +1 -0
  51. package/dist/resolvers/test-path-resolver.js +112 -0
  52. package/dist/types.d.ts +30 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +8 -0
  55. package/package.json +3 -3
  56. package/src/config/extensions.ts +108 -0
  57. package/src/config/loader.ts +13 -3
  58. package/src/engine.ts +99 -2
  59. package/src/evaluators/exit-code-evaluator.ts +27 -9
  60. package/src/evaluators/forbidden-import-evaluator.ts +101 -7
  61. package/src/evaluators/import-boundary-evaluator.ts +135 -0
  62. package/src/evaluators/path-evaluator.ts +66 -3
  63. package/src/evaluators/regex-evaluator.ts +53 -12
  64. package/src/evaluators/schema-artifact-evaluator.ts +134 -0
  65. package/src/evaluators/secrets-scanner-evaluator.ts +89 -11
  66. package/src/evaluators/sg-evaluator.ts +133 -0
  67. package/src/evaluators/test-location-evaluator.ts +47 -35
  68. package/src/evaluators/tsdoc-export-evaluator.ts +19 -5
  69. package/src/fixers/fixers.ts +294 -0
  70. package/src/fixers/test-stub-fixer.ts +118 -0
  71. package/src/host/builtins.ts +17 -1
  72. package/src/host/rule-engine-host.ts +4 -0
  73. package/src/index.ts +4 -0
  74. package/src/resolvers/test-path-resolver.ts +133 -0
  75. package/src/types.ts +34 -0
@@ -1,3 +1,4 @@
1
+ import type { TestPathResolver } from '../resolvers/test-path-resolver';
1
2
  import type { ResultFormatter, RuleEvaluator } from '../types';
2
3
  import { CapabilityRegistry } from './capability-registry';
3
4
  /** Host container for rule-engine capabilities. */
@@ -6,6 +7,8 @@ export declare class RuleEngineHost {
6
7
  readonly evaluators: CapabilityRegistry<RuleEvaluator>;
7
8
  /** Formatter registry keyed by formatter name. */
8
9
  readonly formatters: CapabilityRegistry<ResultFormatter>;
10
+ /** Test-path resolver registry keyed by language name. */
11
+ readonly resolvers: CapabilityRegistry<TestPathResolver>;
9
12
  constructor();
10
13
  }
11
14
  //# sourceMappingURL=rule-engine-host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-engine-host.d.ts","sourceRoot":"","sources":["../../src/host/rule-engine-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,mDAAmD;AACnD,qBAAa,cAAc;IACvB,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACvD,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;;CAM5D"}
1
+ {"version":3,"file":"rule-engine-host.d.ts","sourceRoot":"","sources":["../../src/host/rule-engine-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,mDAAmD;AACnD,qBAAa,cAAc;IACvB,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACvD,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACzD,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;;CAO5D"}
@@ -5,8 +5,11 @@ export class RuleEngineHost {
5
5
  evaluators;
6
6
  /** Formatter registry keyed by formatter name. */
7
7
  formatters;
8
+ /** Test-path resolver registry keyed by language name. */
9
+ resolvers;
8
10
  constructor() {
9
11
  this.evaluators = new CapabilityRegistry('evaluator');
10
12
  this.formatters = new CapabilityRegistry('formatter');
13
+ this.resolvers = new CapabilityRegistry('resolver');
11
14
  }
12
15
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
+ export * from './config/extensions';
1
2
  export * from './config/loader';
2
3
  export * from './engine';
4
+ export * from './fixers/fixers';
5
+ export * from './fixers/test-stub-fixer';
3
6
  export * from './formatters/json';
4
7
  export * from './formatters/text';
5
8
  export * from './host/capability-registry';
6
9
  export * from './host/rule-engine-host';
10
+ export * from './resolvers/test-path-resolver';
7
11
  export * from './types';
8
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
+ export * from './config/extensions.js';
1
2
  export * from './config/loader.js';
2
3
  export * from './engine.js';
4
+ export * from './fixers/fixers.js';
5
+ export * from './fixers/test-stub-fixer.js';
3
6
  export * from './formatters/json.js';
4
7
  export * from './formatters/text.js';
5
8
  export * from './host/capability-registry.js';
6
9
  export * from './host/rule-engine-host.js';
10
+ export * from './resolvers/test-path-resolver.js';
7
11
  export * from './types.js';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pluggable test-path resolution for the test-location evaluator.
3
+ *
4
+ * Each implementation maps a source file path to its conventional test file path
5
+ * for one language, so the same evaluator works across TypeScript, Python, Go, and
6
+ * Rust projects. Resolvers are registered on the host and selected per rule via
7
+ * `evaluator.config.resolver`.
8
+ */
9
+ /** Metadata about an exported symbol, used when generating a test skeleton. */
10
+ export interface ExportInfo {
11
+ /** Symbol name. */
12
+ readonly name: string;
13
+ /** Declaration kind. */
14
+ readonly kind: 'function' | 'class' | 'const' | 'type' | 'interface' | 'unknown';
15
+ /** One-based source line, when known. */
16
+ readonly line?: number;
17
+ }
18
+ /** Maps source files to expected test files for a project type. */
19
+ export interface TestPathResolver {
20
+ /** Language name this resolver handles (registry key). */
21
+ readonly name: string;
22
+ /** Compute the expected test file path for a source file. */
23
+ resolveTestPath(srcRelPath: string): string;
24
+ /** Optionally generate a test skeleton for a source file. */
25
+ generateSkeleton?(srcRelPath: string, exports: ExportInfo[]): string;
26
+ }
27
+ /**
28
+ * TypeScript conventions:
29
+ * src/foo/bar.ts → tests/foo/bar.test.ts
30
+ * packages/core/src/foo/bar.ts → packages/core/tests/foo/bar.test.ts
31
+ */
32
+ export declare class TypeScriptTestPathResolver implements TestPathResolver {
33
+ /** Registry key. */
34
+ readonly name = "typescript";
35
+ constructor();
36
+ /** Map a TS/JS source path to its `tests/…test.ts` counterpart. */
37
+ resolveTestPath(srcRelPath: string): string;
38
+ }
39
+ /**
40
+ * Python conventions (pytest):
41
+ * src/foo/bar.py → tests/foo/test_bar.py
42
+ */
43
+ export declare class PythonTestPathResolver implements TestPathResolver {
44
+ /** Registry key. */
45
+ readonly name = "python";
46
+ constructor();
47
+ /** Map a Python source path to its `tests/…/test_*.py` counterpart. */
48
+ resolveTestPath(srcRelPath: string): string;
49
+ }
50
+ /**
51
+ * Go conventions:
52
+ * foo/bar.go → foo/bar_test.go (sibling)
53
+ */
54
+ export declare class GoTestPathResolver implements TestPathResolver {
55
+ /** Registry key. */
56
+ readonly name = "go";
57
+ constructor();
58
+ /** Map a Go source path to its sibling `_test.go` file. */
59
+ resolveTestPath(srcRelPath: string): string;
60
+ }
61
+ /**
62
+ * Rust conventions (Cargo integration tests):
63
+ * crate/src/foo.rs → crate/tests/foo.rs
64
+ */
65
+ export declare class RustTestPathResolver implements TestPathResolver {
66
+ /** Registry key. */
67
+ readonly name = "rust";
68
+ constructor();
69
+ /** Map a Rust source path to its `tests/` integration-test counterpart. */
70
+ resolveTestPath(srcRelPath: string): string;
71
+ }
72
+ //# sourceMappingURL=test-path-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-path-resolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/test-path-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACvB,mBAAmB;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wBAAwB;IACxB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;IACjF,yCAAyC;IACzC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC7B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5C,6DAA6D;IAC7D,gBAAgB,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;CACxE;AAED;;;;GAIG;AACH,qBAAa,0BAA2B,YAAW,gBAAgB;IAC/D,oBAAoB;IACpB,QAAQ,CAAC,IAAI,gBAAgB;;IAI7B,mEAAmE;IACnE,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAW9C;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,gBAAgB;IAC3D,oBAAoB;IACpB,QAAQ,CAAC,IAAI,YAAY;;IAIzB,uEAAuE;IACvE,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAe9C;AAED;;;GAGG;AACH,qBAAa,kBAAmB,YAAW,gBAAgB;IACvD,oBAAoB;IACpB,QAAQ,CAAC,IAAI,QAAQ;;IAIrB,2DAA2D;IAC3D,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAM9C;AAED;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;IACzD,oBAAoB;IACpB,QAAQ,CAAC,IAAI,UAAU;;IAIvB,2EAA2E;IAC3E,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;CAY9C"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Pluggable test-path resolution for the test-location evaluator.
3
+ *
4
+ * Each implementation maps a source file path to its conventional test file path
5
+ * for one language, so the same evaluator works across TypeScript, Python, Go, and
6
+ * Rust projects. Resolvers are registered on the host and selected per rule via
7
+ * `evaluator.config.resolver`.
8
+ */
9
+ /**
10
+ * TypeScript conventions:
11
+ * src/foo/bar.ts → tests/foo/bar.test.ts
12
+ * packages/core/src/foo/bar.ts → packages/core/tests/foo/bar.test.ts
13
+ */
14
+ export class TypeScriptTestPathResolver {
15
+ /** Registry key. */
16
+ name = 'typescript';
17
+ constructor() { }
18
+ /** Map a TS/JS source path to its `tests/…test.ts` counterpart. */
19
+ resolveTestPath(srcRelPath) {
20
+ if (srcRelPath.includes('.test.') || srcRelPath.includes('.spec.'))
21
+ return srcRelPath;
22
+ const srcIdx = srcRelPath.indexOf('/src/');
23
+ if (srcIdx !== -1) {
24
+ const pkg = srcRelPath.slice(0, srcIdx);
25
+ const rel = srcRelPath.slice(srcIdx + '/src/'.length).replace(/\.(ts|tsx|js|jsx)$/, '.test.ts');
26
+ return `${pkg}/tests/${rel}`;
27
+ }
28
+ const withoutExt = srcRelPath.replace(/\.(ts|tsx|js|jsx)$/, '');
29
+ return `tests/${withoutExt.replace(/^src\//, '')}.test.ts`;
30
+ }
31
+ }
32
+ /**
33
+ * Python conventions (pytest):
34
+ * src/foo/bar.py → tests/foo/test_bar.py
35
+ */
36
+ export class PythonTestPathResolver {
37
+ /** Registry key. */
38
+ name = 'python';
39
+ constructor() { }
40
+ /** Map a Python source path to its `tests/…/test_*.py` counterpart. */
41
+ resolveTestPath(srcRelPath) {
42
+ if (!srcRelPath)
43
+ throw new Error('empty source path');
44
+ if (srcRelPath.endsWith('_test.py') || srcRelPath.includes('/test_') || srcRelPath.startsWith('test_')) {
45
+ return srcRelPath;
46
+ }
47
+ if (srcRelPath.startsWith('tests/'))
48
+ return srcRelPath;
49
+ if (!srcRelPath.endsWith('.py'))
50
+ throw new Error(`unsupported extension for python resolver: ${srcRelPath}`);
51
+ const srcIdx = srcRelPath.indexOf('/src/');
52
+ if (srcIdx !== -1) {
53
+ const pkg = srcRelPath.slice(0, srcIdx);
54
+ return `${pkg}/tests/${testify(srcRelPath.slice(srcIdx + '/src/'.length))}`;
55
+ }
56
+ if (srcRelPath.startsWith('src/'))
57
+ return `tests/${testify(srcRelPath.slice(4))}`;
58
+ return `tests/${testify(srcRelPath)}`;
59
+ }
60
+ }
61
+ /**
62
+ * Go conventions:
63
+ * foo/bar.go → foo/bar_test.go (sibling)
64
+ */
65
+ export class GoTestPathResolver {
66
+ /** Registry key. */
67
+ name = 'go';
68
+ constructor() { }
69
+ /** Map a Go source path to its sibling `_test.go` file. */
70
+ resolveTestPath(srcRelPath) {
71
+ if (!srcRelPath)
72
+ throw new Error('empty source path');
73
+ if (srcRelPath.endsWith('_test.go'))
74
+ return srcRelPath;
75
+ if (!srcRelPath.endsWith('.go'))
76
+ throw new Error(`unsupported extension for go resolver: ${srcRelPath}`);
77
+ return srcRelPath.replace(/\.go$/, '_test.go');
78
+ }
79
+ }
80
+ /**
81
+ * Rust conventions (Cargo integration tests):
82
+ * crate/src/foo.rs → crate/tests/foo.rs
83
+ */
84
+ export class RustTestPathResolver {
85
+ /** Registry key. */
86
+ name = 'rust';
87
+ constructor() { }
88
+ /** Map a Rust source path to its `tests/` integration-test counterpart. */
89
+ resolveTestPath(srcRelPath) {
90
+ if (!srcRelPath)
91
+ throw new Error('empty source path');
92
+ if (srcRelPath.startsWith('tests/') || srcRelPath.includes('/tests/'))
93
+ return srcRelPath;
94
+ if (!srcRelPath.endsWith('.rs'))
95
+ throw new Error(`unsupported extension for rust resolver: ${srcRelPath}`);
96
+ const srcIdx = srcRelPath.indexOf('/src/');
97
+ if (srcIdx !== -1) {
98
+ const crate = srcRelPath.slice(0, srcIdx);
99
+ return `${crate}/tests/${srcRelPath.slice(srcIdx + '/src/'.length)}`;
100
+ }
101
+ if (srcRelPath.startsWith('src/'))
102
+ return `tests/${srcRelPath.slice(4)}`;
103
+ return `tests/${srcRelPath}`;
104
+ }
105
+ }
106
+ /** Prefix the basename of a Python source path with `test_`. */
107
+ function testify(rel) {
108
+ const lastSlash = rel.lastIndexOf('/');
109
+ const dir = lastSlash >= 0 ? rel.slice(0, lastSlash + 1) : '';
110
+ const base = lastSlash >= 0 ? rel.slice(lastSlash + 1) : rel;
111
+ return `${dir}test_${base}`;
112
+ }
package/dist/types.d.ts CHANGED
@@ -49,6 +49,17 @@ export interface ConstraintRuleFile {
49
49
  /** Rule definitions. */
50
50
  rules: ConstraintRule[];
51
51
  }
52
+ /** Relative module paths a preset contributes per capability kind. */
53
+ export interface PresetExtensions {
54
+ /** Test-path resolver module paths. */
55
+ resolvers?: string[];
56
+ /** Evaluator module paths. */
57
+ evaluators?: string[];
58
+ /** Fixer module paths. */
59
+ fixers?: string[];
60
+ /** Formatter module paths. */
61
+ formatters?: string[];
62
+ }
52
63
  /** Preset definition that composes category folders or other presets. */
53
64
  export interface PresetDefinition {
54
65
  /** Preset name. */
@@ -63,6 +74,8 @@ export interface PresetDefinition {
63
74
  mode: FixMode;
64
75
  };
65
76
  }>;
77
+ /** Custom capability modules contributed by this preset (opt-in to load). */
78
+ extensions?: PresetExtensions;
66
79
  }
67
80
  /** Candidate fix emitted by an evaluator or fixer. */
68
81
  export interface Fix {
@@ -79,6 +92,15 @@ export interface Fix {
79
92
  /** Whether this fix may be applied automatically. */
80
93
  mode: Exclude<FixMode, 'none'>;
81
94
  }
95
+ /**
96
+ * What a finding represents.
97
+ *
98
+ * - `violation`: the rule ran and the project breached its policy (the default).
99
+ * - `error`: the rule could not run — a misconfiguration or runtime fault in the
100
+ * evaluator itself. These are not policy breaches and callers may surface them
101
+ * separately (e.g. "rule misconfigured") rather than as project violations.
102
+ */
103
+ export type FindingKind = 'violation' | 'error';
82
104
  /** Finding emitted by a constraint rule. */
83
105
  export interface ConstraintFinding {
84
106
  /** Rule identifier. */
@@ -95,6 +117,8 @@ export interface ConstraintFinding {
95
117
  column?: number;
96
118
  /** Machine-readable evaluator/source code. */
97
119
  code?: string;
120
+ /** Whether this is a policy violation or an evaluator error. Absent means `violation`. */
121
+ kind?: FindingKind;
98
122
  }
99
123
  /** Aggregate result returned by a rule evaluator. */
100
124
  export interface RuleEvaluationResult {
@@ -214,5 +238,11 @@ export declare const PresetDefinitionSchema: z.ZodObject<{
214
238
  }>;
215
239
  }, z.core.$strip>>;
216
240
  }, z.core.$strip>>>;
241
+ extensions: z.ZodOptional<z.ZodObject<{
242
+ resolvers: z.ZodOptional<z.ZodArray<z.ZodString>>;
243
+ evaluators: z.ZodOptional<z.ZodArray<z.ZodString>>;
244
+ fixers: z.ZodOptional<z.ZodArray<z.ZodString>>;
245
+ formatters: z.ZodOptional<z.ZodArray<z.ZodString>>;
246
+ }, z.core.$strip>>;
217
247
  }, z.core.$strip>;
218
248
  //# sourceMappingURL=types.d.ts.map
@@ -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/dist/types.js CHANGED
@@ -46,4 +46,12 @@ export const PresetDefinitionSchema = z.object({
46
46
  overrides: z
47
47
  .record(z.string(), z.object({ fix: z.object({ mode: z.enum(['none', 'suggest', 'auto']) }).optional() }))
48
48
  .optional(),
49
+ extensions: z
50
+ .object({
51
+ resolvers: z.array(z.string()).optional(),
52
+ evaluators: z.array(z.string()).optional(),
53
+ fixers: z.array(z.string()).optional(),
54
+ formatters: z.array(z.string()).optional(),
55
+ })
56
+ .optional(),
49
57
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-rule-engine",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -47,8 +47,8 @@
47
47
  "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"
48
48
  },
49
49
  "dependencies": {
50
- "@gobing-ai/ts-ai-runner": "^0.2.6",
51
- "@gobing-ai/ts-runtime": "^0.2.6",
50
+ "@gobing-ai/ts-ai-runner": "^0.2.7",
51
+ "@gobing-ai/ts-runtime": "^0.2.7",
52
52
  "yaml": "^2.7.0",
53
53
  "zod": "^4.1.0"
54
54
  },
@@ -0,0 +1,108 @@
1
+ import { resolve } from 'node:path';
2
+ import type { RuleEngineHost } from '../host/rule-engine-host';
3
+
4
+ /** A capability kind a preset extension can contribute. */
5
+ export type ExtensionKind = 'resolvers' | 'evaluators' | 'fixers' | 'formatters';
6
+
7
+ /** A single extension module reference, resolved to an absolute path. */
8
+ export interface ExtensionRef {
9
+ /** Capability registry the module registers into. */
10
+ readonly kind: ExtensionKind;
11
+ /** Absolute path to the module to import. */
12
+ readonly absPath: string;
13
+ /** Name of the preset that declared this extension (for diagnostics). */
14
+ readonly presetName: string;
15
+ }
16
+
17
+ /** Options controlling preset-extension loading. */
18
+ export interface LoadExtensionsOptions {
19
+ /**
20
+ * Whether to actually import extension modules. Defaults to `false`: loading
21
+ * arbitrary code referenced by a preset is a trust decision the caller must
22
+ * make explicitly. When refs exist and this is false, loading throws.
23
+ */
24
+ allowExtensions?: boolean;
25
+ /** Optional sink for non-fatal warnings (e.g. built-in overrides). */
26
+ logger?: { warn: (message: string) => void };
27
+ }
28
+
29
+ /** Host registries that can receive extension capabilities (fixers live on the engine, not the host). */
30
+ const HOST_REGISTRY_BY_KIND: Partial<Record<ExtensionKind, 'resolvers' | 'evaluators' | 'formatters'>> = {
31
+ resolvers: 'resolvers',
32
+ evaluators: 'evaluators',
33
+ formatters: 'formatters',
34
+ };
35
+
36
+ /**
37
+ * Collect extension refs declared by a preset's `extensions` block.
38
+ *
39
+ * Paths are resolved relative to the preset file's directory. Use the returned
40
+ * refs with {@link loadExtensionsIntoHost}.
41
+ */
42
+ export function collectPresetExtensions(
43
+ presetName: string,
44
+ presetDir: string,
45
+ extensions: Partial<Record<ExtensionKind, string[] | undefined>> | undefined,
46
+ ): ExtensionRef[] {
47
+ if (extensions === undefined) return [];
48
+ const refs: ExtensionRef[] = [];
49
+ for (const kind of ['resolvers', 'evaluators', 'fixers', 'formatters'] as ExtensionKind[]) {
50
+ for (const path of extensions[kind] ?? []) {
51
+ refs.push({ kind, presetName, absPath: resolve(presetDir, path) });
52
+ }
53
+ }
54
+ return refs;
55
+ }
56
+
57
+ /**
58
+ * Import each extension module and register its export on the matching host
59
+ * registry.
60
+ *
61
+ * A module must default-export (or named-export `extension`) an object with a
62
+ * `name: string` and the capability implementation. Loading is gated by
63
+ * {@link LoadExtensionsOptions.allowExtensions}; when refs are present but
64
+ * loading is not allowed, this throws so the requirement is never silently dropped.
65
+ *
66
+ * @throws When extensions are present but `allowExtensions` is not true, or when
67
+ * a module cannot be imported or lacks a valid `name`.
68
+ */
69
+ export async function loadExtensionsIntoHost(
70
+ host: RuleEngineHost,
71
+ refs: readonly ExtensionRef[],
72
+ options: LoadExtensionsOptions = {},
73
+ ): Promise<void> {
74
+ if (refs.length === 0) return;
75
+ if (options.allowExtensions !== true) {
76
+ const first = refs[0] as ExtensionRef;
77
+ throw new Error(
78
+ `preset "${first.presetName}" declares ${first.kind} extension "${first.absPath}", but extensions are disabled — pass allowExtensions: true to load preset extension modules`,
79
+ );
80
+ }
81
+
82
+ for (const ref of refs) {
83
+ const moduleExports = (await import(ref.absPath)) as Record<string, unknown>;
84
+ const candidate = moduleExports.default ?? moduleExports.extension;
85
+ if (
86
+ candidate === null ||
87
+ typeof candidate !== 'object' ||
88
+ typeof (candidate as { name?: unknown }).name !== 'string'
89
+ ) {
90
+ throw new Error(
91
+ `preset "${ref.presetName}" extension "${ref.absPath}" must export an object with a string "name"`,
92
+ );
93
+ }
94
+ const name = (candidate as { name: string }).name;
95
+ const registryKey = HOST_REGISTRY_BY_KIND[ref.kind];
96
+ if (registryKey === undefined) {
97
+ throw new Error(`preset "${ref.presetName}" ${ref.kind} extensions are not supported`);
98
+ }
99
+ const registry = host[registryKey] as unknown as {
100
+ register: (name: string, impl: unknown, origin: 'builtin' | 'extension') => void;
101
+ has?: (name: string) => boolean;
102
+ };
103
+ if (options.logger && registry.has?.(name)) {
104
+ options.logger.warn(`preset "${ref.presetName}" ${ref.kind} extension overrides existing "${name}"`);
105
+ }
106
+ registry.register(name, candidate, 'extension');
107
+ }
108
+ }
@@ -63,9 +63,12 @@ export async function loadRuleFile(filePath: string): Promise<ConstraintRule[]>
63
63
  }
64
64
 
65
65
  async function loadPresetEntry(merged: MergedRoots, entry: string, seen: Set<string>): Promise<ConstraintRule[]> {
66
- // Sub-preset reference — recurse (cycle-guarded).
66
+ // Sub-preset reference — recurse, erroring on a genuine cycle.
67
67
  const presetPath = findMergedPreset(merged, entry);
68
- if (presetPath !== null && !seen.has(entry)) {
68
+ if (presetPath !== null) {
69
+ if (seen.has(entry)) {
70
+ throw new Error(`Circular preset dependency detected: ${[...seen, entry].join(' → ')}`);
71
+ }
69
72
  seen.add(entry);
70
73
  const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath));
71
74
  if (preset.success) {
@@ -200,7 +203,8 @@ function normalizeFileRules(file: ConstraintRuleFile, sourceDir: string): Constr
200
203
  ...rule,
201
204
  severity: rule.severity ?? file.severity ?? 'error',
202
205
  include: rule.include ?? file.include,
203
- exclude: rule.exclude ?? file.exclude,
206
+ // File-level excludes always apply; a rule's own excludes add to (not replace) them.
207
+ exclude: mergeExcludes(file.exclude, rule.exclude),
204
208
  },
205
209
  {},
206
210
  sourceDir,
@@ -208,6 +212,12 @@ function normalizeFileRules(file: ConstraintRuleFile, sourceDir: string): Constr
208
212
  );
209
213
  }
210
214
 
215
+ /** Union of file-level and rule-level excludes, de-duplicated. Returns undefined when both empty. */
216
+ function mergeExcludes(fileExclude?: string[], ruleExclude?: string[]): string[] | undefined {
217
+ if (fileExclude === undefined && ruleExclude === undefined) return undefined;
218
+ return [...new Set([...(fileExclude ?? []), ...(ruleExclude ?? [])])];
219
+ }
220
+
211
221
  function normalizeRule(rule: ConstraintRule, _defaults: Partial<ConstraintRule>, _sourceDir: string): ConstraintRule {
212
222
  return {
213
223
  ...rule,
package/src/engine.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import type { ProcessExecutor } from '@gobing-ai/ts-runtime';
2
+ import {
3
+ applyFixes as applyFixesImpl,
4
+ builtInFixers,
5
+ type EffectiveFix,
6
+ FIX_MODE_RANK,
7
+ type FixApplicationResult,
8
+ type RuleFixerProvider,
9
+ } from './fixers/fixers';
2
10
  import { registerBuiltins } from './host/builtins';
3
11
  import { RuleEngineHost } from './host/rule-engine-host';
4
- import type { ConstraintFinding, ConstraintRule, RuleEngineResult, RuleEvaluator } from './types';
12
+ import type { ConstraintFinding, ConstraintRule, Fix, FixMode, RuleEngineResult, RuleEvaluator } from './types';
5
13
  import { createFinding } from './types';
6
14
 
7
15
  /** Options for constructing a RuleEngine. */
@@ -17,9 +25,13 @@ export class RuleEngine {
17
25
  /** Capability host used by this engine. */
18
26
  readonly host: RuleEngineHost;
19
27
 
28
+ /** Fixer providers keyed by evaluator type. */
29
+ private readonly fixers: Map<string, RuleFixerProvider>;
30
+
20
31
  constructor(options: RuleEngineOptions = {}) {
21
32
  this.host = options.host ?? new RuleEngineHost();
22
33
  registerBuiltins(this.host, options.processExecutor);
34
+ this.fixers = builtInFixers(this.host, options.processExecutor);
23
35
  }
24
36
 
25
37
  /** Register or replace an evaluator. */
@@ -30,7 +42,7 @@ export class RuleEngine {
30
42
  /** Evaluate all enabled rules against a working directory. */
31
43
  async evaluate(rules: ConstraintRule[], workdir: string): Promise<RuleEngineResult> {
32
44
  const findings: ConstraintFinding[] = [];
33
- const fixes = [];
45
+ const fixes: Fix[] = [];
34
46
  for (const rule of rules) {
35
47
  if (rule.enabled === false) continue;
36
48
  try {
@@ -41,10 +53,95 @@ export class RuleEngine {
41
53
  findings.push(
42
54
  createFinding(rule, error instanceof Error ? error.message : String(error), null, {
43
55
  code: `evaluator:${rule.evaluator.type}`,
56
+ kind: 'error',
44
57
  }),
45
58
  );
46
59
  }
47
60
  }
48
61
  return { findings, fixes };
49
62
  }
63
+
64
+ /**
65
+ * Evaluate all enabled rules and collect candidate fixes.
66
+ *
67
+ * For each rule with findings and a non-none fix mode, looks up the fixer
68
+ * provider by evaluator type and calls `createFixes`. The effective fix mode
69
+ * is the minimum of the rule's configured mode and `maxFixMode`.
70
+ *
71
+ * @param rules - Normalized rule definitions to evaluate.
72
+ * @param workdir - Working directory to scan.
73
+ * @param maxFixMode - Highest fix authority requested by the caller.
74
+ * @returns Findings plus fixes allowed by the requested authority.
75
+ */
76
+ async evaluateWithFixes(
77
+ rules: ConstraintRule[],
78
+ workdir: string,
79
+ maxFixMode: FixMode = 'auto',
80
+ ): Promise<RuleEngineResult> {
81
+ const findings: ConstraintFinding[] = [];
82
+ const fixes: Fix[] = [];
83
+
84
+ for (const rule of rules) {
85
+ if (rule.enabled === false) continue;
86
+
87
+ let ruleFindings: ConstraintFinding[] = [];
88
+ let ruleEvalFixes: Fix[] = [];
89
+ try {
90
+ const result = await this.host.evaluators.get(rule.evaluator.type).evaluate(rule, { rule, workdir });
91
+ ruleFindings = result.findings;
92
+ ruleEvalFixes = result.fixes;
93
+ } catch (error) {
94
+ ruleFindings = [
95
+ createFinding(rule, error instanceof Error ? error.message : String(error), null, {
96
+ code: `evaluator:${rule.evaluator.type}`,
97
+ kind: 'error',
98
+ }),
99
+ ];
100
+ }
101
+
102
+ findings.push(...ruleFindings);
103
+ fixes.push(...ruleEvalFixes);
104
+
105
+ const ruleMode = rule.fix?.mode ?? 'none';
106
+ const effectiveMode = effectiveFixMode(ruleMode, maxFixMode);
107
+
108
+ if (effectiveMode !== 'none' && ruleFindings.length > 0) {
109
+ const provider = this.fixers.get(rule.evaluator.type);
110
+ if (provider) {
111
+ const effectiveFix: EffectiveFix = {
112
+ mode: effectiveMode,
113
+ ...(rule.fix?.replacement !== undefined ? { replacement: rule.fix.replacement } : {}),
114
+ ...(rule.fix?.params !== undefined ? { params: rule.fix.params } : {}),
115
+ };
116
+ const providerFixes = await provider.createFixes({
117
+ rule,
118
+ context: { rule, workdir },
119
+ findings: ruleFindings,
120
+ fix: effectiveFix,
121
+ });
122
+ fixes.push(...providerFixes);
123
+ }
124
+ }
125
+ }
126
+
127
+ return { findings, fixes };
128
+ }
129
+
130
+ /**
131
+ * Apply or preview candidate byte-range fixes.
132
+ *
133
+ * @param workdir - Working directory that fix file paths are relative to.
134
+ * @param fixes - Fixes to apply.
135
+ * @param dryRun - When true, return a diff without writing files.
136
+ * @returns Application details and optional diff.
137
+ */
138
+ async applyFixes(workdir: string, fixes: readonly Fix[], dryRun = false): Promise<FixApplicationResult> {
139
+ return applyFixesImpl(workdir, fixes, dryRun);
140
+ }
141
+ }
142
+
143
+ /** Return the lower-authority mode between what the rule requests and what the caller allows. */
144
+ function effectiveFixMode(ruleMode: FixMode, requestedMode: FixMode): FixMode {
145
+ if (requestedMode === 'none' || ruleMode === 'none') return 'none';
146
+ return FIX_MODE_RANK[ruleMode] <= FIX_MODE_RANK[requestedMode] ? ruleMode : requestedMode;
50
147
  }