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

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 (54) hide show
  1. package/README.md +565 -1
  2. package/dist/config/extensions.d.ts +7 -4
  3. package/dist/config/extensions.d.ts.map +1 -1
  4. package/dist/config/extensions.js +11 -6
  5. package/dist/config/loader.d.ts +29 -2
  6. package/dist/config/loader.d.ts.map +1 -1
  7. package/dist/config/loader.js +104 -34
  8. package/dist/engine.d.ts +8 -1
  9. package/dist/engine.d.ts.map +1 -1
  10. package/dist/engine.js +9 -19
  11. package/dist/evaluators/coverage-gate-evaluator.js +12 -3
  12. package/dist/evaluators/file-utils.d.ts +55 -0
  13. package/dist/evaluators/file-utils.d.ts.map +1 -1
  14. package/dist/evaluators/file-utils.js +49 -0
  15. package/dist/evaluators/forbidden-import-evaluator.d.ts +5 -0
  16. package/dist/evaluators/forbidden-import-evaluator.d.ts.map +1 -1
  17. package/dist/evaluators/forbidden-import-evaluator.js +14 -17
  18. package/dist/evaluators/import-boundary-evaluator.d.ts +5 -0
  19. package/dist/evaluators/import-boundary-evaluator.d.ts.map +1 -1
  20. package/dist/evaluators/import-boundary-evaluator.js +45 -15
  21. package/dist/evaluators/regex-evaluator.d.ts +9 -1
  22. package/dist/evaluators/regex-evaluator.d.ts.map +1 -1
  23. package/dist/evaluators/regex-evaluator.js +43 -14
  24. package/dist/evaluators/secrets-scanner-evaluator.d.ts +5 -0
  25. package/dist/evaluators/secrets-scanner-evaluator.d.ts.map +1 -1
  26. package/dist/evaluators/secrets-scanner-evaluator.js +13 -13
  27. package/dist/evaluators/tsdoc-export-evaluator.d.ts.map +1 -1
  28. package/dist/evaluators/tsdoc-export-evaluator.js +9 -11
  29. package/dist/formatters/json.d.ts.map +1 -1
  30. package/dist/formatters/json.js +2 -0
  31. package/dist/formatters/text.d.ts.map +1 -1
  32. package/dist/formatters/text.js +2 -0
  33. package/dist/resolvers/test-path-resolver.d.ts.map +1 -1
  34. package/dist/resolvers/test-path-resolver.js +5 -0
  35. package/dist/types.d.ts +28 -3
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js +37 -9
  38. package/package.json +6 -3
  39. package/schemas/preset.schema.json +54 -0
  40. package/schemas/rule-file.schema.json +49 -0
  41. package/src/config/extensions.ts +16 -8
  42. package/src/config/loader.ts +151 -34
  43. package/src/engine.ts +9 -19
  44. package/src/evaluators/coverage-gate-evaluator.ts +15 -5
  45. package/src/evaluators/file-utils.ts +92 -0
  46. package/src/evaluators/forbidden-import-evaluator.ts +14 -19
  47. package/src/evaluators/import-boundary-evaluator.ts +56 -40
  48. package/src/evaluators/regex-evaluator.ts +43 -13
  49. package/src/evaluators/secrets-scanner-evaluator.ts +13 -14
  50. package/src/evaluators/tsdoc-export-evaluator.ts +10 -9
  51. package/src/formatters/json.ts +2 -0
  52. package/src/formatters/text.ts +2 -0
  53. package/src/resolvers/test-path-resolver.ts +5 -0
  54. package/src/types.ts +45 -9
package/README.md CHANGED
@@ -1,3 +1,567 @@
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
+ // Returns { rules, extensions } — same shape as loadPreset(). A rule file may
140
+ // declare an `extensions` block; the refs are gated by allowExtensions at load time.
141
+ const { rules, extensions } = await loadRuleFile('.rules/typescript.yaml');
142
+ ```
143
+
144
+ ## Presets
145
+
146
+ 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).
147
+
148
+ Example layout:
149
+
150
+ ```text
151
+ .spur/rules/
152
+ recommended.yaml
153
+ quality/
154
+ coverage.yaml
155
+ architecture/
156
+ imports.yaml
157
+ ```
158
+
159
+ Example preset:
160
+
161
+ ```yaml
162
+ $schema: "@gobing-ai/ts-rule-engine/schemas/preset.schema.json"
163
+ name: recommended
164
+ extends:
165
+ - quality
166
+ - architecture/imports
167
+ disable:
168
+ - legacy-rule
169
+ overrides:
170
+ no-console-log:
171
+ fix:
172
+ mode: suggest
173
+ ```
174
+
175
+ Load just the rules:
176
+
177
+ ```ts
178
+ import { loadPresetRules } from '@gobing-ai/ts-rule-engine';
179
+
180
+ const rules = await loadPresetRules('recommended', {
181
+ roots: ['.spur/rules'],
182
+ });
183
+ ```
184
+
185
+ Load rules plus extension refs:
186
+
187
+ ```ts
188
+ import { loadPreset, loadExtensionsIntoHost, RuleEngine } from '@gobing-ai/ts-rule-engine';
189
+
190
+ const loaded = await loadPreset('recommended', {
191
+ roots: ['.spur/rules'],
192
+ });
193
+
194
+ const engine = new RuleEngine();
195
+ await loadExtensionsIntoHost(engine.host, loaded.extensions, {
196
+ allowExtensions: true,
197
+ });
198
+
199
+ const result = await engine.evaluate(loaded.rules, process.cwd());
200
+ ```
201
+
202
+ 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.
203
+
204
+ ## Evaluating With Fixes
205
+
206
+ Some evaluators have built-in fixer providers. Fixes are never written during evaluation; they are returned as candidates.
207
+
208
+ ```ts
209
+ import { RuleEngine } from '@gobing-ai/ts-rule-engine';
210
+
211
+ const engine = new RuleEngine();
212
+ const result = await engine.evaluateWithFixes(rules, process.cwd(), 'auto');
213
+
214
+ const preview = await engine.applyFixes(process.cwd(), result.fixes, true);
215
+ console.log(preview.diff);
216
+
217
+ // Write changes after you have decided to apply them.
218
+ await engine.applyFixes(process.cwd(), result.fixes);
219
+ ```
220
+
221
+ Fix authority levels:
222
+
223
+ | Mode | Meaning |
224
+ | ---- | ------- |
225
+ | `none` | Do not emit provider fixes. This is the default when `rule.fix` is absent. |
226
+ | `suggest` | Emit fixes only when caller allows at least `suggest`. |
227
+ | `auto` | Emit fixes when caller allows `auto`. |
228
+
229
+ The effective fix mode is the lower authority between `rule.fix.mode` and the caller's `maxFixMode` argument.
230
+
231
+ Example rule with regex replacement:
232
+
233
+ ```yaml
234
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
235
+ rules:
236
+ - id: rename-foo
237
+ description: Replace foo with bar
238
+ evaluator:
239
+ type: regex
240
+ config:
241
+ mode: forbid
242
+ pattern: "\\bfoo\\b"
243
+ flags: g
244
+ fix:
245
+ mode: auto
246
+ replacement: bar
247
+ include:
248
+ - "src/**/*.ts"
249
+ ```
250
+
251
+ Built-in fixer providers:
252
+
253
+ | Evaluator type | Fix behavior |
254
+ | -------------- | ------------ |
255
+ | `regex`, `rg` | Replaces line matches using `fix.replacement`. |
256
+ | `path`, `file-exist` | Deletes files for `must: absent` rules in `auto` mode. |
257
+ | `test-location` | Creates a missing test file using the selected resolver's skeleton when available. |
258
+
259
+ ## Built-in Evaluators
260
+
261
+ | Type | Purpose | Notes |
262
+ | ---- | ------- | ----- |
263
+ | `regex`, `rg` | Match or require text patterns in files. | Pure JS file scanning. Supports inline `(?i)` flags and `multiline`. |
264
+ | `path`, `file-exist` | Check required or forbidden paths. | Supports explicit `paths` or glob-style `must: present/absent`. |
265
+ | `exit-code` | Run a command and evaluate its exit code. | Uses `ProcessExecutor`; inject one through `new RuleEngine({ processExecutor })` for tests. |
266
+ | `forbidden-import` | Block forbidden imports/usages. | Useful for package boundary rules. |
267
+ | `import-boundary` | Enforce scoped architectural import boundaries. | Supports per-boundary scope, excludes, and forbidden patterns. |
268
+ | `secrets-scanner` | Detect hardcoded secrets. | Built-in categories plus custom patterns. |
269
+ | `agent-detection` | Detect coding-agent related files. | Project hygiene use case. |
270
+ | `coverage-gate` | Enforce per-file lcov line coverage thresholds. | Reads `lcov.info`. Supports exemptions. |
271
+ | `tsdoc-export` | Require JSDoc/TSDoc before exported declarations. | TypeScript source scanning. |
272
+ | `test-location` | Enforce test placement and matching source/test pairs. | Uses named test-path resolvers. |
273
+ | `schema-artifact` | Validate JSON schema artifact structure. | Checks existence, JSON validity, title, properties, defs, required array. |
274
+ | `sg` | Run an ast-grep pattern. | Requires the `sg` CLI in the execution environment. |
275
+
276
+ ### Common Evaluator Examples
277
+
278
+ Regex forbid:
279
+
280
+ ```yaml
281
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
282
+ rules:
283
+ - id: no-debugger
284
+ description: Do not commit debugger statements
285
+ evaluator:
286
+ type: regex
287
+ config:
288
+ mode: forbid
289
+ pattern: "\\bdebugger\\b"
290
+ include: ["src/**/*.ts"]
291
+ ```
292
+
293
+ Path presence:
294
+
295
+ ```yaml
296
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
297
+ rules:
298
+ - id: package-readme-required
299
+ description: Each package should document its public API
300
+ evaluator:
301
+ type: path
302
+ config:
303
+ paths: ["README.md"]
304
+ ```
305
+
306
+ Glob absence:
307
+
308
+ ```yaml
309
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
310
+ rules:
311
+ - id: no-dist-in-source
312
+ description: Built artifacts should not be committed
313
+ evaluator:
314
+ type: path
315
+ config:
316
+ must: absent
317
+ include: ["dist/**"]
318
+ ```
319
+
320
+ Coverage gate:
321
+
322
+ ```yaml
323
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
324
+ rules:
325
+ - id: coverage-gate
326
+ description: Source files must meet coverage threshold
327
+ evaluator:
328
+ type: coverage-gate
329
+ config:
330
+ lcovPath: .coverage/lcov.info
331
+ threshold: 90
332
+ exemptions:
333
+ - path: packages/legacy/src/adapter.ts
334
+ threshold: 70
335
+ reason: legacy branch coverage tracked separately
336
+ ```
337
+
338
+ Import boundary:
339
+
340
+ ```yaml
341
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
342
+ rules:
343
+ - id: db-boundary
344
+ description: Only ts-db may import drizzle
345
+ evaluator:
346
+ type: import-boundary
347
+ config:
348
+ boundaries:
349
+ - scope: "packages/*/src/**/*.ts"
350
+ exclude:
351
+ - "packages/db/src/**"
352
+ forbidden:
353
+ - drizzle-orm
354
+ ```
355
+
356
+ Schema artifact:
357
+
358
+ ```yaml
359
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
360
+ rules:
361
+ - id: rule-schema-artifact
362
+ description: Rule JSON schema artifact is complete
363
+ evaluator:
364
+ type: schema-artifact
365
+ config:
366
+ file: schema/rules.schema.json
367
+ requiredTitle: ConstraintRule
368
+ requiredProperties: ["rules"]
369
+ requiredDefs: ["evaluator"]
370
+ requireRequiredArray: true
371
+ ```
372
+
373
+ ast-grep:
374
+
375
+ ```yaml
376
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
377
+ rules:
378
+ - id: no-throw-string
379
+ description: Throw Error objects, not strings
380
+ evaluator:
381
+ type: sg
382
+ config:
383
+ pattern: throw "$MSG"
384
+ language: typescript
385
+ include: ["src/**/*.ts"]
386
+ ```
387
+
388
+ ## Test-Path Resolvers
389
+
390
+ The `test-location` evaluator can require source files to have corresponding test files. The resolver is selected by `evaluator.config.resolver`.
391
+
392
+ | Resolver | Source path | Expected test path |
393
+ | -------- | ----------- | ------------------ |
394
+ | `typescript` | `src/foo/bar.ts` | `tests/foo/bar.test.ts` |
395
+ | `typescript` | `packages/core/src/foo.ts` | `packages/core/tests/foo.test.ts` |
396
+ | `python` | `src/foo/bar.py` | `tests/foo/test_bar.py` |
397
+ | `go` | `foo/bar.go` | `foo/bar_test.go` |
398
+ | `rust` | `crate/src/foo.rs` | `crate/tests/foo.rs` |
399
+
400
+ Example:
401
+
402
+ ```yaml
403
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
404
+ rules:
405
+ - id: python-sources-have-tests
406
+ description: Python sources should have pytest files
407
+ evaluator:
408
+ type: test-location
409
+ config:
410
+ expected: "tests/**/*.py"
411
+ resolver: python
412
+ requireCorrespondingTest: true
413
+ include: ["src/**/*.py"]
414
+ ```
415
+
416
+ ## Custom Evaluators
417
+
418
+ Register a custom evaluator directly:
419
+
420
+ ```ts
421
+ import {
422
+ RuleEngine,
423
+ createFinding,
424
+ type RuleEvaluator,
425
+ } from '@gobing-ai/ts-rule-engine';
426
+
427
+ const evaluator: RuleEvaluator = {
428
+ async evaluate(rule, context) {
429
+ if (!context.workdir.includes('service')) {
430
+ return {
431
+ findings: [
432
+ createFinding(rule, 'workspace path must include "service"', null, {
433
+ code: 'custom:not-service',
434
+ }),
435
+ ],
436
+ fixes: [],
437
+ };
438
+ }
439
+ return { findings: [], fixes: [] };
440
+ },
441
+ };
442
+
443
+ const engine = new RuleEngine();
444
+ engine.registerEvaluator('workspace-name', evaluator);
445
+ ```
446
+
447
+ Then use it in a rule:
448
+
449
+ ```yaml
450
+ $schema: "@gobing-ai/ts-rule-engine/schemas/rule-file.schema.json"
451
+ rules:
452
+ - id: workspace-name
453
+ description: Check workspace naming convention
454
+ evaluator:
455
+ type: workspace-name
456
+ ```
457
+
458
+ ## Preset Extensions
459
+
460
+ Preset extensions are trusted local modules. They are disabled unless the caller explicitly passes `allowExtensions: true` to `loadExtensionsIntoHost()`.
461
+
462
+ Preset:
463
+
464
+ ```yaml
465
+ $schema: "@gobing-ai/ts-rule-engine/schemas/preset.schema.json"
466
+ name: local
467
+ extends:
468
+ - quality
469
+ extensions:
470
+ resolvers:
471
+ - ./extensions/custom-resolver.ts
472
+ evaluators:
473
+ - ./extensions/custom-evaluator.ts
474
+ formatters:
475
+ - ./extensions/compact-formatter.ts
476
+ ```
477
+
478
+ Resolver extension:
479
+
480
+ ```ts
481
+ export default {
482
+ name: 'custom',
483
+ resolveTestPath(srcRelPath: string): string {
484
+ return srcRelPath.replace(/^src\//, 'tests/').replace(/\.ts$/, '.spec.ts');
485
+ },
486
+ };
487
+ ```
488
+
489
+ Evaluator extension:
490
+
491
+ ```ts
492
+ import type { RuleEvaluator } from '@gobing-ai/ts-rule-engine';
493
+
494
+ const evaluator: RuleEvaluator & { name: string } = {
495
+ name: 'custom-check',
496
+ async evaluate() {
497
+ return { findings: [], fixes: [] };
498
+ },
499
+ };
500
+
501
+ export default evaluator;
502
+ ```
503
+
504
+ Load extensions:
505
+
506
+ ```ts
507
+ const loaded = await loadPreset('local', { roots: ['.spur/rules'] });
508
+ const engine = new RuleEngine();
509
+
510
+ await loadExtensionsIntoHost(engine.host, loaded.extensions, {
511
+ allowExtensions: true,
512
+ logger: { warn: console.warn },
513
+ });
514
+ ```
515
+
516
+ Supported extension kinds:
517
+
518
+ | Kind | Registry | Required shape |
519
+ | ---- | -------- | -------------- |
520
+ | `resolvers` | `host.resolvers` | object with `name` and `resolveTestPath()` |
521
+ | `evaluators` | `host.evaluators` | object with `name` and `evaluate()` |
522
+ | `formatters` | `host.formatters` | object with `name` and `format()` |
523
+
524
+ `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.
525
+
526
+ ## Formatting Results
527
+
528
+ ```ts
529
+ import { JsonFormatter, TextFormatter } from '@gobing-ai/ts-rule-engine';
530
+
531
+ const text = new TextFormatter().format(result);
532
+ const json = new JsonFormatter().format(result);
533
+ ```
534
+
535
+ Text output is intended for humans:
536
+
537
+ ```text
538
+ ERROR no-console-log src/index.ts:12 forbidden pattern found: console\.log\(
539
+ ```
540
+
541
+ JSON output is the full `RuleEngineResult` object.
542
+
543
+ ## Error Handling
544
+
545
+ Evaluator runtime errors are captured as findings with:
546
+
547
+ - `kind: "error"`
548
+ - `code: "evaluator:<type>"`
549
+ - `filePath: null`
550
+
551
+ That lets downstream tools distinguish policy violations from misconfigured or failing evaluators.
552
+
553
+ ```ts
554
+ const errors = result.findings.filter((finding) => finding.kind === 'error');
555
+ const violations = result.findings.filter((finding) => finding.kind !== 'error');
556
+ ```
557
+
558
+ ## Package Boundary
559
+
560
+ This package owns rule definitions, preset loading, evaluators, formatters, test-path resolvers, and fix application. It does not own:
561
+
562
+ - CLI argument parsing
563
+ - process exit policy
564
+ - repository-specific rule catalogs
565
+ - publishing or CI integration
566
+
567
+ Those concerns should live in downstream tools that consume this library.
@@ -22,14 +22,17 @@ export interface LoadExtensionsOptions {
22
22
  logger?: {
23
23
  warn: (message: string) => void;
24
24
  };
25
+ /** Optional module loader seam for tests or embedders with custom import policy. */
26
+ moduleLoader?: (absPath: string) => Promise<Record<string, unknown>>;
25
27
  }
26
28
  /**
27
- * Collect extension refs declared by a preset's `extensions` block.
29
+ * Collect extension refs declared by a preset's or rule file's `extensions` block.
28
30
  *
29
- * Paths are resolved relative to the preset file's directory. Use the returned
30
- * refs with {@link loadExtensionsIntoHost}.
31
+ * Paths are resolved relative to the declaring file's directory. Use the returned
32
+ * refs with {@link loadExtensionsIntoHost}. Rule files and presets are treated
33
+ * identically — both flow through the same trust gate at load time.
31
34
  */
32
- export declare function collectPresetExtensions(presetName: string, presetDir: string, extensions: Partial<Record<ExtensionKind, string[] | undefined>> | undefined): ExtensionRef[];
35
+ export declare function collectExtensions(sourceName: string, sourceDir: string, extensions: Partial<Record<ExtensionKind, string[] | undefined>> | undefined): ExtensionRef[];
33
36
  /**
34
37
  * Import each extension module and register its export on the matching host
35
38
  * registry.
@@ -1 +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"}
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;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,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"}
@@ -6,18 +6,19 @@ const HOST_REGISTRY_BY_KIND = {
6
6
  formatters: 'formatters',
7
7
  };
8
8
  /**
9
- * Collect extension refs declared by a preset's `extensions` block.
9
+ * Collect extension refs declared by a preset's or rule file's `extensions` block.
10
10
  *
11
- * Paths are resolved relative to the preset file's directory. Use the returned
12
- * refs with {@link loadExtensionsIntoHost}.
11
+ * Paths are resolved relative to the declaring file's directory. Use the returned
12
+ * refs with {@link loadExtensionsIntoHost}. Rule files and presets are treated
13
+ * identically — both flow through the same trust gate at load time.
13
14
  */
14
- export function collectPresetExtensions(presetName, presetDir, extensions) {
15
+ export function collectExtensions(sourceName, sourceDir, extensions) {
15
16
  if (extensions === undefined)
16
17
  return [];
17
18
  const refs = [];
18
19
  for (const kind of ['resolvers', 'evaluators', 'fixers', 'formatters']) {
19
20
  for (const path of extensions[kind] ?? []) {
20
- refs.push({ kind, presetName, absPath: resolve(presetDir, path) });
21
+ refs.push({ kind, presetName: sourceName, absPath: resolve(sourceDir, path) });
21
22
  }
22
23
  }
23
24
  return refs;
@@ -41,8 +42,9 @@ export async function loadExtensionsIntoHost(host, refs, options = {}) {
41
42
  const first = refs[0];
42
43
  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
  }
45
+ const loadModule = options.moduleLoader ?? defaultModuleLoader;
44
46
  for (const ref of refs) {
45
- const moduleExports = (await import(ref.absPath));
47
+ const moduleExports = await loadModule(ref.absPath);
46
48
  const candidate = moduleExports.default ?? moduleExports.extension;
47
49
  if (candidate === null ||
48
50
  typeof candidate !== 'object' ||
@@ -61,3 +63,6 @@ export async function loadExtensionsIntoHost(host, refs, options = {}) {
61
63
  registry.register(name, candidate, 'extension');
62
64
  }
63
65
  }
66
+ async function defaultModuleLoader(absPath) {
67
+ return (await import(absPath));
68
+ }
@@ -1,4 +1,5 @@
1
1
  import { type ConstraintRule } from '../types';
2
+ import { type ExtensionRef } from './extensions';
2
3
  /** Options for loading rule presets. */
3
4
  export interface RuleLoaderOptions {
4
5
  /**
@@ -9,6 +10,23 @@ export interface RuleLoaderOptions {
9
10
  * loader stays agnostic to any project layout convention.
10
11
  */
11
12
  roots: string[];
13
+ /** When true, honor top-level `$schema` refs in preset and rule files. Defaults to true. */
14
+ validateSchema?: boolean;
15
+ /** Optional fetch implementation for remote HTTP(S) schema refs. */
16
+ fetch?: (input: string) => Promise<Response>;
17
+ }
18
+ export interface RuleFileLoadOptions {
19
+ /** When true, honor top-level `$schema` refs. Defaults to true. */
20
+ validateSchema?: boolean;
21
+ /** Optional fetch implementation for remote HTTP(S) schema refs. */
22
+ fetch?: (input: string) => Promise<Response>;
23
+ }
24
+ /** Loaded preset rules plus extension module refs declared by composed presets. */
25
+ export interface LoadedPreset {
26
+ /** Normalized rules after preset disable/override handling. */
27
+ readonly rules: ConstraintRule[];
28
+ /** Extension modules declared by the preset graph, resolved to absolute paths. */
29
+ readonly extensions: ExtensionRef[];
12
30
  }
13
31
  /**
14
32
  * Load and normalize a preset by name, resolving across one or more rule roots.
@@ -17,7 +35,16 @@ export interface RuleLoaderOptions {
17
35
  * so a caller can layer project-local rules over shared/global rules and inherit
18
36
  * the rest of a preset's categories from the lower-priority roots.
19
37
  */
38
+ export declare function loadPreset(name: string, options: RuleLoaderOptions): Promise<LoadedPreset>;
20
39
  export declare function loadPresetRules(name: string, options: RuleLoaderOptions): Promise<ConstraintRule[]>;
21
- /** Load a direct rule file from disk. */
22
- export declare function loadRuleFile(filePath: string): Promise<ConstraintRule[]>;
40
+ /**
41
+ * Load a direct rule file from disk.
42
+ *
43
+ * Returns the normalized rules plus any extension modules the file declares in an
44
+ * `extensions` block, resolved to absolute paths. Rule-file extensions are treated
45
+ * exactly like preset extensions — pass the refs to {@link loadExtensionsIntoHost},
46
+ * which enforces the same `allowExtensions` trust gate. A single-rule file (one rule
47
+ * object, not a `rules:` array) cannot declare extensions and yields `extensions: []`.
48
+ */
49
+ export declare function loadRuleFile(filePath: string, options?: RuleFileLoadOptions): Promise<LoadedPreset>;
23
50
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,KAAK,cAAc,EAMtB,MAAM,UAAU,CAAC;AAElB,wCAAwC;AACxC,MAAM,WAAW,iBAAiB;IAC9B;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACnB;AAUD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAkBzG;AAED,yCAAyC;AACzC,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAE9E"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,cAAc,EAOtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAEpE,wCAAwC;AACxC,MAAM,WAAW,iBAAiB;IAC9B;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,4FAA4F;IAC5F,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,mBAAmB;IAChC,mEAAmE;IACnE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChD;AAED,mFAAmF;AACnF,MAAM,WAAW,YAAY;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,QAAQ,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC;CACvC;AAUD;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAahG;AAyCD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAEzG;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAS7G"}