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

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 (80) hide show
  1. package/README.md +563 -1
  2. package/dist/config/extensions.d.ts +48 -0
  3. package/dist/config/extensions.d.ts.map +1 -0
  4. package/dist/config/extensions.js +67 -0
  5. package/dist/config/loader.d.ts +20 -1
  6. package/dist/config/loader.d.ts.map +1 -1
  7. package/dist/config/loader.js +52 -26
  8. package/dist/engine.d.ts +26 -1
  9. package/dist/engine.d.ts.map +1 -1
  10. package/dist/engine.js +79 -0
  11. package/dist/evaluators/exit-code-evaluator.d.ts +1 -1
  12. package/dist/evaluators/exit-code-evaluator.d.ts.map +1 -1
  13. package/dist/evaluators/exit-code-evaluator.js +22 -9
  14. package/dist/evaluators/forbidden-import-evaluator.d.ts +16 -3
  15. package/dist/evaluators/forbidden-import-evaluator.d.ts.map +1 -1
  16. package/dist/evaluators/forbidden-import-evaluator.js +71 -6
  17. package/dist/evaluators/import-boundary-evaluator.d.ts +19 -0
  18. package/dist/evaluators/import-boundary-evaluator.d.ts.map +1 -0
  19. package/dist/evaluators/import-boundary-evaluator.js +85 -0
  20. package/dist/evaluators/path-evaluator.d.ts +15 -2
  21. package/dist/evaluators/path-evaluator.d.ts.map +1 -1
  22. package/dist/evaluators/path-evaluator.js +49 -3
  23. package/dist/evaluators/regex-evaluator.d.ts.map +1 -1
  24. package/dist/evaluators/regex-evaluator.js +43 -8
  25. package/dist/evaluators/schema-artifact-evaluator.d.ts +21 -0
  26. package/dist/evaluators/schema-artifact-evaluator.d.ts.map +1 -0
  27. package/dist/evaluators/schema-artifact-evaluator.js +102 -0
  28. package/dist/evaluators/secrets-scanner-evaluator.d.ts +13 -2
  29. package/dist/evaluators/secrets-scanner-evaluator.d.ts.map +1 -1
  30. package/dist/evaluators/secrets-scanner-evaluator.js +72 -9
  31. package/dist/evaluators/sg-evaluator.d.ts +19 -0
  32. package/dist/evaluators/sg-evaluator.d.ts.map +1 -0
  33. package/dist/evaluators/sg-evaluator.js +112 -0
  34. package/dist/evaluators/test-location-evaluator.d.ts +14 -1
  35. package/dist/evaluators/test-location-evaluator.d.ts.map +1 -1
  36. package/dist/evaluators/test-location-evaluator.js +42 -22
  37. package/dist/evaluators/tsdoc-export-evaluator.js +19 -5
  38. package/dist/fixers/fixers.d.ts +86 -0
  39. package/dist/fixers/fixers.d.ts.map +1 -0
  40. package/dist/fixers/fixers.js +230 -0
  41. package/dist/fixers/test-stub-fixer.d.ts +49 -0
  42. package/dist/fixers/test-stub-fixer.d.ts.map +1 -0
  43. package/dist/fixers/test-stub-fixer.js +91 -0
  44. package/dist/host/builtins.d.ts.map +1 -1
  45. package/dist/host/builtins.js +12 -1
  46. package/dist/host/rule-engine-host.d.ts +3 -0
  47. package/dist/host/rule-engine-host.d.ts.map +1 -1
  48. package/dist/host/rule-engine-host.js +3 -0
  49. package/dist/index.d.ts +4 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +4 -0
  52. package/dist/resolvers/test-path-resolver.d.ts +72 -0
  53. package/dist/resolvers/test-path-resolver.d.ts.map +1 -0
  54. package/dist/resolvers/test-path-resolver.js +112 -0
  55. package/dist/types.d.ts +36 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js +10 -0
  58. package/package.json +4 -3
  59. package/schemas/preset.schema.json +40 -0
  60. package/schemas/rule-file.schema.json +49 -0
  61. package/src/config/extensions.ts +115 -0
  62. package/src/config/loader.ts +81 -25
  63. package/src/engine.ts +99 -2
  64. package/src/evaluators/exit-code-evaluator.ts +27 -9
  65. package/src/evaluators/forbidden-import-evaluator.ts +101 -7
  66. package/src/evaluators/import-boundary-evaluator.ts +135 -0
  67. package/src/evaluators/path-evaluator.ts +66 -3
  68. package/src/evaluators/regex-evaluator.ts +53 -12
  69. package/src/evaluators/schema-artifact-evaluator.ts +134 -0
  70. package/src/evaluators/secrets-scanner-evaluator.ts +89 -11
  71. package/src/evaluators/sg-evaluator.ts +133 -0
  72. package/src/evaluators/test-location-evaluator.ts +47 -35
  73. package/src/evaluators/tsdoc-export-evaluator.ts +19 -5
  74. package/src/fixers/fixers.ts +294 -0
  75. package/src/fixers/test-stub-fixer.ts +118 -0
  76. package/src/host/builtins.ts +17 -1
  77. package/src/host/rule-engine-host.ts +4 -0
  78. package/src/index.ts +4 -0
  79. package/src/resolvers/test-path-resolver.ts +133 -0
  80. package/src/types.ts +40 -0
package/README.md CHANGED
@@ -1,3 +1,565 @@
1
1
  # @gobing-ai/ts-rule-engine
2
2
 
3
- Constraint rule schemas, preset loading, evaluator orchestration, and result formatting for TypeScript/Bun projects.
3
+ Constraint rule loading, evaluation, formatting, and fix generation for Bun/TypeScript projects.
4
+
5
+ This package is a library. It does not ship a CLI. Downstream tools can use it to load rule presets, evaluate a workspace, format findings, collect fix candidates, and optionally apply those fixes.
6
+
7
+ ## Briefing
8
+
9
+ `ts-rule-engine` is a policy workflow engine: loaders turn rule files and presets into `ConstraintRule` objects, `RuleEngine` dispatches each rule to a matching evaluator through `RuleEngineHost`, and the result can be formatted for users or converted into fix candidates for controlled application.
10
+
11
+ ```mermaid
12
+ erDiagram
13
+ RULE_ENGINE ||--|| RULE_ENGINE_HOST : uses
14
+ RULE_ENGINE ||--o{ CONSTRAINT_RULE : evaluates
15
+ RULE_ENGINE_HOST ||--o{ EVALUATOR : registers
16
+ RULE_ENGINE_HOST ||--o{ RESOLVER : registers
17
+ RULE_ENGINE_HOST ||--o{ FORMATTER : registers
18
+ RULE_ENGINE ||--o{ FIXER : owns
19
+ PRESET ||--o{ CONSTRAINT_RULE : composes
20
+ PRESET ||--o{ EXTENSION : declares
21
+ EXTENSION }o--|| RULE_ENGINE_HOST : loads_into
22
+ EVALUATOR ||--o{ FINDING : emits
23
+ FIXER ||--o{ FIX : emits
24
+ FORMATTER ||--|| RESULT : renders
25
+ ```
26
+
27
+ | Entity | One-line Description |
28
+ |--------|----------------------|
29
+ | `RuleEngine` | Orchestrates the evaluation workflow for enabled rules in a target workspace. |
30
+ | `RuleEngineHost` | Holds named capability registries for evaluators, resolvers, and formatters. |
31
+ | `ConstraintRule` | Declarative policy unit with matching scope, severity, evaluator config, and optional fix config. |
32
+ | `Preset` | YAML/JSON composition unit that collects rule categories, other presets, disabled rules, and extension refs. |
33
+ | `Evaluator` | Executes one rule type and emits findings plus any evaluator-native fixes. |
34
+ | `Resolver` | Maps source paths to related test paths or skeletons for rules such as `test-location`. |
35
+ | `Formatter` | Renders a `RuleEngineResult` as text, JSON, or a downstream custom report format. |
36
+ | `Fixer` | Produces file edits from findings when a rule opts into manual or automatic fix modes. |
37
+ | `Extension` | Trusted local module declared by presets to register custom resolvers, evaluators, or formatters. |
38
+ | `Finding` | Structured policy violation or evaluator error with severity, location, and machine-readable code. |
39
+ | `Fix` | Candidate byte-range or file-level edit returned separately from findings and applied only on request. |
40
+ | `RuleEngineResult` | Aggregate output containing all findings and fixes for one evaluation run. |
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ bun add @gobing-ai/ts-rule-engine
46
+ ```
47
+
48
+ ## Mental Model
49
+
50
+ ```mermaid
51
+ flowchart TD
52
+ Files["YAML / JSON rule files"] --> Loader["loadRuleFile() / loadPreset()"]
53
+ Loader --> Rules["ConstraintRule[]"]
54
+ Rules --> Engine["RuleEngine"]
55
+ Engine --> Host["RuleEngineHost registries"]
56
+ Host --> Evaluators["evaluators by type"]
57
+ Host --> Resolvers["test path resolvers"]
58
+ Host --> Formatters["text / json formatters"]
59
+ Engine --> Result["{ findings, fixes }"]
60
+ Result --> Formatter["TextFormatter / JsonFormatter"]
61
+ Result --> Apply["applyFixes()"]
62
+ ```
63
+
64
+ Core concepts:
65
+
66
+ - `ConstraintRule`: one policy check. It has an `id`, `severity`, evaluator type/config, optional include/exclude globs, and optional fix config.
67
+ - `RuleEngine`: runs enabled rules against a `workdir`.
68
+ - `RuleEngineHost`: registry container for evaluators, formatters, and test-path resolvers.
69
+ - `RuleEvaluator`: implementation of one rule type, such as `regex`, `path`, or `coverage-gate`.
70
+ - `Fix`: byte-range replacement candidate. Fixes are collected separately from findings and are only written when you call `applyFixes()`.
71
+ - Preset: YAML/JSON file that composes rule categories and can expose extension modules.
72
+
73
+ ## Quick Start
74
+
75
+ ```ts
76
+ import { RuleEngine, TextFormatter, type ConstraintRule } from '@gobing-ai/ts-rule-engine';
77
+
78
+ const rules: ConstraintRule[] = [
79
+ {
80
+ id: 'no-console-log',
81
+ description: 'Do not commit console.log calls',
82
+ enabled: true,
83
+ severity: 'error',
84
+ include: ['src/**/*.ts'],
85
+ evaluator: {
86
+ type: 'regex',
87
+ config: {
88
+ mode: 'forbid',
89
+ pattern: 'console\\.log\\(',
90
+ },
91
+ },
92
+ },
93
+ ];
94
+
95
+ const engine = new RuleEngine();
96
+ const result = await engine.evaluate(rules, process.cwd());
97
+
98
+ console.log(new TextFormatter().format(result));
99
+ process.exitCode = result.findings.some((finding) => finding.severity === 'error') ? 1 : 0;
100
+ ```
101
+
102
+ ## Rule Files
103
+
104
+ Rule files can be YAML, JSON, or a single rule object. File loads honor a top-level `$schema` ref by default, then validate the internal Zod schema. The `$schema` value is resolved from the bundled package schema (shipped under `node_modules/@gobing-ai/ts-rule-engine/schemas/`) — no network access. Quote the value, since YAML treats a leading `@` as reserved. Relative paths and (opt-in) remote URLs are also supported; see `@gobing-ai/ts-runtime` → *Structured config* for the full resolution rules. A multi-rule YAML file looks like this:
105
+
106
+ ```yaml
107
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
108
+ include:
109
+ - "packages/*/src/**/*.ts"
110
+ exclude:
111
+ - "**/*.test.ts"
112
+ severity: error
113
+ rules:
114
+ - id: no-console-log
115
+ description: Do not commit console.log calls
116
+ evaluator:
117
+ type: regex
118
+ config:
119
+ mode: forbid
120
+ pattern: "console\\.log\\("
121
+
122
+ - id: source-files-have-tests
123
+ description: Source files should have matching tests
124
+ evaluator:
125
+ type: test-location
126
+ config:
127
+ expected: "packages/*/tests/**/*.test.ts"
128
+ requireCorrespondingTest: true
129
+ resolver: typescript
130
+ include:
131
+ - "packages/*/src/**/*.ts"
132
+ ```
133
+
134
+ Load a rule file directly:
135
+
136
+ ```ts
137
+ import { loadRuleFile } from '@gobing-ai/ts-rule-engine';
138
+
139
+ const rules = await loadRuleFile('.rules/typescript.yaml');
140
+ ```
141
+
142
+ ## Presets
143
+
144
+ Presets compose category folders, other presets, and rule-file subpaths across one or more roots. Preset loads also honor top-level `$schema` refs by default, resolved from the bundled package schema (no network access).
145
+
146
+ Example layout:
147
+
148
+ ```text
149
+ .spur/rules/
150
+ recommended.yaml
151
+ quality/
152
+ coverage.yaml
153
+ architecture/
154
+ imports.yaml
155
+ ```
156
+
157
+ Example preset:
158
+
159
+ ```yaml
160
+ $schema: "@gobing-ai/ts-rule-engine/schemas/preset.schema.json"
161
+ name: recommended
162
+ extends:
163
+ - quality
164
+ - architecture/imports
165
+ disable:
166
+ - legacy-rule
167
+ overrides:
168
+ no-console-log:
169
+ fix:
170
+ mode: suggest
171
+ ```
172
+
173
+ Load just the rules:
174
+
175
+ ```ts
176
+ import { loadPresetRules } from '@gobing-ai/ts-rule-engine';
177
+
178
+ const rules = await loadPresetRules('recommended', {
179
+ roots: ['.spur/rules'],
180
+ });
181
+ ```
182
+
183
+ Load rules plus extension refs:
184
+
185
+ ```ts
186
+ import { loadPreset, loadExtensionsIntoHost, RuleEngine } from '@gobing-ai/ts-rule-engine';
187
+
188
+ const loaded = await loadPreset('recommended', {
189
+ roots: ['.spur/rules'],
190
+ });
191
+
192
+ const engine = new RuleEngine();
193
+ await loadExtensionsIntoHost(engine.host, loaded.extensions, {
194
+ allowExtensions: true,
195
+ });
196
+
197
+ const result = await engine.evaluate(loaded.rules, process.cwd());
198
+ ```
199
+
200
+ Roots are ordered highest priority first. If two roots contain the same relative rule file, the first root wins and lower-priority roots fill gaps.
201
+
202
+ ## Evaluating With Fixes
203
+
204
+ Some evaluators have built-in fixer providers. Fixes are never written during evaluation; they are returned as candidates.
205
+
206
+ ```ts
207
+ import { RuleEngine } from '@gobing-ai/ts-rule-engine';
208
+
209
+ const engine = new RuleEngine();
210
+ const result = await engine.evaluateWithFixes(rules, process.cwd(), 'auto');
211
+
212
+ const preview = await engine.applyFixes(process.cwd(), result.fixes, true);
213
+ console.log(preview.diff);
214
+
215
+ // Write changes after you have decided to apply them.
216
+ await engine.applyFixes(process.cwd(), result.fixes);
217
+ ```
218
+
219
+ Fix authority levels:
220
+
221
+ | Mode | Meaning |
222
+ | ---- | ------- |
223
+ | `none` | Do not emit provider fixes. This is the default when `rule.fix` is absent. |
224
+ | `suggest` | Emit fixes only when caller allows at least `suggest`. |
225
+ | `auto` | Emit fixes when caller allows `auto`. |
226
+
227
+ The effective fix mode is the lower authority between `rule.fix.mode` and the caller's `maxFixMode` argument.
228
+
229
+ Example rule with regex replacement:
230
+
231
+ ```yaml
232
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
233
+ rules:
234
+ - id: rename-foo
235
+ description: Replace foo with bar
236
+ evaluator:
237
+ type: regex
238
+ config:
239
+ mode: forbid
240
+ pattern: "\\bfoo\\b"
241
+ flags: g
242
+ fix:
243
+ mode: auto
244
+ replacement: bar
245
+ include:
246
+ - "src/**/*.ts"
247
+ ```
248
+
249
+ Built-in fixer providers:
250
+
251
+ | Evaluator type | Fix behavior |
252
+ | -------------- | ------------ |
253
+ | `regex`, `rg` | Replaces line matches using `fix.replacement`. |
254
+ | `path`, `file-exist` | Deletes files for `must: absent` rules in `auto` mode. |
255
+ | `test-location` | Creates a missing test file using the selected resolver's skeleton when available. |
256
+
257
+ ## Built-in Evaluators
258
+
259
+ | Type | Purpose | Notes |
260
+ | ---- | ------- | ----- |
261
+ | `regex`, `rg` | Match or require text patterns in files. | Pure JS file scanning. Supports inline `(?i)` flags and `multiline`. |
262
+ | `path`, `file-exist` | Check required or forbidden paths. | Supports explicit `paths` or glob-style `must: present/absent`. |
263
+ | `exit-code` | Run a command and evaluate its exit code. | Uses `ProcessExecutor`; inject one through `new RuleEngine({ processExecutor })` for tests. |
264
+ | `forbidden-import` | Block forbidden imports/usages. | Useful for package boundary rules. |
265
+ | `import-boundary` | Enforce scoped architectural import boundaries. | Supports per-boundary scope, excludes, and forbidden patterns. |
266
+ | `secrets-scanner` | Detect hardcoded secrets. | Built-in categories plus custom patterns. |
267
+ | `agent-detection` | Detect coding-agent related files. | Project hygiene use case. |
268
+ | `coverage-gate` | Enforce per-file lcov line coverage thresholds. | Reads `lcov.info`. Supports exemptions. |
269
+ | `tsdoc-export` | Require JSDoc/TSDoc before exported declarations. | TypeScript source scanning. |
270
+ | `test-location` | Enforce test placement and matching source/test pairs. | Uses named test-path resolvers. |
271
+ | `schema-artifact` | Validate JSON schema artifact structure. | Checks existence, JSON validity, title, properties, defs, required array. |
272
+ | `sg` | Run an ast-grep pattern. | Requires the `sg` CLI in the execution environment. |
273
+
274
+ ### Common Evaluator Examples
275
+
276
+ Regex forbid:
277
+
278
+ ```yaml
279
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
280
+ rules:
281
+ - id: no-debugger
282
+ description: Do not commit debugger statements
283
+ evaluator:
284
+ type: regex
285
+ config:
286
+ mode: forbid
287
+ pattern: "\\bdebugger\\b"
288
+ include: ["src/**/*.ts"]
289
+ ```
290
+
291
+ Path presence:
292
+
293
+ ```yaml
294
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
295
+ rules:
296
+ - id: package-readme-required
297
+ description: Each package should document its public API
298
+ evaluator:
299
+ type: path
300
+ config:
301
+ paths: ["README.md"]
302
+ ```
303
+
304
+ Glob absence:
305
+
306
+ ```yaml
307
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
308
+ rules:
309
+ - id: no-dist-in-source
310
+ description: Built artifacts should not be committed
311
+ evaluator:
312
+ type: path
313
+ config:
314
+ must: absent
315
+ include: ["dist/**"]
316
+ ```
317
+
318
+ Coverage gate:
319
+
320
+ ```yaml
321
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
322
+ rules:
323
+ - id: coverage-gate
324
+ description: Source files must meet coverage threshold
325
+ evaluator:
326
+ type: coverage-gate
327
+ config:
328
+ lcovPath: .coverage/lcov.info
329
+ threshold: 90
330
+ exemptions:
331
+ - path: packages/legacy/src/adapter.ts
332
+ threshold: 70
333
+ reason: legacy branch coverage tracked separately
334
+ ```
335
+
336
+ Import boundary:
337
+
338
+ ```yaml
339
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
340
+ rules:
341
+ - id: db-boundary
342
+ description: Only ts-db may import drizzle
343
+ evaluator:
344
+ type: import-boundary
345
+ config:
346
+ boundaries:
347
+ - scope: "packages/*/src/**/*.ts"
348
+ exclude:
349
+ - "packages/db/src/**"
350
+ forbidden:
351
+ - drizzle-orm
352
+ ```
353
+
354
+ Schema artifact:
355
+
356
+ ```yaml
357
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
358
+ rules:
359
+ - id: rule-schema-artifact
360
+ description: Rule JSON schema artifact is complete
361
+ evaluator:
362
+ type: schema-artifact
363
+ config:
364
+ file: schema/rules.schema.json
365
+ requiredTitle: ConstraintRule
366
+ requiredProperties: ["rules"]
367
+ requiredDefs: ["evaluator"]
368
+ requireRequiredArray: true
369
+ ```
370
+
371
+ ast-grep:
372
+
373
+ ```yaml
374
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
375
+ rules:
376
+ - id: no-throw-string
377
+ description: Throw Error objects, not strings
378
+ evaluator:
379
+ type: sg
380
+ config:
381
+ pattern: throw "$MSG"
382
+ language: typescript
383
+ include: ["src/**/*.ts"]
384
+ ```
385
+
386
+ ## Test-Path Resolvers
387
+
388
+ The `test-location` evaluator can require source files to have corresponding test files. The resolver is selected by `evaluator.config.resolver`.
389
+
390
+ | Resolver | Source path | Expected test path |
391
+ | -------- | ----------- | ------------------ |
392
+ | `typescript` | `src/foo/bar.ts` | `tests/foo/bar.test.ts` |
393
+ | `typescript` | `packages/core/src/foo.ts` | `packages/core/tests/foo.test.ts` |
394
+ | `python` | `src/foo/bar.py` | `tests/foo/test_bar.py` |
395
+ | `go` | `foo/bar.go` | `foo/bar_test.go` |
396
+ | `rust` | `crate/src/foo.rs` | `crate/tests/foo.rs` |
397
+
398
+ Example:
399
+
400
+ ```yaml
401
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
402
+ rules:
403
+ - id: python-sources-have-tests
404
+ description: Python sources should have pytest files
405
+ evaluator:
406
+ type: test-location
407
+ config:
408
+ expected: "tests/**/*.py"
409
+ resolver: python
410
+ requireCorrespondingTest: true
411
+ include: ["src/**/*.py"]
412
+ ```
413
+
414
+ ## Custom Evaluators
415
+
416
+ Register a custom evaluator directly:
417
+
418
+ ```ts
419
+ import {
420
+ RuleEngine,
421
+ createFinding,
422
+ type RuleEvaluator,
423
+ } from '@gobing-ai/ts-rule-engine';
424
+
425
+ const evaluator: RuleEvaluator = {
426
+ async evaluate(rule, context) {
427
+ if (!context.workdir.includes('service')) {
428
+ return {
429
+ findings: [
430
+ createFinding(rule, 'workspace path must include "service"', null, {
431
+ code: 'custom:not-service',
432
+ }),
433
+ ],
434
+ fixes: [],
435
+ };
436
+ }
437
+ return { findings: [], fixes: [] };
438
+ },
439
+ };
440
+
441
+ const engine = new RuleEngine();
442
+ engine.registerEvaluator('workspace-name', evaluator);
443
+ ```
444
+
445
+ Then use it in a rule:
446
+
447
+ ```yaml
448
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
449
+ rules:
450
+ - id: workspace-name
451
+ description: Check workspace naming convention
452
+ evaluator:
453
+ type: workspace-name
454
+ ```
455
+
456
+ ## Preset Extensions
457
+
458
+ Preset extensions are trusted local modules. They are disabled unless the caller explicitly passes `allowExtensions: true` to `loadExtensionsIntoHost()`.
459
+
460
+ Preset:
461
+
462
+ ```yaml
463
+ $schema: "@gobing-ai/ts-rule-engine/schemas/preset.schema.json"
464
+ name: local
465
+ extends:
466
+ - quality
467
+ extensions:
468
+ resolvers:
469
+ - ./extensions/custom-resolver.ts
470
+ evaluators:
471
+ - ./extensions/custom-evaluator.ts
472
+ formatters:
473
+ - ./extensions/compact-formatter.ts
474
+ ```
475
+
476
+ Resolver extension:
477
+
478
+ ```ts
479
+ export default {
480
+ name: 'custom',
481
+ resolveTestPath(srcRelPath: string): string {
482
+ return srcRelPath.replace(/^src\//, 'tests/').replace(/\.ts$/, '.spec.ts');
483
+ },
484
+ };
485
+ ```
486
+
487
+ Evaluator extension:
488
+
489
+ ```ts
490
+ import type { RuleEvaluator } from '@gobing-ai/ts-rule-engine';
491
+
492
+ const evaluator: RuleEvaluator & { name: string } = {
493
+ name: 'custom-check',
494
+ async evaluate() {
495
+ return { findings: [], fixes: [] };
496
+ },
497
+ };
498
+
499
+ export default evaluator;
500
+ ```
501
+
502
+ Load extensions:
503
+
504
+ ```ts
505
+ const loaded = await loadPreset('local', { roots: ['.spur/rules'] });
506
+ const engine = new RuleEngine();
507
+
508
+ await loadExtensionsIntoHost(engine.host, loaded.extensions, {
509
+ allowExtensions: true,
510
+ logger: { warn: console.warn },
511
+ });
512
+ ```
513
+
514
+ Supported extension kinds:
515
+
516
+ | Kind | Registry | Required shape |
517
+ | ---- | -------- | -------------- |
518
+ | `resolvers` | `host.resolvers` | object with `name` and `resolveTestPath()` |
519
+ | `evaluators` | `host.evaluators` | object with `name` and `evaluate()` |
520
+ | `formatters` | `host.formatters` | object with `name` and `format()` |
521
+
522
+ `fixers` can be declared in preset metadata but are not loaded into `RuleEngineHost` by `loadExtensionsIntoHost()` because fixer providers live on the engine's fixer map.
523
+
524
+ ## Formatting Results
525
+
526
+ ```ts
527
+ import { JsonFormatter, TextFormatter } from '@gobing-ai/ts-rule-engine';
528
+
529
+ const text = new TextFormatter().format(result);
530
+ const json = new JsonFormatter().format(result);
531
+ ```
532
+
533
+ Text output is intended for humans:
534
+
535
+ ```text
536
+ ERROR no-console-log src/index.ts:12 forbidden pattern found: console\.log\(
537
+ ```
538
+
539
+ JSON output is the full `RuleEngineResult` object.
540
+
541
+ ## Error Handling
542
+
543
+ Evaluator runtime errors are captured as findings with:
544
+
545
+ - `kind: "error"`
546
+ - `code: "evaluator:<type>"`
547
+ - `filePath: null`
548
+
549
+ That lets downstream tools distinguish policy violations from misconfigured or failing evaluators.
550
+
551
+ ```ts
552
+ const errors = result.findings.filter((finding) => finding.kind === 'error');
553
+ const violations = result.findings.filter((finding) => finding.kind !== 'error');
554
+ ```
555
+
556
+ ## Package Boundary
557
+
558
+ This package owns rule definitions, preset loading, evaluators, formatters, test-path resolvers, and fix application. It does not own:
559
+
560
+ - CLI argument parsing
561
+ - process exit policy
562
+ - repository-specific rule catalogs
563
+ - publishing or CI integration
564
+
565
+ Those concerns should live in downstream tools that consume this library.
@@ -0,0 +1,48 @@
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
+ /** Optional module loader seam for tests or embedders with custom import policy. */
26
+ moduleLoader?: (absPath: string) => Promise<Record<string, unknown>>;
27
+ }
28
+ /**
29
+ * Collect extension refs declared by a preset's `extensions` block.
30
+ *
31
+ * Paths are resolved relative to the preset file's directory. Use the returned
32
+ * refs with {@link loadExtensionsIntoHost}.
33
+ */
34
+ export declare function collectPresetExtensions(presetName: string, presetDir: string, extensions: Partial<Record<ExtensionKind, string[] | undefined>> | undefined): ExtensionRef[];
35
+ /**
36
+ * Import each extension module and register its export on the matching host
37
+ * registry.
38
+ *
39
+ * A module must default-export (or named-export `extension`) an object with a
40
+ * `name: string` and the capability implementation. Loading is gated by
41
+ * {@link LoadExtensionsOptions.allowExtensions}; when refs are present but
42
+ * loading is not allowed, this throws so the requirement is never silently dropped.
43
+ *
44
+ * @throws When extensions are present but `allowExtensions` is not true, or when
45
+ * a module cannot be imported or lacks a valid `name`.
46
+ */
47
+ export declare function loadExtensionsIntoHost(host: RuleEngineHost, refs: readonly ExtensionRef[], options?: LoadExtensionsOptions): Promise<void>;
48
+ //# 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;IAC7C,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACxE;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,CAoCf"}
@@ -0,0 +1,67 @@
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
+ const loadModule = options.moduleLoader ?? defaultModuleLoader;
45
+ for (const ref of refs) {
46
+ const moduleExports = await loadModule(ref.absPath);
47
+ const candidate = moduleExports.default ?? moduleExports.extension;
48
+ if (candidate === null ||
49
+ typeof candidate !== 'object' ||
50
+ typeof candidate.name !== 'string') {
51
+ throw new Error(`preset "${ref.presetName}" extension "${ref.absPath}" must export an object with a string "name"`);
52
+ }
53
+ const name = candidate.name;
54
+ const registryKey = HOST_REGISTRY_BY_KIND[ref.kind];
55
+ if (registryKey === undefined) {
56
+ throw new Error(`preset "${ref.presetName}" ${ref.kind} extensions are not supported`);
57
+ }
58
+ const registry = host[registryKey];
59
+ if (options.logger && registry.has?.(name)) {
60
+ options.logger.warn(`preset "${ref.presetName}" ${ref.kind} extension overrides existing "${name}"`);
61
+ }
62
+ registry.register(name, candidate, 'extension');
63
+ }
64
+ }
65
+ async function defaultModuleLoader(absPath) {
66
+ return (await import(absPath));
67
+ }