@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.
- package/README.md +565 -1
- package/dist/config/extensions.d.ts +7 -4
- package/dist/config/extensions.d.ts.map +1 -1
- package/dist/config/extensions.js +11 -6
- package/dist/config/loader.d.ts +29 -2
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +104 -34
- package/dist/engine.d.ts +8 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +9 -19
- package/dist/evaluators/coverage-gate-evaluator.js +12 -3
- package/dist/evaluators/file-utils.d.ts +55 -0
- package/dist/evaluators/file-utils.d.ts.map +1 -1
- package/dist/evaluators/file-utils.js +49 -0
- package/dist/evaluators/forbidden-import-evaluator.d.ts +5 -0
- package/dist/evaluators/forbidden-import-evaluator.d.ts.map +1 -1
- package/dist/evaluators/forbidden-import-evaluator.js +14 -17
- package/dist/evaluators/import-boundary-evaluator.d.ts +5 -0
- package/dist/evaluators/import-boundary-evaluator.d.ts.map +1 -1
- package/dist/evaluators/import-boundary-evaluator.js +45 -15
- package/dist/evaluators/regex-evaluator.d.ts +9 -1
- package/dist/evaluators/regex-evaluator.d.ts.map +1 -1
- package/dist/evaluators/regex-evaluator.js +43 -14
- package/dist/evaluators/secrets-scanner-evaluator.d.ts +5 -0
- package/dist/evaluators/secrets-scanner-evaluator.d.ts.map +1 -1
- package/dist/evaluators/secrets-scanner-evaluator.js +13 -13
- package/dist/evaluators/tsdoc-export-evaluator.d.ts.map +1 -1
- package/dist/evaluators/tsdoc-export-evaluator.js +9 -11
- package/dist/formatters/json.d.ts.map +1 -1
- package/dist/formatters/json.js +2 -0
- package/dist/formatters/text.d.ts.map +1 -1
- package/dist/formatters/text.js +2 -0
- package/dist/resolvers/test-path-resolver.d.ts.map +1 -1
- package/dist/resolvers/test-path-resolver.js +5 -0
- package/dist/types.d.ts +28 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +37 -9
- package/package.json +6 -3
- package/schemas/preset.schema.json +54 -0
- package/schemas/rule-file.schema.json +49 -0
- package/src/config/extensions.ts +16 -8
- package/src/config/loader.ts +151 -34
- package/src/engine.ts +9 -19
- package/src/evaluators/coverage-gate-evaluator.ts +15 -5
- package/src/evaluators/file-utils.ts +92 -0
- package/src/evaluators/forbidden-import-evaluator.ts +14 -19
- package/src/evaluators/import-boundary-evaluator.ts +56 -40
- package/src/evaluators/regex-evaluator.ts +43 -13
- package/src/evaluators/secrets-scanner-evaluator.ts +13 -14
- package/src/evaluators/tsdoc-export-evaluator.ts +10 -9
- package/src/formatters/json.ts +2 -0
- package/src/formatters/text.ts +2 -0
- package/src/resolvers/test-path-resolver.ts +5 -0
- 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
|
|
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
|
|
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
|
|
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;
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
+
}
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
22
|
-
|
|
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":"
|
|
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"}
|