@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
@@ -0,0 +1,46 @@
1
+ import type { RuleEngineHost } from '../host/rule-engine-host';
2
+ /** A capability kind a preset extension can contribute. */
3
+ export type ExtensionKind = 'resolvers' | 'evaluators' | 'fixers' | 'formatters';
4
+ /** A single extension module reference, resolved to an absolute path. */
5
+ export interface ExtensionRef {
6
+ /** Capability registry the module registers into. */
7
+ readonly kind: ExtensionKind;
8
+ /** Absolute path to the module to import. */
9
+ readonly absPath: string;
10
+ /** Name of the preset that declared this extension (for diagnostics). */
11
+ readonly presetName: string;
12
+ }
13
+ /** Options controlling preset-extension loading. */
14
+ export interface LoadExtensionsOptions {
15
+ /**
16
+ * Whether to actually import extension modules. Defaults to `false`: loading
17
+ * arbitrary code referenced by a preset is a trust decision the caller must
18
+ * make explicitly. When refs exist and this is false, loading throws.
19
+ */
20
+ allowExtensions?: boolean;
21
+ /** Optional sink for non-fatal warnings (e.g. built-in overrides). */
22
+ logger?: {
23
+ warn: (message: string) => void;
24
+ };
25
+ }
26
+ /**
27
+ * Collect extension refs declared by a preset's `extensions` block.
28
+ *
29
+ * Paths are resolved relative to the preset file's directory. Use the returned
30
+ * refs with {@link loadExtensionsIntoHost}.
31
+ */
32
+ export declare function collectPresetExtensions(presetName: string, presetDir: string, extensions: Partial<Record<ExtensionKind, string[] | undefined>> | undefined): ExtensionRef[];
33
+ /**
34
+ * Import each extension module and register its export on the matching host
35
+ * registry.
36
+ *
37
+ * A module must default-export (or named-export `extension`) an object with a
38
+ * `name: string` and the capability implementation. Loading is gated by
39
+ * {@link LoadExtensionsOptions.allowExtensions}; when refs are present but
40
+ * loading is not allowed, this throws so the requirement is never silently dropped.
41
+ *
42
+ * @throws When extensions are present but `allowExtensions` is not true, or when
43
+ * a module cannot be imported or lacks a valid `name`.
44
+ */
45
+ export declare function loadExtensionsIntoHost(host: RuleEngineHost, refs: readonly ExtensionRef[], options?: LoadExtensionsOptions): Promise<void>;
46
+ //# sourceMappingURL=extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/config/extensions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEjF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IACzB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CAChD;AASD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACnC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,GAC7E,YAAY,EAAE,CAShB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CACxC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GACpC,OAAO,CAAC,IAAI,CAAC,CAmCf"}
@@ -0,0 +1,63 @@
1
+ import { resolve } from 'node:path';
2
+ /** Host registries that can receive extension capabilities (fixers live on the engine, not the host). */
3
+ const HOST_REGISTRY_BY_KIND = {
4
+ resolvers: 'resolvers',
5
+ evaluators: 'evaluators',
6
+ formatters: 'formatters',
7
+ };
8
+ /**
9
+ * Collect extension refs declared by a preset's `extensions` block.
10
+ *
11
+ * Paths are resolved relative to the preset file's directory. Use the returned
12
+ * refs with {@link loadExtensionsIntoHost}.
13
+ */
14
+ export function collectPresetExtensions(presetName, presetDir, extensions) {
15
+ if (extensions === undefined)
16
+ return [];
17
+ const refs = [];
18
+ for (const kind of ['resolvers', 'evaluators', 'fixers', 'formatters']) {
19
+ for (const path of extensions[kind] ?? []) {
20
+ refs.push({ kind, presetName, absPath: resolve(presetDir, path) });
21
+ }
22
+ }
23
+ return refs;
24
+ }
25
+ /**
26
+ * Import each extension module and register its export on the matching host
27
+ * registry.
28
+ *
29
+ * A module must default-export (or named-export `extension`) an object with a
30
+ * `name: string` and the capability implementation. Loading is gated by
31
+ * {@link LoadExtensionsOptions.allowExtensions}; when refs are present but
32
+ * loading is not allowed, this throws so the requirement is never silently dropped.
33
+ *
34
+ * @throws When extensions are present but `allowExtensions` is not true, or when
35
+ * a module cannot be imported or lacks a valid `name`.
36
+ */
37
+ export async function loadExtensionsIntoHost(host, refs, options = {}) {
38
+ if (refs.length === 0)
39
+ return;
40
+ if (options.allowExtensions !== true) {
41
+ const first = refs[0];
42
+ throw new Error(`preset "${first.presetName}" declares ${first.kind} extension "${first.absPath}", but extensions are disabled — pass allowExtensions: true to load preset extension modules`);
43
+ }
44
+ for (const ref of refs) {
45
+ const moduleExports = (await import(ref.absPath));
46
+ const candidate = moduleExports.default ?? moduleExports.extension;
47
+ if (candidate === null ||
48
+ typeof candidate !== 'object' ||
49
+ typeof candidate.name !== 'string') {
50
+ throw new Error(`preset "${ref.presetName}" extension "${ref.absPath}" must export an object with a string "name"`);
51
+ }
52
+ const name = candidate.name;
53
+ const registryKey = HOST_REGISTRY_BY_KIND[ref.kind];
54
+ if (registryKey === undefined) {
55
+ throw new Error(`preset "${ref.presetName}" ${ref.kind} extensions are not supported`);
56
+ }
57
+ const registry = host[registryKey];
58
+ if (options.logger && registry.has?.(name)) {
59
+ options.logger.warn(`preset "${ref.presetName}" ${ref.kind} extension overrides existing "${name}"`);
60
+ }
61
+ registry.register(name, candidate, 'extension');
62
+ }
63
+ }
@@ -34,9 +34,12 @@ export async function loadRuleFile(filePath) {
34
34
  return normalizeRuleFile(await readStructuredFile(resolve(filePath)), dirname(resolve(filePath)));
35
35
  }
36
36
  async function loadPresetEntry(merged, entry, seen) {
37
- // Sub-preset reference — recurse (cycle-guarded).
37
+ // Sub-preset reference — recurse, erroring on a genuine cycle.
38
38
  const presetPath = findMergedPreset(merged, entry);
39
- if (presetPath !== null && !seen.has(entry)) {
39
+ if (presetPath !== null) {
40
+ if (seen.has(entry)) {
41
+ throw new Error(`Circular preset dependency detected: ${[...seen, entry].join(' → ')}`);
42
+ }
40
43
  seen.add(entry);
41
44
  const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath));
42
45
  if (preset.success) {
@@ -170,9 +173,16 @@ function normalizeFileRules(file, sourceDir) {
170
173
  ...rule,
171
174
  severity: rule.severity ?? file.severity ?? 'error',
172
175
  include: rule.include ?? file.include,
173
- exclude: rule.exclude ?? file.exclude,
176
+ // File-level excludes always apply; a rule's own excludes add to (not replace) them.
177
+ exclude: mergeExcludes(file.exclude, rule.exclude),
174
178
  }, {}, sourceDir));
175
179
  }
180
+ /** Union of file-level and rule-level excludes, de-duplicated. Returns undefined when both empty. */
181
+ function mergeExcludes(fileExclude, ruleExclude) {
182
+ if (fileExclude === undefined && ruleExclude === undefined)
183
+ return undefined;
184
+ return [...new Set([...(fileExclude ?? []), ...(ruleExclude ?? [])])];
185
+ }
176
186
  function normalizeRule(rule, _defaults, _sourceDir) {
177
187
  return {
178
188
  ...rule,
package/dist/engine.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ProcessExecutor } from '@gobing-ai/ts-runtime';
2
+ import { type FixApplicationResult } from './fixers/fixers';
2
3
  import { RuleEngineHost } from './host/rule-engine-host';
3
- import type { ConstraintRule, RuleEngineResult, RuleEvaluator } from './types';
4
+ import type { ConstraintRule, Fix, FixMode, RuleEngineResult, RuleEvaluator } from './types';
4
5
  /** Options for constructing a RuleEngine. */
5
6
  export interface RuleEngineOptions {
6
7
  /** Optional executor supplied to process-backed evaluators. */
@@ -12,10 +13,34 @@ export interface RuleEngineOptions {
12
13
  export declare class RuleEngine {
13
14
  /** Capability host used by this engine. */
14
15
  readonly host: RuleEngineHost;
16
+ /** Fixer providers keyed by evaluator type. */
17
+ private readonly fixers;
15
18
  constructor(options?: RuleEngineOptions);
16
19
  /** Register or replace an evaluator. */
17
20
  registerEvaluator(type: string, evaluator: RuleEvaluator): void;
18
21
  /** Evaluate all enabled rules against a working directory. */
19
22
  evaluate(rules: ConstraintRule[], workdir: string): Promise<RuleEngineResult>;
23
+ /**
24
+ * Evaluate all enabled rules and collect candidate fixes.
25
+ *
26
+ * For each rule with findings and a non-none fix mode, looks up the fixer
27
+ * provider by evaluator type and calls `createFixes`. The effective fix mode
28
+ * is the minimum of the rule's configured mode and `maxFixMode`.
29
+ *
30
+ * @param rules - Normalized rule definitions to evaluate.
31
+ * @param workdir - Working directory to scan.
32
+ * @param maxFixMode - Highest fix authority requested by the caller.
33
+ * @returns Findings plus fixes allowed by the requested authority.
34
+ */
35
+ evaluateWithFixes(rules: ConstraintRule[], workdir: string, maxFixMode?: FixMode): Promise<RuleEngineResult>;
36
+ /**
37
+ * Apply or preview candidate byte-range fixes.
38
+ *
39
+ * @param workdir - Working directory that fix file paths are relative to.
40
+ * @param fixes - Fixes to apply.
41
+ * @param dryRun - When true, return a diff without writing files.
42
+ * @returns Application details and optional diff.
43
+ */
44
+ applyFixes(workdir: string, fixes: readonly Fix[], dryRun?: boolean): Promise<FixApplicationResult>;
20
45
  }
21
46
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAqB,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGlG,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAC9B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,mCAAmC;IACnC,IAAI,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,4EAA4E;AAC5E,qBAAa,UAAU;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,OAAO,GAAE,iBAAsB;IAK3C,wCAAwC;IACxC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAI/D,8DAA8D;IACxD,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAmBtF"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAKH,KAAK,oBAAoB,EAE5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAqB,cAAc,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGhH,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAC9B,+DAA+D;IAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,mCAAmC;IACnC,IAAI,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,4EAA4E;AAC5E,qBAAa,UAAU;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAE9B,+CAA+C;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiC;gBAE5C,OAAO,GAAE,iBAAsB;IAM3C,wCAAwC;IACxC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAI/D,8DAA8D;IACxD,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAqBnF;;;;;;;;;;;OAWG;IACG,iBAAiB,CACnB,KAAK,EAAE,cAAc,EAAE,EACvB,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,OAAgB,GAC7B,OAAO,CAAC,gBAAgB,CAAC;IAkD5B;;;;;;;OAOG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,EAAE,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAG1G"}
package/dist/engine.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyFixes as applyFixesImpl, builtInFixers, FIX_MODE_RANK, } from './fixers/fixers.js';
1
2
  import { registerBuiltins } from './host/builtins.js';
2
3
  import { RuleEngineHost } from './host/rule-engine-host.js';
3
4
  import { createFinding } from './types.js';
@@ -5,9 +6,12 @@ import { createFinding } from './types.js';
5
6
  export class RuleEngine {
6
7
  /** Capability host used by this engine. */
7
8
  host;
9
+ /** Fixer providers keyed by evaluator type. */
10
+ fixers;
8
11
  constructor(options = {}) {
9
12
  this.host = options.host ?? new RuleEngineHost();
10
13
  registerBuiltins(this.host, options.processExecutor);
14
+ this.fixers = builtInFixers(this.host, options.processExecutor);
11
15
  }
12
16
  /** Register or replace an evaluator. */
13
17
  registerEvaluator(type, evaluator) {
@@ -28,9 +32,84 @@ export class RuleEngine {
28
32
  catch (error) {
29
33
  findings.push(createFinding(rule, error instanceof Error ? error.message : String(error), null, {
30
34
  code: `evaluator:${rule.evaluator.type}`,
35
+ kind: 'error',
31
36
  }));
32
37
  }
33
38
  }
34
39
  return { findings, fixes };
35
40
  }
41
+ /**
42
+ * Evaluate all enabled rules and collect candidate fixes.
43
+ *
44
+ * For each rule with findings and a non-none fix mode, looks up the fixer
45
+ * provider by evaluator type and calls `createFixes`. The effective fix mode
46
+ * is the minimum of the rule's configured mode and `maxFixMode`.
47
+ *
48
+ * @param rules - Normalized rule definitions to evaluate.
49
+ * @param workdir - Working directory to scan.
50
+ * @param maxFixMode - Highest fix authority requested by the caller.
51
+ * @returns Findings plus fixes allowed by the requested authority.
52
+ */
53
+ async evaluateWithFixes(rules, workdir, maxFixMode = 'auto') {
54
+ const findings = [];
55
+ const fixes = [];
56
+ for (const rule of rules) {
57
+ if (rule.enabled === false)
58
+ continue;
59
+ let ruleFindings = [];
60
+ let ruleEvalFixes = [];
61
+ try {
62
+ const result = await this.host.evaluators.get(rule.evaluator.type).evaluate(rule, { rule, workdir });
63
+ ruleFindings = result.findings;
64
+ ruleEvalFixes = result.fixes;
65
+ }
66
+ catch (error) {
67
+ ruleFindings = [
68
+ createFinding(rule, error instanceof Error ? error.message : String(error), null, {
69
+ code: `evaluator:${rule.evaluator.type}`,
70
+ kind: 'error',
71
+ }),
72
+ ];
73
+ }
74
+ findings.push(...ruleFindings);
75
+ fixes.push(...ruleEvalFixes);
76
+ const ruleMode = rule.fix?.mode ?? 'none';
77
+ const effectiveMode = effectiveFixMode(ruleMode, maxFixMode);
78
+ if (effectiveMode !== 'none' && ruleFindings.length > 0) {
79
+ const provider = this.fixers.get(rule.evaluator.type);
80
+ if (provider) {
81
+ const effectiveFix = {
82
+ mode: effectiveMode,
83
+ ...(rule.fix?.replacement !== undefined ? { replacement: rule.fix.replacement } : {}),
84
+ ...(rule.fix?.params !== undefined ? { params: rule.fix.params } : {}),
85
+ };
86
+ const providerFixes = await provider.createFixes({
87
+ rule,
88
+ context: { rule, workdir },
89
+ findings: ruleFindings,
90
+ fix: effectiveFix,
91
+ });
92
+ fixes.push(...providerFixes);
93
+ }
94
+ }
95
+ }
96
+ return { findings, fixes };
97
+ }
98
+ /**
99
+ * Apply or preview candidate byte-range fixes.
100
+ *
101
+ * @param workdir - Working directory that fix file paths are relative to.
102
+ * @param fixes - Fixes to apply.
103
+ * @param dryRun - When true, return a diff without writing files.
104
+ * @returns Application details and optional diff.
105
+ */
106
+ async applyFixes(workdir, fixes, dryRun = false) {
107
+ return applyFixesImpl(workdir, fixes, dryRun);
108
+ }
109
+ }
110
+ /** Return the lower-authority mode between what the rule requests and what the caller allows. */
111
+ function effectiveFixMode(ruleMode, requestedMode) {
112
+ if (requestedMode === 'none' || ruleMode === 'none')
113
+ return 'none';
114
+ return FIX_MODE_RANK[ruleMode] <= FIX_MODE_RANK[requestedMode] ? ruleMode : requestedMode;
36
115
  }
@@ -4,7 +4,7 @@ import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type
4
4
  export declare class ExitCodeEvaluator implements RuleEvaluator {
5
5
  private readonly executor;
6
6
  constructor(executor?: ProcessExecutor);
7
- /** Run configured command and emit a finding on non-zero exit. */
7
+ /** Run configured command and emit a finding unless the exit code matches `successCode`. */
8
8
  evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
9
9
  }
10
10
  //# sourceMappingURL=exit-code-evaluator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"exit-code-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/exit-code-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAElB,2EAA2E;AAC3E,qBAAa,iBAAkB,YAAW,aAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,GAAE,eAA2C;IAElF,kEAAkE;IAC5D,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAe5F"}
1
+ {"version":3,"file":"exit-code-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/exit-code-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAElB,2EAA2E;AAC3E,qBAAa,iBAAkB,YAAW,aAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,GAAE,eAA2C;IAElF,4FAA4F;IACtF,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CA2B5F"}
@@ -6,28 +6,41 @@ export class ExitCodeEvaluator {
6
6
  constructor(executor = new NodeProcessExecutor()) {
7
7
  this.executor = executor;
8
8
  }
9
- /** Run configured command and emit a finding on non-zero exit. */
9
+ /** Run configured command and emit a finding unless the exit code matches `successCode`. */
10
10
  async evaluate(rule, context) {
11
11
  const config = rule.evaluator.config ?? {};
12
12
  const command = stringConfig(config, 'command');
13
13
  const args = arrayConfig(config, 'args', []);
14
- const result = await this.executor.run({ command, args, cwd: context.workdir, rejectOnError: false });
15
- if (result.exitCode === 0)
14
+ const successCode = numberConfig(config, 'successCode', 0);
15
+ const timeout = numberConfig(config, 'timeout', 60_000);
16
+ const result = await this.executor.run({
17
+ command,
18
+ args,
19
+ cwd: context.workdir,
20
+ timeout,
21
+ rejectOnError: false,
22
+ label: 'exit-code',
23
+ });
24
+ if (result.exitCode === successCode)
16
25
  return { findings: [], fixes: [] };
26
+ const template = stringConfig(config, 'message', `Command failed (exit {code}): ${command} ${args.join(' ')}`.trim());
27
+ const message = template.replaceAll('{code}', String(result.exitCode));
17
28
  return {
18
- findings: [
19
- createFinding(rule, `Command failed: ${command} ${args.join(' ')}`.trim(), null, {
20
- code: 'exit-code:failed',
21
- }),
22
- ],
29
+ findings: [createFinding(rule, message, null, { code: 'exit-code:failed' })],
23
30
  fixes: [],
24
31
  };
25
32
  }
26
33
  }
27
- function stringConfig(config, key) {
34
+ function numberConfig(config, key, fallback) {
35
+ const value = config[key];
36
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
37
+ }
38
+ function stringConfig(config, key, fallback) {
28
39
  const value = config[key];
29
40
  if (typeof value === 'string')
30
41
  return value;
42
+ if (fallback !== undefined)
43
+ return fallback;
31
44
  throw new Error(`exit-code evaluator requires string config "${key}"`);
32
45
  }
33
46
  function arrayConfig(config, key, fallback) {
@@ -1,8 +1,21 @@
1
1
  import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
2
- /** Detects imports matching forbidden package or path prefixes. */
2
+ /**
3
+ * Detects forbidden imports / API usage.
4
+ *
5
+ * Two config shapes are supported:
6
+ * - Simple: `{ patterns: string[] }` — substring match against any import specifier,
7
+ * scoped by the rule's own `include` / `exclude`.
8
+ * - Structured: `{ forbidden: [...], scope: { include, exclude } }` — each forbidden
9
+ * entry is an exact `specifier` (also matching require/dynamic forms unless
10
+ * `includeRequire:false`) or a raw `pattern`, scoped by `scope.include` /
11
+ * `scope.exclude` globs.
12
+ */
3
13
  export declare class ForbiddenImportEvaluator implements RuleEvaluator {
4
- constructor();
5
- /** Evaluate import declarations against configured forbidden prefixes. */
14
+ /** Evaluate import/usage against the configured forbidden set. */
6
15
  evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
16
+ /** Legacy path: `{ patterns: string[] }`, substring-matched against import specifiers. */
17
+ private evaluateSimple;
18
+ /** Structured path: `{ forbidden: [...], scope: { include, exclude } }`. */
19
+ private evaluateStructured;
7
20
  }
8
21
  //# sourceMappingURL=forbidden-import-evaluator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"forbidden-import-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/forbidden-import-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB,mEAAmE;AACnE,qBAAa,wBAAyB,YAAW,aAAa;;IAG1D,0EAA0E;IACpE,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CA4B5F"}
1
+ {"version":3,"file":"forbidden-import-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/forbidden-import-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAclB;;;;;;;;;;GAUG;AACH,qBAAa,wBAAyB,YAAW,aAAa;IAC1D,kEAAkE;IAC5D,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAOzF,0FAA0F;YAC5E,cAAc;IA+B5B,4EAA4E;YAC9D,kBAAkB;CAoCnC"}
@@ -1,11 +1,26 @@
1
1
  import { createFinding, } from '../types.js';
2
- import { discoverFiles, readWorkdirFile } from './file-utils.js';
3
- /** Detects imports matching forbidden package or path prefixes. */
2
+ import { discoverFiles, matchesGlob, readWorkdirFile } from './file-utils.js';
3
+ /**
4
+ * Detects forbidden imports / API usage.
5
+ *
6
+ * Two config shapes are supported:
7
+ * - Simple: `{ patterns: string[] }` — substring match against any import specifier,
8
+ * scoped by the rule's own `include` / `exclude`.
9
+ * - Structured: `{ forbidden: [...], scope: { include, exclude } }` — each forbidden
10
+ * entry is an exact `specifier` (also matching require/dynamic forms unless
11
+ * `includeRequire:false`) or a raw `pattern`, scoped by `scope.include` /
12
+ * `scope.exclude` globs.
13
+ */
4
14
  export class ForbiddenImportEvaluator {
5
- constructor() { }
6
- /** Evaluate import declarations against configured forbidden prefixes. */
15
+ /** Evaluate import/usage against the configured forbidden set. */
7
16
  async evaluate(rule, context) {
8
17
  const config = rule.evaluator.config ?? {};
18
+ return Array.isArray(config.forbidden)
19
+ ? this.evaluateStructured(rule, context, config)
20
+ : this.evaluateSimple(rule, context, config);
21
+ }
22
+ /** Legacy path: `{ patterns: string[] }`, substring-matched against import specifiers. */
23
+ async evaluateSimple(rule, context, config) {
9
24
  const forbidden = arrayConfig(config, 'patterns');
10
25
  const files = await discoverFiles({
11
26
  workdir: context.workdir,
@@ -16,8 +31,7 @@ export class ForbiddenImportEvaluator {
16
31
  for (const file of files) {
17
32
  const lines = (await readWorkdirFile(context.workdir, file)).split('\n');
18
33
  for (const [index, line] of lines.entries()) {
19
- const imported = /(?:from\s+|import\s*\(|^\s*import\s*)['"](?<specifier>[^'"]+)['"]/.exec(line)?.groups
20
- ?.specifier;
34
+ const imported = importSpecifier(line);
21
35
  if (imported === undefined)
22
36
  continue;
23
37
  const matched = forbidden.find((pattern) => imported.includes(pattern));
@@ -31,6 +45,54 @@ export class ForbiddenImportEvaluator {
31
45
  }
32
46
  return { findings, fixes: [] };
33
47
  }
48
+ /** Structured path: `{ forbidden: [...], scope: { include, exclude } }`. */
49
+ async evaluateStructured(rule, context, config) {
50
+ const scope = config.scope;
51
+ const include = stringArray(scope?.include);
52
+ if (include === undefined) {
53
+ throw new Error('forbidden-import evaluator requires string[] config "scope.include"');
54
+ }
55
+ const exclude = stringArray(scope?.exclude) ?? [];
56
+ const entries = config.forbidden.map(compileEntry);
57
+ // Discover all source files, then apply scope globs precisely (discoverFiles'
58
+ // include matching is intentionally loose, so it cannot do `**`-anchored scoping).
59
+ const files = (await discoverFiles({ workdir: context.workdir }))
60
+ .filter((file) => include.some((glob) => matchesGlob(file, glob)))
61
+ .filter((file) => !exclude.some((glob) => matchesGlob(file, glob)));
62
+ const findings = [];
63
+ for (const file of files) {
64
+ const lines = (await readWorkdirFile(context.workdir, file)).split('\n');
65
+ for (const [index, line] of lines.entries()) {
66
+ const hit = entries.find((entry) => entry.regex.test(line));
67
+ if (hit !== undefined) {
68
+ findings.push(createFinding(rule, `Forbidden import/usage of "${hit.label}"`, file, {
69
+ line: index + 1,
70
+ code: 'import:forbidden',
71
+ }));
72
+ }
73
+ }
74
+ }
75
+ return { findings, fixes: [] };
76
+ }
77
+ }
78
+ /** Extract the specifier from an import/require/dynamic-import line, if any. */
79
+ function importSpecifier(line) {
80
+ return /(?:from\s+|import\s*\(|^\s*import\s*)['"](?<specifier>[^'"]+)['"]/.exec(line)?.groups?.specifier;
81
+ }
82
+ /** Compile a forbidden entry into a line-matching regex. */
83
+ function compileEntry(entry) {
84
+ if ('specifier' in entry) {
85
+ const spec = escapeRegExp(entry.specifier);
86
+ const boundary = `(?:/|['"])`;
87
+ const source = entry.includeRequire === false
88
+ ? `from\\s+['"]${spec}${boundary}`
89
+ : `(?:from\\s+|require\\(\\s*|import\\(\\s*)['"]${spec}${boundary}`;
90
+ return { regex: new RegExp(source), label: entry.specifier };
91
+ }
92
+ return { regex: new RegExp(entry.pattern), label: entry.pattern };
93
+ }
94
+ function escapeRegExp(value) {
95
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34
96
  }
35
97
  function arrayConfig(config, key) {
36
98
  const value = config[key];
@@ -40,3 +102,6 @@ function arrayConfig(config, key) {
40
102
  return [value];
41
103
  throw new Error(`forbidden-import evaluator requires string[] config "${key}"`);
42
104
  }
105
+ function stringArray(value) {
106
+ return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : undefined;
107
+ }
@@ -0,0 +1,19 @@
1
+ import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
2
+ /**
3
+ * Enforces architectural import boundaries without spawning a subprocess.
4
+ *
5
+ * Files matching a boundary's `scope` glob are scanned in-memory. Each forbidden
6
+ * entry is either a string (matched as an import-specifier substring) or an object
7
+ * with a `pattern` regex and an optional `mode` (`import` | `usage`).
8
+ *
9
+ * ## Options (in `evaluator.config`)
10
+ * - `boundaries` — non-empty array of boundary declarations:
11
+ * - `scope` — glob pattern selecting files this boundary applies to.
12
+ * - `forbidden` — array of strings or `{ pattern, mode?, syntax? }` objects.
13
+ * - `exclude` — optional globs within the scope to ignore.
14
+ */
15
+ export declare class ImportBoundaryEvaluator implements RuleEvaluator {
16
+ /** Evaluate import boundaries across all in-scope files. */
17
+ evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
18
+ }
19
+ //# sourceMappingURL=import-boundary-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-boundary-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/import-boundary-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AA4BlB;;;;;;;;;;;;GAYG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IACzD,4DAA4D;IACtD,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAuC5F"}
@@ -0,0 +1,85 @@
1
+ import { createFinding, } from '../types.js';
2
+ import { discoverFiles, matchesGlob, readWorkdirFile } from './file-utils.js';
3
+ /**
4
+ * Enforces architectural import boundaries without spawning a subprocess.
5
+ *
6
+ * Files matching a boundary's `scope` glob are scanned in-memory. Each forbidden
7
+ * entry is either a string (matched as an import-specifier substring) or an object
8
+ * with a `pattern` regex and an optional `mode` (`import` | `usage`).
9
+ *
10
+ * ## Options (in `evaluator.config`)
11
+ * - `boundaries` — non-empty array of boundary declarations:
12
+ * - `scope` — glob pattern selecting files this boundary applies to.
13
+ * - `forbidden` — array of strings or `{ pattern, mode?, syntax? }` objects.
14
+ * - `exclude` — optional globs within the scope to ignore.
15
+ */
16
+ export class ImportBoundaryEvaluator {
17
+ /** Evaluate import boundaries across all in-scope files. */
18
+ async evaluate(rule, context) {
19
+ const config = rule.evaluator.config ?? {};
20
+ const boundaries = config.boundaries;
21
+ if (!Array.isArray(boundaries) || boundaries.length === 0) {
22
+ throw new Error('import-boundary evaluator requires non-empty array config "boundaries"');
23
+ }
24
+ const compiled = boundaries.map((b) => compileBoundary(b));
25
+ // Discover all files once; filter per boundary below.
26
+ const allFiles = await discoverFiles({ workdir: context.workdir });
27
+ const findings = [];
28
+ for (const boundary of compiled) {
29
+ const inScope = allFiles
30
+ .filter((file) => matchesGlob(file, boundary.scope))
31
+ .filter((file) => !boundary.excludePatterns.some((ex) => matchesGlob(file, ex)));
32
+ for (const file of inScope) {
33
+ const content = await readWorkdirFile(context.workdir, file);
34
+ const lines = content.split('\n');
35
+ for (const [index, line] of lines.entries()) {
36
+ for (const entry of boundary.forbidden) {
37
+ if (entry.importOnly && !isImportLine(line))
38
+ continue;
39
+ if (entry.regex.test(line)) {
40
+ findings.push(createFinding(rule, `forbidden in boundary "${boundary.scope}": ${entry.label}`, file, {
41
+ line: index + 1,
42
+ code: 'import-boundary:violation',
43
+ }));
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ return { findings, fixes: [] };
50
+ }
51
+ }
52
+ /** Compile a raw boundary declaration into a scan-ready form. */
53
+ function compileBoundary(decl) {
54
+ return {
55
+ scope: decl.scope,
56
+ excludePatterns: decl.exclude ?? [],
57
+ forbidden: decl.forbidden.map((entry) => compileEntry(entry)),
58
+ };
59
+ }
60
+ /** Compile one forbidden entry into a regex + metadata. */
61
+ function compileEntry(entry) {
62
+ if (typeof entry === 'string') {
63
+ // String form: match as an import specifier substring.
64
+ const escaped = escapeRegExp(entry);
65
+ return {
66
+ regex: new RegExp(`(?:from\\s+|require\\(\\s*|import\\(\\s*)['"](?:[^'"]*)?${escaped}(?:[^'"]*)?['"]`),
67
+ label: entry,
68
+ importOnly: true,
69
+ };
70
+ }
71
+ // Object form with `pattern`.
72
+ const importOnly = (entry.mode ?? 'import') !== 'usage';
73
+ return {
74
+ regex: new RegExp(entry.pattern),
75
+ label: entry.pattern,
76
+ importOnly,
77
+ };
78
+ }
79
+ /** Return true when a source line is an import/export/require/dynamic-import statement. */
80
+ function isImportLine(line) {
81
+ return /(?:^\s*import\b|^\s*export\b.*\bfrom\b|(?:from|require|import)\s*\(?\s*['"])/.test(line);
82
+ }
83
+ function escapeRegExp(value) {
84
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
85
+ }
@@ -1,9 +1,22 @@
1
1
  import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
2
- /** Evaluates file or directory existence constraints. */
2
+ /**
3
+ * Evaluates file/directory presence constraints.
4
+ *
5
+ * Two config shapes are supported:
6
+ * - Glob form: `{ must: 'present' | 'absent' }` scoped by the rule's `include` /
7
+ * `exclude` globs. `present` flags each include glob that matches zero files;
8
+ * `absent` flags each in-scope file that exists.
9
+ * - Explicit form: `{ paths: string[], mode?: 'require' | 'forbid' }` — checks the
10
+ * exact paths relative to the workdir (`require` = must exist, `forbid` = must not).
11
+ */
3
12
  export declare class PathEvaluator implements RuleEvaluator {
4
13
  private readonly fs;
5
14
  constructor();
6
- /** Evaluate required or forbidden paths. */
15
+ /** Evaluate required or forbidden paths in either config form. */
7
16
  evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
17
+ /** Glob form driven by `must` + the rule's include/exclude globs. */
18
+ private evaluateGlob;
19
+ /** Explicit form: exact `paths` checked with `mode` require/forbid. */
20
+ private evaluateExplicit;
8
21
  }
9
22
  //# sourceMappingURL=path-evaluator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/path-evaluator.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAElB,yDAAyD;AACzD,qBAAa,aAAc,YAAW,aAAa;IAC/C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,4CAA4C;IACtC,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAgB5F"}
1
+ {"version":3,"file":"path-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/path-evaluator.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAGlB;;;;;;;;;GASG;AACH,qBAAa,aAAc,YAAW,aAAa;IAC/C,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,kEAAkE;IAC5D,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IASzF,qEAAqE;YACvD,YAAY;IAuC1B,uEAAuE;YACzD,gBAAgB;CAmBjC"}