@gobing-ai/ts-rule-engine 0.3.1 → 0.3.3

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 (86) hide show
  1. package/README.md +328 -58
  2. package/dist/config/extensions.d.ts +10 -7
  3. package/dist/config/extensions.d.ts.map +1 -1
  4. package/dist/config/extensions.js +48 -23
  5. package/dist/config/loader.d.ts +7 -0
  6. package/dist/config/loader.d.ts.map +1 -1
  7. package/dist/config/loader.js +17 -12
  8. package/dist/engine.d.ts +13 -2
  9. package/dist/engine.d.ts.map +1 -1
  10. package/dist/engine.js +107 -45
  11. package/dist/evaluators/agent-detection-evaluator.d.ts.map +1 -1
  12. package/dist/evaluators/agent-detection-evaluator.js +2 -9
  13. package/dist/evaluators/coverage-gate-evaluator.d.ts.map +1 -1
  14. package/dist/evaluators/coverage-gate-evaluator.js +4 -5
  15. package/dist/evaluators/exit-code-evaluator.d.ts.map +1 -1
  16. package/dist/evaluators/exit-code-evaluator.js +6 -23
  17. package/dist/evaluators/file-utils.d.ts +25 -1
  18. package/dist/evaluators/file-utils.d.ts.map +1 -1
  19. package/dist/evaluators/file-utils.js +48 -8
  20. package/dist/evaluators/forbidden-import-evaluator.js +2 -10
  21. package/dist/evaluators/path-evaluator.d.ts.map +1 -1
  22. package/dist/evaluators/path-evaluator.js +5 -18
  23. package/dist/evaluators/regex-evaluator.js +3 -11
  24. package/dist/evaluators/ripgrep-evaluator.d.ts +50 -0
  25. package/dist/evaluators/ripgrep-evaluator.d.ts.map +1 -0
  26. package/dist/evaluators/ripgrep-evaluator.js +145 -0
  27. package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -1
  28. package/dist/evaluators/schema-artifact-evaluator.js +3 -7
  29. package/dist/evaluators/sg-evaluator.d.ts +10 -2
  30. package/dist/evaluators/sg-evaluator.d.ts.map +1 -1
  31. package/dist/evaluators/sg-evaluator.js +21 -4
  32. package/dist/evaluators/test-location-evaluator.d.ts +2 -2
  33. package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
  34. package/dist/evaluators/test-location-evaluator.js +2 -15
  35. package/dist/events.d.ts +33 -0
  36. package/dist/events.d.ts.map +1 -0
  37. package/dist/events.js +0 -0
  38. package/dist/fixers/fixers.d.ts +1 -1
  39. package/dist/fixers/fixers.d.ts.map +1 -1
  40. package/dist/fixers/fixers.js +4 -5
  41. package/dist/fixers/test-stub-fixer.d.ts +1 -1
  42. package/dist/fixers/test-stub-fixer.d.ts.map +1 -1
  43. package/dist/fixers/test-stub-fixer.js +3 -4
  44. package/dist/host/builtins.d.ts.map +1 -1
  45. package/dist/host/builtins.js +5 -3
  46. package/dist/host/bundled-rules.d.ts +1 -1
  47. package/dist/host/bundled-rules.js +1 -1
  48. package/dist/host/rule-engine-host.d.ts +1 -1
  49. package/dist/host/rule-engine-host.d.ts.map +1 -1
  50. package/dist/host/rule-engine-host.js +1 -1
  51. package/dist/index.d.ts +3 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +7 -1
  54. package/dist/types.d.ts +2 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/types.js +6 -0
  57. package/package.json +4 -5
  58. package/rules/example.yaml +13 -0
  59. package/src/config/extensions.ts +58 -29
  60. package/src/config/loader.ts +27 -12
  61. package/src/engine.ts +132 -47
  62. package/src/evaluators/agent-detection-evaluator.ts +2 -8
  63. package/src/evaluators/coverage-gate-evaluator.ts +4 -5
  64. package/src/evaluators/exit-code-evaluator.ts +6 -23
  65. package/src/evaluators/file-utils.ts +70 -8
  66. package/src/evaluators/forbidden-import-evaluator.ts +2 -9
  67. package/src/evaluators/path-evaluator.ts +5 -18
  68. package/src/evaluators/regex-evaluator.ts +4 -11
  69. package/src/evaluators/ripgrep-evaluator.ts +167 -0
  70. package/src/evaluators/schema-artifact-evaluator.ts +3 -8
  71. package/src/evaluators/sg-evaluator.ts +21 -4
  72. package/src/evaluators/test-location-evaluator.ts +3 -16
  73. package/src/events.ts +13 -0
  74. package/src/fixers/fixers.ts +12 -6
  75. package/src/fixers/test-stub-fixer.ts +4 -5
  76. package/src/host/builtins.ts +5 -3
  77. package/src/host/bundled-rules.ts +1 -1
  78. package/src/host/rule-engine-host.ts +1 -1
  79. package/src/index.ts +8 -1
  80. package/src/types.ts +7 -0
  81. package/dist/host/capability-registry.d.ts +0 -10
  82. package/dist/host/capability-registry.d.ts.map +0 -1
  83. package/dist/host/capability-registry.js +0 -9
  84. package/rules/recommended.yaml +0 -10
  85. package/rules/spur-dev.yaml +0 -6
  86. package/src/host/capability-registry.ts +0 -9
@@ -11,7 +11,7 @@
11
11
  * @module rule-engine/fixers/test-stub-fixer
12
12
  */
13
13
  import { type ProcessExecutor } from '@gobing-ai/ts-runtime';
14
- import type { CapabilityRegistry } from '../host/capability-registry';
14
+ import type { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
15
15
  import type { TestPathResolver } from '../resolvers/test-path-resolver';
16
16
  import type { Fix } from '../types';
17
17
  import type { RuleFixerInput, RuleFixerProvider } from './fixers';
@@ -1 +1 @@
1
- {"version":3,"file":"test-stub-fixer.d.ts","sourceRoot":"","sources":["../../src/fixers/test-stub-fixer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,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,EAA4C,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACvG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACvE,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"}
@@ -10,13 +10,12 @@
10
10
  *
11
11
  * @module rule-engine/fixers/test-stub-fixer
12
12
  */
13
- import { isAbsolute, join } from 'node:path';
14
- import { NodeFileSystem } from '@gobing-ai/ts-runtime';
13
+ import { isAbsolutePath, joinPath, NodeFileSystem } from '@gobing-ai/ts-runtime';
15
14
  /** Real file-system implementation backed by the runtime FileSystem seam. */
16
15
  export const realFileSystem = new NodeFileSystem();
17
16
  /** Normalize a finding path to a repository-relative POSIX path, or return null when invalid. */
18
17
  function normalizeFindingPath(filePath) {
19
- if (!filePath || isAbsolute(filePath))
18
+ if (!filePath || isAbsolutePath(filePath))
20
19
  return null;
21
20
  const normalized = filePath.replaceAll('\\', '/').replace(/^\.\//, '');
22
21
  if (normalized === '..' || normalized.startsWith('../') || normalized.includes('/../'))
@@ -68,7 +67,7 @@ export class TestStubFixer {
68
67
  catch {
69
68
  continue;
70
69
  }
71
- const absTestPath = join(context.workdir, testRelPath);
70
+ const absTestPath = joinPath(context.workdir, testRelPath);
72
71
  if (await this.fs.exists(absTestPath))
73
72
  continue;
74
73
  // Export discovery is intentionally omitted — pass empty exports array.
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/host/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAqB7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAuBvF"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/host/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAsB7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAwBvF"}
@@ -5,6 +5,7 @@ import { ForbiddenImportEvaluator } from '../evaluators/forbidden-import-evaluat
5
5
  import { ImportBoundaryEvaluator } from '../evaluators/import-boundary-evaluator.js';
6
6
  import { PathEvaluator } from '../evaluators/path-evaluator.js';
7
7
  import { RegexEvaluator } from '../evaluators/regex-evaluator.js';
8
+ import { RipgrepEvaluator } from '../evaluators/ripgrep-evaluator.js';
8
9
  import { SchemaArtifactEvaluator } from '../evaluators/schema-artifact-evaluator.js';
9
10
  import { SecretsScannerEvaluator } from '../evaluators/secrets-scanner-evaluator.js';
10
11
  import { SgEvaluator } from '../evaluators/sg-evaluator.js';
@@ -15,10 +16,11 @@ import { TextFormatter } from '../formatters/text.js';
15
16
  import { GoTestPathResolver, PythonTestPathResolver, RustTestPathResolver, TypeScriptTestPathResolver, } from '../resolvers/test-path-resolver.js';
16
17
  /** Register bundled evaluators and formatters on a host. */
17
18
  export function registerBuiltins(host, executor) {
18
- const regex = new RegexEvaluator();
19
19
  const path = new PathEvaluator();
20
- host.evaluators.register('regex', regex, 'builtin');
21
- host.evaluators.register('rg', regex, 'builtin');
20
+ // `regex` = JS RegExp engine; `rg` = real ripgrep engine (ReDoS-immune, prunes during
21
+ // traversal). The two are honestly named distinct engines, not aliases.
22
+ host.evaluators.register('regex', new RegexEvaluator(), 'builtin');
23
+ host.evaluators.register('rg', new RipgrepEvaluator(executor), 'builtin');
22
24
  host.evaluators.register('path', path, 'builtin');
23
25
  host.evaluators.register('file-exist', path, 'builtin');
24
26
  host.evaluators.register('forbidden-import', new ForbiddenImportEvaluator(), 'builtin');
@@ -2,7 +2,7 @@
2
2
  * Resolve the absolute path to the rule presets bundled with
3
3
  * `@gobing-ai/ts-rule-engine`.
4
4
  *
5
- * The directory ships portable presets (`recommended`, `spur-dev`) and category
5
+ * The directory ships a generic example preset and category
6
6
  * folders (`typescript`, `structure`, `quality`) so a consumer gets a working
7
7
  * default ruleset without authoring any files. Pass the returned path as the
8
8
  * lowest-priority entry to {@link loadPreset}'s `roots`, letting project-local
@@ -12,7 +12,7 @@ const defaultFs = new NodeSyncFileSystem();
12
12
  * Resolve the absolute path to the rule presets bundled with
13
13
  * `@gobing-ai/ts-rule-engine`.
14
14
  *
15
- * The directory ships portable presets (`recommended`, `spur-dev`) and category
15
+ * The directory ships a generic example preset and category
16
16
  * folders (`typescript`, `structure`, `quality`) so a consumer gets a working
17
17
  * default ruleset without authoring any files. Pass the returned path as the
18
18
  * lowest-priority entry to {@link loadPreset}'s `roots`, letting project-local
@@ -1,6 +1,6 @@
1
+ import { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
1
2
  import type { TestPathResolver } from '../resolvers/test-path-resolver';
2
3
  import type { ResultFormatter, RuleEvaluator } from '../types';
3
- import { CapabilityRegistry } from './capability-registry';
4
4
  /** Host container for rule-engine capabilities. */
5
5
  export declare class RuleEngineHost {
6
6
  /** Evaluator registry keyed by evaluator type. */
@@ -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,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"}
1
+ {"version":3,"file":"rule-engine-host.d.ts","sourceRoot":"","sources":["../../src/host/rule-engine-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE/D,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"}
@@ -1,4 +1,4 @@
1
- import { CapabilityRegistry } from './capability-registry.js';
1
+ import { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
2
2
  /** Host container for rule-engine capabilities. */
3
3
  export class RuleEngineHost {
4
4
  /** Evaluator registry keyed by evaluator type. */
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
+ export { type CapabilityEntry, type CapabilityOrigin, CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
1
2
  export * from './config/extensions';
2
3
  export * from './config/loader';
3
4
  export * from './engine';
5
+ export { isRipgrepCompatiblePattern } from './evaluators/ripgrep-evaluator';
6
+ export type { RuleEngineEvents } from './events';
4
7
  export * from './fixers/fixers';
5
8
  export * from './fixers/test-stub-fixer';
6
9
  export * from './formatters/json';
7
10
  export * from './formatters/text';
8
11
  export * from './host/bundled-rules';
9
- export * from './host/capability-registry';
10
12
  export * from './host/rule-engine-host';
11
13
  export * from './resolvers/test-path-resolver';
12
14
  export * from './types';
@@ -1 +1 @@
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,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAC/G,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAC5E,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,12 +1,18 @@
1
+ // Public re-export of the shared plugin registry (ADR-010). Sourced directly from
2
+ // the shared plugin core; the former host/capability-registry.ts shim was removed
3
+ // once the one-release back-compat window closed.
4
+ export { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
1
5
  export * from './config/extensions.js';
2
6
  export * from './config/loader.js';
3
7
  export * from './engine.js';
8
+ // Public dialect helper for the `rg` (ripgrep) evaluator — consumed by the downstream
9
+ // rule-file converter and the rg-dialect rule to keep JS-only patterns off the `rg` type.
10
+ export { isRipgrepCompatiblePattern } from './evaluators/ripgrep-evaluator.js';
4
11
  export * from './fixers/fixers.js';
5
12
  export * from './fixers/test-stub-fixer.js';
6
13
  export * from './formatters/json.js';
7
14
  export * from './formatters/text.js';
8
15
  export * from './host/bundled-rules.js';
9
- export * from './host/capability-registry.js';
10
16
  export * from './host/rule-engine-host.js';
11
17
  export * from './resolvers/test-path-resolver.js';
12
18
  export * from './types.js';
package/dist/types.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  /** Finding severity emitted by the rule engine. */
3
3
  export type RuleSeverity = 'error' | 'warning' | 'info';
4
+ /** Numeric rank for severity comparison; higher = more severe. */
5
+ export declare const SEVERITY_RANK: Record<RuleSeverity, number>;
4
6
  /** Fix authority level for candidate fixes. */
5
7
  export type FixMode = 'none' | 'suggest' | 'auto';
6
8
  /** Structured configuration for a rule evaluator. */
@@ -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
@@ -1,5 +1,11 @@
1
1
  import { assertRelativeExtensionPath } from '@gobing-ai/ts-runtime/plugin';
2
2
  import { z } from 'zod';
3
+ /** Numeric rank for severity comparison; higher = more severe. */
4
+ export const SEVERITY_RANK = {
5
+ error: 3,
6
+ warning: 2,
7
+ info: 1,
8
+ };
3
9
  /** Create a finding with inherited rule severity. */
4
10
  export function createFinding(rule, message, filePath, extras = {}) {
5
11
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-rule-engine",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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,9 @@
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.1",
53
- "@gobing-ai/ts-db": "^0.3.1",
54
- "@gobing-ai/ts-runtime": "^0.3.1",
55
- "@gobing-ai/ts-utils": "^0.3.1",
52
+ "@gobing-ai/ts-ai-runner": "^0.3.3",
53
+ "@gobing-ai/ts-infra": "^0.3.3",
54
+ "@gobing-ai/ts-runtime": "^0.3.3",
56
55
  "yaml": "^2.7.0",
57
56
  "zod": "^4.1.0"
58
57
  },
@@ -0,0 +1,13 @@
1
+ # Example preset — demonstrates the extends mechanism by composing all three
2
+ # bundled rule categories (typescript, quality, structure).
3
+ #
4
+ # Load with: loadPreset('example', { roots: [bundledRulesRoot()] })
5
+ #
6
+ # Bundled with @gobing-ai/ts-rule-engine. A project may override individual rule
7
+ # files by placing same-named files under a local rules root that shadows the
8
+ # bundled copy.
9
+ name: example
10
+ extends:
11
+ - typescript
12
+ - quality
13
+ - structure
@@ -1,6 +1,12 @@
1
- import { resolve } from 'node:path';
1
+ import { fileURLToPath } from 'node:url';
2
+ import type { Logger } from '@gobing-ai/ts-infra';
3
+ import { basenamePath, dirnamePath, resolvePath, SEP } from '@gobing-ai/ts-runtime';
4
+ import type {
5
+ ExtensionRef as SharedExtensionRef,
6
+ LoadExtensionsOptions as SharedLoadExtensionsOptions,
7
+ } from '@gobing-ai/ts-runtime/plugin';
8
+ import { loadExtensionModules } from '@gobing-ai/ts-runtime/plugin';
2
9
  import type { RuleEngineHost } from '../host/rule-engine-host';
3
-
4
10
  /** A capability kind a preset extension can contribute. */
5
11
  export type ExtensionKind = 'resolvers' | 'evaluators' | 'fixers' | 'formatters';
6
12
 
@@ -23,7 +29,7 @@ export interface LoadExtensionsOptions {
23
29
  */
24
30
  allowExtensions?: boolean;
25
31
  /** Optional sink for non-fatal warnings (e.g. built-in overrides). */
26
- logger?: { warn: (message: string) => void };
32
+ logger?: Pick<Logger, 'warn'>;
27
33
  /** Optional module loader seam for tests or embedders with custom import policy. */
28
34
  moduleLoader?: (absPath: string) => Promise<Record<string, unknown>>;
29
35
  }
@@ -51,7 +57,7 @@ export function collectExtensions(
51
57
  const refs: ExtensionRef[] = [];
52
58
  for (const kind of ['resolvers', 'evaluators', 'fixers', 'formatters'] as ExtensionKind[]) {
53
59
  for (const path of extensions[kind] ?? []) {
54
- refs.push({ kind, presetName: sourceName, absPath: resolve(sourceDir, path) });
60
+ refs.push({ kind, presetName: sourceName, absPath: resolvePath(sourceDir, path) });
55
61
  }
56
62
  }
57
63
  return refs;
@@ -61,13 +67,17 @@ export function collectExtensions(
61
67
  * Import each extension module and register its export on the matching host
62
68
  * registry.
63
69
  *
64
- * A module must default-export (or named-export `extension`) an object with a
65
- * `name: string` and the capability implementation. Loading is gated by
70
+ * Delegates generic loading (trust gate, path guard, module import, export-shape
71
+ * validation) to the shared ``loadExtensionModules`` from ts-runtime/plugin, then
72
+ * routes each capability to the correct host registry based on ``ref.kind``.
73
+ *
74
+ * A module must default-export (or named-export ``extension``) an object with a
75
+ * ``name: string`` and the capability implementation. Loading is gated by
66
76
  * {@link LoadExtensionsOptions.allowExtensions}; when refs are present but
67
77
  * loading is not allowed, this throws so the requirement is never silently dropped.
68
78
  *
69
- * @throws When extensions are present but `allowExtensions` is not true, or when
70
- * a module cannot be imported or lacks a valid `name`.
79
+ * @throws When extensions are present but ``allowExtensions`` is not true, or when
80
+ * a module cannot be imported or lacks a valid ``name``.
71
81
  */
72
82
  export async function loadExtensionsIntoHost(
73
83
  host: RuleEngineHost,
@@ -75,40 +85,59 @@ export async function loadExtensionsIntoHost(
75
85
  options: LoadExtensionsOptions = {},
76
86
  ): Promise<void> {
77
87
  if (refs.length === 0) return;
78
- if (options.allowExtensions !== true) {
79
- const first = refs[0] as ExtensionRef;
80
- throw new Error(
81
- `preset "${first.presetName}" declares ${first.kind} extension "${first.absPath}", but extensions are disabled — pass allowExtensions: true to load preset extension modules`,
82
- );
83
- }
84
88
 
85
- const loadModule = options.moduleLoader ?? defaultModuleLoader;
89
+ // Normalize file:// URLs to file paths so dirname/basename produce valid
90
+ // path components that isAbsolute() accepts (e.g. import.meta.url in tests).
91
+ const toFilePath = (p: string): string => (p.startsWith('file://') ? fileURLToPath(p) : p);
92
+
93
+ // Defense-in-depth: pre-validate `..` traversal in caller-supplied absPath
94
+ // before the basename adaptation strips it. The shared loader's
95
+ // assertRelativeExtensionPath also runs on the derived (basename) path.
86
96
  for (const ref of refs) {
87
- const moduleExports = await loadModule(ref.absPath);
88
- const candidate = moduleExports.default ?? moduleExports.extension;
89
- if (
90
- candidate === null ||
91
- typeof candidate !== 'object' ||
92
- typeof (candidate as { name?: unknown }).name !== 'string'
93
- ) {
97
+ const segments = toFilePath(ref.absPath).split(SEP);
98
+ if (segments.includes('..')) {
94
99
  throw new Error(
95
- `preset "${ref.presetName}" extension "${ref.absPath}" must export an object with a string "name"`,
100
+ `extension path "${ref.absPath}" declared by "${ref.presetName}" must not contain ".." traversal`,
96
101
  );
97
102
  }
98
- const name = (candidate as { name: string }).name;
99
- const registryKey = HOST_REGISTRY_BY_KIND[ref.kind];
103
+ }
104
+
105
+ // Adapt rule-engine ExtensionRef → shared ExtensionRef so the generic loader
106
+ // governs every import. The shared loader resolves (baseDir, path) →
107
+ // absPath internally; we supply dirname/basename so the resolved path
108
+ // reconstructs the caller's original absPath.
109
+ const sharedRefs: SharedExtensionRef<ExtensionKind>[] = refs.map((ref) => {
110
+ const fp = toFilePath(ref.absPath);
111
+ return {
112
+ kind: ref.kind,
113
+ path: `./${basenamePath(fp)}`,
114
+ baseDir: dirnamePath(fp),
115
+ sourceName: ref.presetName,
116
+ };
117
+ });
118
+
119
+ const moduleLoader = options.moduleLoader ?? defaultModuleLoader;
120
+ const sharedOptions: SharedLoadExtensionsOptions = {
121
+ allowExtensions: options.allowExtensions,
122
+ logger: options.logger,
123
+ moduleLoader,
124
+ };
125
+
126
+ await loadExtensionModules<ExtensionKind>(sharedRefs, sharedOptions, async (sharedRef, extension) => {
127
+ const name = extension.name as string;
128
+ const registryKey = HOST_REGISTRY_BY_KIND[sharedRef.kind];
100
129
  if (registryKey === undefined) {
101
- throw new Error(`preset "${ref.presetName}" ${ref.kind} extensions are not supported`);
130
+ throw new Error(`"${sharedRef.sourceName}" ${sharedRef.kind} extensions are not supported`);
102
131
  }
103
132
  const registry = host[registryKey] as unknown as {
104
133
  register: (name: string, impl: unknown, origin: 'builtin' | 'extension') => void;
105
134
  has?: (name: string) => boolean;
106
135
  };
107
136
  if (options.logger && registry.has?.(name)) {
108
- options.logger.warn(`preset "${ref.presetName}" ${ref.kind} extension overrides existing "${name}"`);
137
+ options.logger.warn(`"${sharedRef.sourceName}" ${sharedRef.kind} extension overrides existing "${name}"`);
109
138
  }
110
- registry.register(name, candidate, 'extension');
111
- }
139
+ registry.register(name, extension, 'extension');
140
+ });
112
141
  }
113
142
 
114
143
  async function defaultModuleLoader(absPath: string): Promise<Record<string, unknown>> {
@@ -1,5 +1,13 @@
1
- import { basename, dirname, join, relative, resolve, sep } from 'node:path';
2
- import { loadStructuredConfig, NodeFileSystem } from '@gobing-ai/ts-runtime';
1
+ import {
2
+ basenamePath,
3
+ dirnamePath,
4
+ joinPath,
5
+ loadStructuredConfig,
6
+ NodeFileSystem,
7
+ relativePath,
8
+ resolvePath,
9
+ SEP,
10
+ } from '@gobing-ai/ts-runtime';
3
11
  import {
4
12
  type ConstraintRule,
5
13
  type ConstraintRuleFile,
@@ -27,6 +35,7 @@ export interface RuleLoaderOptions {
27
35
  fetch?: (input: string) => Promise<Response>;
28
36
  }
29
37
 
38
+ /** Options for loading a single rule file directly from disk. */
30
39
  export interface RuleFileLoadOptions {
31
40
  /** When true, honor top-level `$schema` refs. Defaults to true. */
32
41
  validateSchema?: boolean;
@@ -58,12 +67,12 @@ interface MergedRoots {
58
67
  * the rest of a preset's categories from the lower-priority roots.
59
68
  */
60
69
  export async function loadPreset(name: string, options: RuleLoaderOptions): Promise<LoadedPreset> {
61
- const merged = await buildMergedRoots(options.roots.map((root) => resolve(root)));
70
+ const merged = await buildMergedRoots(options.roots.map((root) => resolvePath(root)));
62
71
  const presetPath = findMergedPreset(merged, name);
63
72
  if (presetPath === null) return { rules: [], extensions: [] };
64
73
  const preset = PresetDefinitionSchema.parse(await readStructuredFile(presetPath, options)) as PresetDefinition;
65
74
  const rules: ConstraintRule[] = [];
66
- const extensions = collectExtensions(preset.name, dirname(presetPath), preset.extensions);
75
+ const extensions = collectExtensions(preset.name, dirnamePath(presetPath), preset.extensions);
67
76
  for (const entry of preset.extends) {
68
77
  const loaded = await loadPresetEntry(merged, entry, new Set([name]), options);
69
78
  rules.push(...loaded.rules);
@@ -111,6 +120,12 @@ function assertFixModeNotPromoted(presetName: string, rule: ConstraintRule, over
111
120
  }
112
121
  }
113
122
 
123
+ /**
124
+ * Load a preset's resolved rules (convenience wrapper).
125
+ *
126
+ * Shortcut for `(await loadPreset(name, options)).rules` — returns only the
127
+ * normalized rule set without the extension module refs.
128
+ */
114
129
  export async function loadPresetRules(name: string, options: RuleLoaderOptions): Promise<ConstraintRule[]> {
115
130
  return (await loadPreset(name, options)).rules;
116
131
  }
@@ -125,12 +140,12 @@ export async function loadPresetRules(name: string, options: RuleLoaderOptions):
125
140
  * object, not a `rules:` array) cannot declare extensions and yields `extensions: []`.
126
141
  */
127
142
  export async function loadRuleFile(filePath: string, options: RuleFileLoadOptions = {}): Promise<LoadedPreset> {
128
- const resolved = resolve(filePath);
143
+ const resolved = resolvePath(filePath);
129
144
  const raw = await readStructuredFile(resolved, options);
130
145
  const rules = normalizeRuleFile(raw, resolved);
131
146
  const parsed = ConstraintRuleFileSchema.safeParse(raw);
132
147
  const extensions = parsed.success
133
- ? collectExtensions(basename(resolved), dirname(resolved), parsed.data.extensions)
148
+ ? collectExtensions(basenamePath(resolved), dirnamePath(resolved), parsed.data.extensions)
134
149
  : [];
135
150
  return { rules, extensions };
136
151
  }
@@ -151,7 +166,7 @@ async function loadPresetEntry(
151
166
  const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath, options));
152
167
  if (preset.success) {
153
168
  const rules: ConstraintRule[] = [];
154
- const extensions = collectExtensions(preset.data.name, dirname(presetPath), preset.data.extensions);
169
+ const extensions = collectExtensions(preset.data.name, dirnamePath(presetPath), preset.data.extensions);
155
170
  for (const child of preset.data.extends) {
156
171
  const loaded = await loadPresetEntry(merged, child, nextSeen, options);
157
172
  rules.push(...loaded.rules);
@@ -196,7 +211,7 @@ async function buildMergedRoots(roots: readonly string[]): Promise<MergedRoots>
196
211
  const categories = new Set<string>();
197
212
  for (const root of roots) {
198
213
  for (const absPath of await walkYamlFiles(fs, root)) {
199
- const relPath = relative(root, absPath).split(sep).join('/');
214
+ const relPath = relativePath(root, absPath).split(SEP).join('/');
200
215
  const slashIdx = relPath.indexOf('/');
201
216
  if (slashIdx > 0) categories.add(relPath.slice(0, slashIdx));
202
217
  if (!files.has(relPath)) files.set(relPath, absPath);
@@ -249,7 +264,7 @@ async function walkYamlFiles(fs: NodeFileSystem, dir: string, depth = 0): Promis
249
264
  const acc: string[] = [];
250
265
  for (const entry of (await fs.readDir(dir)).sort()) {
251
266
  if (depth === 0 && entry === 'presets') continue;
252
- const fullPath = join(dir, entry);
267
+ const fullPath = joinPath(dir, entry);
253
268
  const entryStat = await fs.stat(fullPath);
254
269
  if (entryStat?.isDirectory()) {
255
270
  acc.push(...(await walkYamlFiles(fs, fullPath, depth + 1)));
@@ -267,7 +282,7 @@ async function listImmediateDirs(fs: NodeFileSystem, dir: string): Promise<strin
267
282
  const dirs: string[] = [];
268
283
  for (const entry of await fs.readDir(dir)) {
269
284
  if (entry === 'presets') continue;
270
- const entryStat = await fs.stat(join(dir, entry));
285
+ const entryStat = await fs.stat(joinPath(dir, entry));
271
286
  if (entryStat?.isDirectory()) dirs.push(entry);
272
287
  }
273
288
  return dirs;
@@ -287,7 +302,7 @@ async function readStructuredFile(
287
302
  type ParsedRule = Omit<ConstraintRule, 'severity'> & { severity?: ConstraintRule['severity'] };
288
303
 
289
304
  function normalizeRuleFile(raw: unknown, filePath: string): ConstraintRule[] {
290
- const sourceDir = dirname(filePath);
305
+ const sourceDir = dirnamePath(filePath);
291
306
  const maybeFile = ConstraintRuleFileSchema.safeParse(raw);
292
307
  if (maybeFile.success) return normalizeFileRules(maybeFile.data, sourceDir);
293
308
  const maybeRule = ConstraintRuleSchema.safeParse(raw);
@@ -297,7 +312,7 @@ function normalizeRuleFile(raw: unknown, filePath: string): ConstraintRule[] {
297
312
  // single-rule schema. Include field paths so the offending key is obvious.
298
313
  const isRuleFileShape = typeof raw === 'object' && raw !== null && 'rules' in raw;
299
314
  const issues = (isRuleFileShape ? maybeFile.error : maybeRule.error).issues;
300
- throw new Error(`Invalid rule file "${basename(filePath)}": ${formatIssues(issues)}`);
315
+ throw new Error(`Invalid rule file "${basenamePath(filePath)}": ${formatIssues(issues)}`);
301
316
  }
302
317
 
303
318
  /** Render Zod issues as `path: message` fragments for actionable diagnostics. */