@gobing-ai/ts-rule-engine 0.2.7 → 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.
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.
@@ -22,6 +22,8 @@ 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
29
  * Collect extension refs declared by a preset's `extensions` block.
@@ -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;;;;;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"}
@@ -41,8 +41,9 @@ export async function loadExtensionsIntoHost(host, refs, options = {}) {
41
41
  const first = refs[0];
42
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
43
  }
44
+ const loadModule = options.moduleLoader ?? defaultModuleLoader;
44
45
  for (const ref of refs) {
45
- const moduleExports = (await import(ref.absPath));
46
+ const moduleExports = await loadModule(ref.absPath);
46
47
  const candidate = moduleExports.default ?? moduleExports.extension;
47
48
  if (candidate === null ||
48
49
  typeof candidate !== 'object' ||
@@ -61,3 +62,6 @@ export async function loadExtensionsIntoHost(host, refs, options = {}) {
61
62
  registry.register(name, candidate, 'extension');
62
63
  }
63
64
  }
65
+ async function defaultModuleLoader(absPath) {
66
+ return (await import(absPath));
67
+ }
@@ -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,8 @@ 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
40
  /** Load a direct rule file from disk. */
22
- export declare function loadRuleFile(filePath: string): Promise<ConstraintRule[]>;
41
+ export declare function loadRuleFile(filePath: string, options?: RuleFileLoadOptions): Promise<ConstraintRule[]>;
23
42
  //# 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,EAMtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAE1E,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,CAqBhG;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAEzG;AAED,yCAAyC;AACzC,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAGjH"}
@@ -1,7 +1,7 @@
1
- import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
2
- import { NodeFileSystem } from '@gobing-ai/ts-runtime';
3
- import { parse } from 'yaml';
1
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
2
+ import { loadStructuredConfig, NodeFileSystem } from '@gobing-ai/ts-runtime';
4
3
  import { ConstraintRuleFileSchema, ConstraintRuleSchema, PresetDefinitionSchema, } from '../types.js';
4
+ import { collectPresetExtensions } from './extensions.js';
5
5
  /**
6
6
  * Load and normalize a preset by name, resolving across one or more rule roots.
7
7
  *
@@ -9,15 +9,18 @@ import { ConstraintRuleFileSchema, ConstraintRuleSchema, PresetDefinitionSchema,
9
9
  * so a caller can layer project-local rules over shared/global rules and inherit
10
10
  * the rest of a preset's categories from the lower-priority roots.
11
11
  */
12
- export async function loadPresetRules(name, options) {
12
+ export async function loadPreset(name, options) {
13
13
  const merged = await buildMergedRoots(options.roots.map((root) => resolve(root)));
14
14
  const presetPath = findMergedPreset(merged, name);
15
15
  if (presetPath === null)
16
- return [];
17
- const preset = PresetDefinitionSchema.parse(await readStructuredFile(presetPath));
16
+ return { rules: [], extensions: [] };
17
+ const preset = PresetDefinitionSchema.parse(await readStructuredFile(presetPath, options));
18
18
  const rules = [];
19
+ const extensions = collectPresetExtensions(preset.name, dirname(presetPath), preset.extensions);
19
20
  for (const entry of preset.extends) {
20
- rules.push(...(await loadPresetEntry(merged, entry, new Set([name]))));
21
+ const loaded = await loadPresetEntry(merged, entry, new Set([name]), options);
22
+ rules.push(...loaded.rules);
23
+ extensions.push(...loaded.extensions);
21
24
  }
22
25
  const disabled = new Set(preset.disable ?? []);
23
26
  const normalized = rules.filter((rule) => !disabled.has(rule.id));
@@ -27,41 +30,52 @@ export async function loadPresetRules(name, options) {
27
30
  rule.fix = { ...(rule.fix ?? { mode: 'none' }), ...override.fix };
28
31
  }
29
32
  }
30
- return normalized;
33
+ return { rules: normalized, extensions };
34
+ }
35
+ export async function loadPresetRules(name, options) {
36
+ return (await loadPreset(name, options)).rules;
31
37
  }
32
38
  /** Load a direct rule file from disk. */
33
- export async function loadRuleFile(filePath) {
34
- return normalizeRuleFile(await readStructuredFile(resolve(filePath)), dirname(resolve(filePath)));
39
+ export async function loadRuleFile(filePath, options = {}) {
40
+ const resolved = resolve(filePath);
41
+ return normalizeRuleFile(await readStructuredFile(resolved, options), dirname(resolved));
35
42
  }
36
- async function loadPresetEntry(merged, entry, seen) {
43
+ async function loadPresetEntry(merged, entry, seen, options) {
37
44
  // Sub-preset reference — recurse, erroring on a genuine cycle.
38
45
  const presetPath = findMergedPreset(merged, entry);
39
46
  if (presetPath !== null) {
40
47
  if (seen.has(entry)) {
41
48
  throw new Error(`Circular preset dependency detected: ${[...seen, entry].join(' → ')}`);
42
49
  }
43
- seen.add(entry);
44
- const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath));
50
+ const nextSeen = new Set([...seen, entry]);
51
+ const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath, options));
45
52
  if (preset.success) {
46
53
  const rules = [];
47
- for (const child of preset.data.extends)
48
- rules.push(...(await loadPresetEntry(merged, child, seen)));
49
- return rules.filter((rule) => !(preset.data.disable ?? []).includes(rule.id));
54
+ const extensions = collectPresetExtensions(preset.data.name, dirname(presetPath), preset.data.extensions);
55
+ for (const child of preset.data.extends) {
56
+ const loaded = await loadPresetEntry(merged, child, nextSeen, options);
57
+ rules.push(...loaded.rules);
58
+ extensions.push(...loaded.extensions);
59
+ }
60
+ return { rules: rules.filter((rule) => !(preset.data.disable ?? []).includes(rule.id)), extensions };
50
61
  }
51
62
  }
52
63
  // Category folder reference — load every winning file under that prefix.
53
64
  if (merged.categories.has(entry)) {
54
65
  const rules = [];
55
66
  for (const absPath of mergedFilesInCategory(merged, entry)) {
56
- rules.push(...(await loadRuleFile(absPath)));
67
+ rules.push(...normalizeRuleFile(await readStructuredFile(absPath, options), dirname(absPath)));
57
68
  }
58
- return rules;
69
+ return { rules, extensions: [] };
59
70
  }
60
71
  // Sub-path reference — a single winning rule file within a category.
61
72
  const subPath = findMergedFile(merged, entry);
62
73
  if (subPath !== null)
63
- return loadRuleFile(subPath);
64
- return [];
74
+ return {
75
+ rules: normalizeRuleFile(await readStructuredFile(subPath, options), dirname(subPath)),
76
+ extensions: [],
77
+ };
78
+ return { rules: [], extensions: [] };
65
79
  }
66
80
  /**
67
81
  * Build the merged view across ordered roots.
@@ -155,9 +169,11 @@ async function listImmediateDirs(fs, dir) {
155
169
  }
156
170
  return dirs;
157
171
  }
158
- async function readStructuredFile(path) {
159
- const content = await new NodeFileSystem().readFile(path);
160
- return extname(path) === '.json' ? JSON.parse(content) : parse(content);
172
+ async function readStructuredFile(path, options = {}) {
173
+ return await loadStructuredConfig(path, {
174
+ validateSchema: options.validateSchema,
175
+ fetch: options.fetch,
176
+ });
161
177
  }
162
178
  function normalizeRuleFile(raw, sourceDir) {
163
179
  const maybeFile = ConstraintRuleFileSchema.safeParse(raw);
package/dist/types.d.ts CHANGED
@@ -40,6 +40,8 @@ export interface RuleFixConfig {
40
40
  }
41
41
  /** Rule file shape before normalization. */
42
42
  export interface ConstraintRuleFile {
43
+ /** Optional JSON Schema ref used by editors and loader validation. */
44
+ $schema?: string;
43
45
  /** File-level default include patterns. */
44
46
  include?: string[];
45
47
  /** File-level default exclude patterns. */
@@ -62,6 +64,8 @@ export interface PresetExtensions {
62
64
  }
63
65
  /** Preset definition that composes category folders or other presets. */
64
66
  export interface PresetDefinition {
67
+ /** Optional JSON Schema ref used by editors and loader validation. */
68
+ $schema?: string;
65
69
  /** Preset name. */
66
70
  name: string;
67
71
  /** Category folders or preset names to compose. */
@@ -191,6 +195,7 @@ export declare const ConstraintRuleSchema: z.ZodObject<{
191
195
  }, z.core.$strip>;
192
196
  /** Zod schema for a constraint rule file. */
193
197
  export declare const ConstraintRuleFileSchema: z.ZodObject<{
198
+ $schema: z.ZodOptional<z.ZodString>;
194
199
  include: z.ZodOptional<z.ZodArray<z.ZodString>>;
195
200
  exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
196
201
  severity: z.ZodOptional<z.ZodEnum<{
@@ -226,6 +231,7 @@ export declare const ConstraintRuleFileSchema: z.ZodObject<{
226
231
  }, z.core.$strip>;
227
232
  /** Zod schema for a preset definition. */
228
233
  export declare const PresetDefinitionSchema: z.ZodObject<{
234
+ $schema: z.ZodOptional<z.ZodString>;
229
235
  name: z.ZodString;
230
236
  extends: z.ZodDefault<z.ZodArray<z.ZodString>>;
231
237
  disable: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -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
@@ -33,6 +33,7 @@ export const ConstraintRuleSchema = z.object({
33
33
  });
34
34
  /** Zod schema for a constraint rule file. */
35
35
  export const ConstraintRuleFileSchema = z.object({
36
+ $schema: z.string().optional(),
36
37
  include: z.array(z.string()).optional(),
37
38
  exclude: z.array(z.string()).optional(),
38
39
  severity: z.enum(['error', 'warning', 'info']).optional(),
@@ -40,6 +41,7 @@ export const ConstraintRuleFileSchema = z.object({
40
41
  });
41
42
  /** Zod schema for a preset definition. */
42
43
  export const PresetDefinitionSchema = z.object({
44
+ $schema: z.string().optional(),
43
45
  name: z.string().min(1),
44
46
  extends: z.array(z.string()).default([]),
45
47
  disable: z.array(z.string()).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-rule-engine",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  "dist",
35
+ "schemas",
35
36
  "src",
36
37
  "README.md"
37
38
  ],
@@ -47,8 +48,8 @@
47
48
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-rule-engine-v<version> && git push --tags' && exit 1"
48
49
  },
49
50
  "dependencies": {
50
- "@gobing-ai/ts-ai-runner": "^0.2.7",
51
- "@gobing-ai/ts-runtime": "^0.2.7",
51
+ "@gobing-ai/ts-ai-runner": "^0.2.8",
52
+ "@gobing-ai/ts-runtime": "^0.2.8",
52
53
  "yaml": "^2.7.0",
53
54
  "zod": "^4.1.0"
54
55
  },
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/gobing-ai/ts-libs/main/packages/rule-engine/schemas/preset.schema.json",
4
+ "title": "@gobing-ai/ts-rule-engine Preset",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["name"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "name": { "type": "string" },
11
+ "extends": { "type": "array", "items": { "type": "string" } },
12
+ "disable": { "type": "array", "items": { "type": "string" } },
13
+ "overrides": {
14
+ "type": "object",
15
+ "additionalProperties": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "fix": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "mode": { "enum": ["none", "suggest", "auto"] }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "extensions": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "properties": {
33
+ "resolvers": { "type": "array", "items": { "type": "string" } },
34
+ "evaluators": { "type": "array", "items": { "type": "string" } },
35
+ "fixers": { "type": "array", "items": { "type": "string" } },
36
+ "formatters": { "type": "array", "items": { "type": "string" } }
37
+ }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/gobing-ai/ts-libs/main/packages/rule-engine/schemas/rule-file.schema.json",
4
+ "title": "@gobing-ai/ts-rule-engine Rule File",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["rules"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "include": { "type": "array", "items": { "type": "string" } },
11
+ "exclude": { "type": "array", "items": { "type": "string" } },
12
+ "severity": { "enum": ["error", "warning", "info"] },
13
+ "rules": { "type": "array", "items": { "$ref": "#/$defs/rule" } }
14
+ },
15
+ "$defs": {
16
+ "rule": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": ["id", "evaluator"],
20
+ "properties": {
21
+ "id": { "type": "string" },
22
+ "description": { "type": "string" },
23
+ "enabled": { "type": "boolean" },
24
+ "severity": { "enum": ["error", "warning", "info"] },
25
+ "include": { "type": "array", "items": { "type": "string" } },
26
+ "exclude": { "type": "array", "items": { "type": "string" } },
27
+ "evaluator": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "required": ["type"],
31
+ "properties": {
32
+ "type": { "type": "string" },
33
+ "config": { "type": "object", "additionalProperties": true }
34
+ }
35
+ },
36
+ "fix": { "$ref": "#/$defs/fix" }
37
+ }
38
+ },
39
+ "fix": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "mode": { "enum": ["none", "suggest", "auto"] },
44
+ "replacement": { "type": "string" },
45
+ "params": { "type": "object", "additionalProperties": true }
46
+ }
47
+ }
48
+ }
49
+ }
@@ -24,6 +24,8 @@ export interface LoadExtensionsOptions {
24
24
  allowExtensions?: boolean;
25
25
  /** Optional sink for non-fatal warnings (e.g. built-in overrides). */
26
26
  logger?: { warn: (message: string) => void };
27
+ /** Optional module loader seam for tests or embedders with custom import policy. */
28
+ moduleLoader?: (absPath: string) => Promise<Record<string, unknown>>;
27
29
  }
28
30
 
29
31
  /** Host registries that can receive extension capabilities (fixers live on the engine, not the host). */
@@ -79,8 +81,9 @@ export async function loadExtensionsIntoHost(
79
81
  );
80
82
  }
81
83
 
84
+ const loadModule = options.moduleLoader ?? defaultModuleLoader;
82
85
  for (const ref of refs) {
83
- const moduleExports = (await import(ref.absPath)) as Record<string, unknown>;
86
+ const moduleExports = await loadModule(ref.absPath);
84
87
  const candidate = moduleExports.default ?? moduleExports.extension;
85
88
  if (
86
89
  candidate === null ||
@@ -106,3 +109,7 @@ export async function loadExtensionsIntoHost(
106
109
  registry.register(name, candidate, 'extension');
107
110
  }
108
111
  }
112
+
113
+ async function defaultModuleLoader(absPath: string): Promise<Record<string, unknown>> {
114
+ return (await import(absPath)) as Record<string, unknown>;
115
+ }
@@ -1,6 +1,5 @@
1
- import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
2
- import { NodeFileSystem } from '@gobing-ai/ts-runtime';
3
- import { parse } from 'yaml';
1
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
2
+ import { loadStructuredConfig, NodeFileSystem } from '@gobing-ai/ts-runtime';
4
3
  import {
5
4
  type ConstraintRule,
6
5
  type ConstraintRuleFile,
@@ -9,6 +8,7 @@ import {
9
8
  type PresetDefinition,
10
9
  PresetDefinitionSchema,
11
10
  } from '../types';
11
+ import { collectPresetExtensions, type ExtensionRef } from './extensions';
12
12
 
13
13
  /** Options for loading rule presets. */
14
14
  export interface RuleLoaderOptions {
@@ -20,6 +20,25 @@ export interface RuleLoaderOptions {
20
20
  * loader stays agnostic to any project layout convention.
21
21
  */
22
22
  roots: string[];
23
+ /** When true, honor top-level `$schema` refs in preset and rule files. Defaults to true. */
24
+ validateSchema?: boolean;
25
+ /** Optional fetch implementation for remote HTTP(S) schema refs. */
26
+ fetch?: (input: string) => Promise<Response>;
27
+ }
28
+
29
+ export interface RuleFileLoadOptions {
30
+ /** When true, honor top-level `$schema` refs. Defaults to true. */
31
+ validateSchema?: boolean;
32
+ /** Optional fetch implementation for remote HTTP(S) schema refs. */
33
+ fetch?: (input: string) => Promise<Response>;
34
+ }
35
+
36
+ /** Loaded preset rules plus extension module refs declared by composed presets. */
37
+ export interface LoadedPreset {
38
+ /** Normalized rules after preset disable/override handling. */
39
+ readonly rules: ConstraintRule[];
40
+ /** Extension modules declared by the preset graph, resolved to absolute paths. */
41
+ readonly extensions: ExtensionRef[];
23
42
  }
24
43
 
25
44
  /** Merged view of rule roots: winning file per relative path, plus categories. */
@@ -37,14 +56,17 @@ interface MergedRoots {
37
56
  * so a caller can layer project-local rules over shared/global rules and inherit
38
57
  * the rest of a preset's categories from the lower-priority roots.
39
58
  */
40
- export async function loadPresetRules(name: string, options: RuleLoaderOptions): Promise<ConstraintRule[]> {
59
+ export async function loadPreset(name: string, options: RuleLoaderOptions): Promise<LoadedPreset> {
41
60
  const merged = await buildMergedRoots(options.roots.map((root) => resolve(root)));
42
61
  const presetPath = findMergedPreset(merged, name);
43
- if (presetPath === null) return [];
44
- const preset = PresetDefinitionSchema.parse(await readStructuredFile(presetPath)) as PresetDefinition;
62
+ if (presetPath === null) return { rules: [], extensions: [] };
63
+ const preset = PresetDefinitionSchema.parse(await readStructuredFile(presetPath, options)) as PresetDefinition;
45
64
  const rules: ConstraintRule[] = [];
65
+ const extensions = collectPresetExtensions(preset.name, dirname(presetPath), preset.extensions);
46
66
  for (const entry of preset.extends) {
47
- rules.push(...(await loadPresetEntry(merged, entry, new Set([name]))));
67
+ const loaded = await loadPresetEntry(merged, entry, new Set([name]), options);
68
+ rules.push(...loaded.rules);
69
+ extensions.push(...loaded.extensions);
48
70
  }
49
71
  const disabled = new Set(preset.disable ?? []);
50
72
  const normalized = rules.filter((rule) => !disabled.has(rule.id));
@@ -54,27 +76,42 @@ export async function loadPresetRules(name: string, options: RuleLoaderOptions):
54
76
  rule.fix = { ...(rule.fix ?? { mode: 'none' }), ...override.fix };
55
77
  }
56
78
  }
57
- return normalized;
79
+ return { rules: normalized, extensions };
80
+ }
81
+
82
+ export async function loadPresetRules(name: string, options: RuleLoaderOptions): Promise<ConstraintRule[]> {
83
+ return (await loadPreset(name, options)).rules;
58
84
  }
59
85
 
60
86
  /** Load a direct rule file from disk. */
61
- export async function loadRuleFile(filePath: string): Promise<ConstraintRule[]> {
62
- return normalizeRuleFile(await readStructuredFile(resolve(filePath)), dirname(resolve(filePath)));
87
+ export async function loadRuleFile(filePath: string, options: RuleFileLoadOptions = {}): Promise<ConstraintRule[]> {
88
+ const resolved = resolve(filePath);
89
+ return normalizeRuleFile(await readStructuredFile(resolved, options), dirname(resolved));
63
90
  }
64
91
 
65
- async function loadPresetEntry(merged: MergedRoots, entry: string, seen: Set<string>): Promise<ConstraintRule[]> {
92
+ async function loadPresetEntry(
93
+ merged: MergedRoots,
94
+ entry: string,
95
+ seen: Set<string>,
96
+ options: Pick<RuleLoaderOptions, 'validateSchema' | 'fetch'>,
97
+ ): Promise<LoadedPreset> {
66
98
  // Sub-preset reference — recurse, erroring on a genuine cycle.
67
99
  const presetPath = findMergedPreset(merged, entry);
68
100
  if (presetPath !== null) {
69
101
  if (seen.has(entry)) {
70
102
  throw new Error(`Circular preset dependency detected: ${[...seen, entry].join(' → ')}`);
71
103
  }
72
- seen.add(entry);
73
- const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath));
104
+ const nextSeen = new Set([...seen, entry]);
105
+ const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath, options));
74
106
  if (preset.success) {
75
107
  const rules: ConstraintRule[] = [];
76
- for (const child of preset.data.extends) rules.push(...(await loadPresetEntry(merged, child, seen)));
77
- return rules.filter((rule) => !(preset.data.disable ?? []).includes(rule.id));
108
+ const extensions = collectPresetExtensions(preset.data.name, dirname(presetPath), preset.data.extensions);
109
+ for (const child of preset.data.extends) {
110
+ const loaded = await loadPresetEntry(merged, child, nextSeen, options);
111
+ rules.push(...loaded.rules);
112
+ extensions.push(...loaded.extensions);
113
+ }
114
+ return { rules: rules.filter((rule) => !(preset.data.disable ?? []).includes(rule.id)), extensions };
78
115
  }
79
116
  }
80
117
 
@@ -82,16 +119,20 @@ async function loadPresetEntry(merged: MergedRoots, entry: string, seen: Set<str
82
119
  if (merged.categories.has(entry)) {
83
120
  const rules: ConstraintRule[] = [];
84
121
  for (const absPath of mergedFilesInCategory(merged, entry)) {
85
- rules.push(...(await loadRuleFile(absPath)));
122
+ rules.push(...normalizeRuleFile(await readStructuredFile(absPath, options), dirname(absPath)));
86
123
  }
87
- return rules;
124
+ return { rules, extensions: [] };
88
125
  }
89
126
 
90
127
  // Sub-path reference — a single winning rule file within a category.
91
128
  const subPath = findMergedFile(merged, entry);
92
- if (subPath !== null) return loadRuleFile(subPath);
129
+ if (subPath !== null)
130
+ return {
131
+ rules: normalizeRuleFile(await readStructuredFile(subPath, options), dirname(subPath)),
132
+ extensions: [],
133
+ };
93
134
 
94
- return [];
135
+ return { rules: [], extensions: [] };
95
136
  }
96
137
 
97
138
  /**
@@ -183,9 +224,14 @@ async function listImmediateDirs(fs: NodeFileSystem, dir: string): Promise<strin
183
224
  return dirs;
184
225
  }
185
226
 
186
- async function readStructuredFile(path: string): Promise<unknown> {
187
- const content = await new NodeFileSystem().readFile(path);
188
- return extname(path) === '.json' ? JSON.parse(content) : parse(content);
227
+ async function readStructuredFile(
228
+ path: string,
229
+ options: Pick<RuleLoaderOptions, 'validateSchema' | 'fetch'> = {},
230
+ ): Promise<unknown> {
231
+ return await loadStructuredConfig(path, {
232
+ validateSchema: options.validateSchema,
233
+ fetch: options.fetch,
234
+ });
189
235
  }
190
236
 
191
237
  function normalizeRuleFile(raw: unknown, sourceDir: string): ConstraintRule[] {
package/src/types.ts CHANGED
@@ -46,6 +46,8 @@ export interface RuleFixConfig {
46
46
 
47
47
  /** Rule file shape before normalization. */
48
48
  export interface ConstraintRuleFile {
49
+ /** Optional JSON Schema ref used by editors and loader validation. */
50
+ $schema?: string;
49
51
  /** File-level default include patterns. */
50
52
  include?: string[];
51
53
  /** File-level default exclude patterns. */
@@ -70,6 +72,8 @@ export interface PresetExtensions {
70
72
 
71
73
  /** Preset definition that composes category folders or other presets. */
72
74
  export interface PresetDefinition {
75
+ /** Optional JSON Schema ref used by editors and loader validation. */
76
+ $schema?: string;
73
77
  /** Preset name. */
74
78
  name: string;
75
79
  /** Category folders or preset names to compose. */
@@ -206,6 +210,7 @@ export const ConstraintRuleSchema = z.object({
206
210
 
207
211
  /** Zod schema for a constraint rule file. */
208
212
  export const ConstraintRuleFileSchema = z.object({
213
+ $schema: z.string().optional(),
209
214
  include: z.array(z.string()).optional(),
210
215
  exclude: z.array(z.string()).optional(),
211
216
  severity: z.enum(['error', 'warning', 'info']).optional(),
@@ -214,6 +219,7 @@ export const ConstraintRuleFileSchema = z.object({
214
219
 
215
220
  /** Zod schema for a preset definition. */
216
221
  export const PresetDefinitionSchema = z.object({
222
+ $schema: z.string().optional(),
217
223
  name: z.string().min(1),
218
224
  extends: z.array(z.string()).default([]),
219
225
  disable: z.array(z.string()).optional(),