@gobing-ai/ts-rule-engine 0.2.9 → 0.3.1
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/dist/host/bundled-rules.d.ts +26 -0
- package/dist/host/bundled-rules.d.ts.map +1 -0
- package/dist/host/bundled-rules.js +76 -0
- package/dist/host/capability-registry.d.ts +9 -23
- package/dist/host/capability-registry.d.ts.map +1 -1
- package/dist/host/capability-registry.js +9 -28
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -6
- package/package.json +6 -5
- package/rules/quality/coverage-gate.yaml +21 -0
- package/rules/quality/tsdoc-exports.yaml +24 -0
- package/rules/recommended.yaml +10 -0
- package/rules/spur-dev.yaml +6 -0
- package/rules/structure/test-location.yaml +38 -0
- package/rules/typescript/no-biome-suppressions.yaml +23 -0
- package/src/host/bundled-rules.ts +78 -0
- package/src/host/capability-registry.ts +9 -41
- package/src/index.ts +1 -0
- package/src/types.ts +13 -6
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the absolute path to the rule presets bundled with
|
|
3
|
+
* `@gobing-ai/ts-rule-engine`.
|
|
4
|
+
*
|
|
5
|
+
* The directory ships portable presets (`recommended`, `spur-dev`) and category
|
|
6
|
+
* folders (`typescript`, `structure`, `quality`) so a consumer gets a working
|
|
7
|
+
* default ruleset without authoring any files. Pass the returned path as the
|
|
8
|
+
* lowest-priority entry to {@link loadPreset}'s `roots`, letting project-local
|
|
9
|
+
* and user-global roots shadow individual files while inheriting the rest.
|
|
10
|
+
*
|
|
11
|
+
* Resolution walks up from this module's compiled location (under `dist/` at
|
|
12
|
+
* runtime, under `src/` in tests) until it finds the bundled `rules/` directory,
|
|
13
|
+
* which makes it robust to the build's `src`→`dist` layout shift. Returns `null`
|
|
14
|
+
* if the directory is absent (e.g. a partial install that excluded the assets).
|
|
15
|
+
*/
|
|
16
|
+
export declare function bundledRulesRoot(): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* List the relative paths of every bundled rule asset (presets and category rule
|
|
19
|
+
* files), each as a `/`-joined path relative to {@link bundledRulesRoot}.
|
|
20
|
+
*
|
|
21
|
+
* Intended for consumers that copy the bundled rules into a writable location
|
|
22
|
+
* (e.g. a per-user global rules directory) on first run. Returns an empty array
|
|
23
|
+
* when no bundled directory is present.
|
|
24
|
+
*/
|
|
25
|
+
export declare function listBundledRuleFiles(): string[];
|
|
26
|
+
//# sourceMappingURL=bundled-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundled-rules.d.ts","sourceRoot":"","sources":["../../src/host/bundled-rules.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAoBD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAI/C"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { dirnamePath, joinPath, NodeSyncFileSystem } from '@gobing-ai/ts-runtime';
|
|
2
|
+
/**
|
|
3
|
+
* Directory name holding the rule presets and category folders bundled with this
|
|
4
|
+
* package. Lives at the package root (sibling to `dist/`), shipped via the
|
|
5
|
+
* package `files` allowlist.
|
|
6
|
+
*/
|
|
7
|
+
const BUNDLED_RULES_DIR = 'rules';
|
|
8
|
+
/** Memoized result so the upward filesystem walk runs at most once per process. */
|
|
9
|
+
let cachedRoot;
|
|
10
|
+
const defaultFs = new NodeSyncFileSystem();
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the absolute path to the rule presets bundled with
|
|
13
|
+
* `@gobing-ai/ts-rule-engine`.
|
|
14
|
+
*
|
|
15
|
+
* The directory ships portable presets (`recommended`, `spur-dev`) and category
|
|
16
|
+
* folders (`typescript`, `structure`, `quality`) so a consumer gets a working
|
|
17
|
+
* default ruleset without authoring any files. Pass the returned path as the
|
|
18
|
+
* lowest-priority entry to {@link loadPreset}'s `roots`, letting project-local
|
|
19
|
+
* and user-global roots shadow individual files while inheriting the rest.
|
|
20
|
+
*
|
|
21
|
+
* Resolution walks up from this module's compiled location (under `dist/` at
|
|
22
|
+
* runtime, under `src/` in tests) until it finds the bundled `rules/` directory,
|
|
23
|
+
* which makes it robust to the build's `src`→`dist` layout shift. Returns `null`
|
|
24
|
+
* if the directory is absent (e.g. a partial install that excluded the assets).
|
|
25
|
+
*/
|
|
26
|
+
export function bundledRulesRoot() {
|
|
27
|
+
return bundledRulesRootWithFs(defaultFs);
|
|
28
|
+
}
|
|
29
|
+
function bundledRulesRootWithFs(fs) {
|
|
30
|
+
if (cachedRoot !== undefined)
|
|
31
|
+
return cachedRoot;
|
|
32
|
+
let dir = import.meta.dirname;
|
|
33
|
+
// Walk to filesystem root at most; the package root is only a few levels up.
|
|
34
|
+
while (true) {
|
|
35
|
+
const candidate = joinPath(dir, BUNDLED_RULES_DIR);
|
|
36
|
+
if (fs.stat(candidate)?.isDirectory() === true) {
|
|
37
|
+
cachedRoot = candidate;
|
|
38
|
+
return cachedRoot;
|
|
39
|
+
}
|
|
40
|
+
const parent = dirnamePath(dir);
|
|
41
|
+
if (parent === dir)
|
|
42
|
+
break;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
cachedRoot = null;
|
|
46
|
+
return cachedRoot;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* List the relative paths of every bundled rule asset (presets and category rule
|
|
50
|
+
* files), each as a `/`-joined path relative to {@link bundledRulesRoot}.
|
|
51
|
+
*
|
|
52
|
+
* Intended for consumers that copy the bundled rules into a writable location
|
|
53
|
+
* (e.g. a per-user global rules directory) on first run. Returns an empty array
|
|
54
|
+
* when no bundled directory is present.
|
|
55
|
+
*/
|
|
56
|
+
export function listBundledRuleFiles() {
|
|
57
|
+
const root = bundledRulesRoot();
|
|
58
|
+
if (root === null)
|
|
59
|
+
return [];
|
|
60
|
+
return walk(defaultFs, root, '').sort();
|
|
61
|
+
}
|
|
62
|
+
/** Recursively collect YAML/JSON files under `dir`, returning paths relative to the walk origin. */
|
|
63
|
+
function walk(fs, dir, relPrefix) {
|
|
64
|
+
const acc = [];
|
|
65
|
+
for (const entry of fs.readDir(dir)) {
|
|
66
|
+
const abs = joinPath(dir, entry);
|
|
67
|
+
const rel = relPrefix.length > 0 ? `${relPrefix}/${entry}` : entry;
|
|
68
|
+
if (fs.stat(abs)?.isDirectory() === true) {
|
|
69
|
+
acc.push(...walk(fs, abs, rel));
|
|
70
|
+
}
|
|
71
|
+
else if (/\.(ya?ml|json)$/i.test(entry)) {
|
|
72
|
+
acc.push(rel);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return acc;
|
|
76
|
+
}
|
|
@@ -1,24 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
/** Typed registry used by the rule engine host. */
|
|
11
|
-
export declare class CapabilityRegistry<TCapability> {
|
|
12
|
-
private readonly kind;
|
|
13
|
-
private readonly capabilities;
|
|
14
|
-
constructor(kind: string);
|
|
15
|
-
/** Register or replace a capability. */
|
|
16
|
-
register(name: string, capability: TCapability, origin?: CapabilityOrigin): void;
|
|
17
|
-
/** Return true when a capability exists. */
|
|
18
|
-
has(name: string): boolean;
|
|
19
|
-
/** Get a registered capability or throw a clear error. */
|
|
20
|
-
get(name: string): TCapability;
|
|
21
|
-
/** List registered capability names. */
|
|
22
|
-
list(): string[];
|
|
23
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility re-export (ADR-010 / task 0008).
|
|
3
|
+
*
|
|
4
|
+
* The capability registry now lives in the shared plugin core
|
|
5
|
+
* (`@gobing-ai/ts-runtime/plugin`). This module re-exports it from the original
|
|
6
|
+
* path so the public barrel and existing importers keep working unchanged. New
|
|
7
|
+
* code should import from `@gobing-ai/ts-runtime/plugin` directly.
|
|
8
|
+
*/
|
|
9
|
+
export { type CapabilityEntry, type CapabilityOrigin, CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
|
|
24
10
|
//# sourceMappingURL=capability-registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capability-registry.d.ts","sourceRoot":"","sources":["../../src/host/capability-registry.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"capability-registry.d.ts","sourceRoot":"","sources":["../../src/host/capability-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -1,28 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.capabilities.set(name, { capability, origin });
|
|
11
|
-
}
|
|
12
|
-
/** Return true when a capability exists. */
|
|
13
|
-
has(name) {
|
|
14
|
-
return this.capabilities.has(name);
|
|
15
|
-
}
|
|
16
|
-
/** Get a registered capability or throw a clear error. */
|
|
17
|
-
get(name) {
|
|
18
|
-
const entry = this.capabilities.get(name);
|
|
19
|
-
if (entry === undefined) {
|
|
20
|
-
throw new Error(`Unknown ${this.kind}: ${name}`);
|
|
21
|
-
}
|
|
22
|
-
return entry.capability;
|
|
23
|
-
}
|
|
24
|
-
/** List registered capability names. */
|
|
25
|
-
list() {
|
|
26
|
-
return [...this.capabilities.keys()];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility re-export (ADR-010 / task 0008).
|
|
3
|
+
*
|
|
4
|
+
* The capability registry now lives in the shared plugin core
|
|
5
|
+
* (`@gobing-ai/ts-runtime/plugin`). This module re-exports it from the original
|
|
6
|
+
* path so the public barrel and existing importers keep working unchanged. New
|
|
7
|
+
* code should import from `@gobing-ai/ts-runtime/plugin` directly.
|
|
8
|
+
*/
|
|
9
|
+
export { CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './fixers/fixers';
|
|
|
5
5
|
export * from './fixers/test-stub-fixer';
|
|
6
6
|
export * from './formatters/json';
|
|
7
7
|
export * from './formatters/text';
|
|
8
|
+
export * from './host/bundled-rules';
|
|
8
9
|
export * from './host/capability-registry';
|
|
9
10
|
export * from './host/rule-engine-host';
|
|
10
11
|
export * from './resolvers/test-path-resolver';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from './fixers/fixers.js';
|
|
|
5
5
|
export * from './fixers/test-stub-fixer.js';
|
|
6
6
|
export * from './formatters/json.js';
|
|
7
7
|
export * from './formatters/text.js';
|
|
8
|
+
export * from './host/bundled-rules.js';
|
|
8
9
|
export * from './host/capability-registry.js';
|
|
9
10
|
export * from './host/rule-engine-host.js';
|
|
10
11
|
export * from './resolvers/test-path-resolver.js';
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRelativeExtensionPath } from '@gobing-ai/ts-runtime/plugin';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/** Create a finding with inherited rule severity. */
|
|
3
4
|
export function createFinding(rule, message, filePath, extras = {}) {
|
|
@@ -39,16 +40,23 @@ export const ConstraintRuleSchema = z.object({
|
|
|
39
40
|
*
|
|
40
41
|
* Rejects absolute paths and `..` traversal: extension declarations are data, and a
|
|
41
42
|
* path that escapes the declaring file's directory is a trust-boundary violation even
|
|
42
|
-
* when extension loading is explicitly allowed.
|
|
43
|
+
* when extension loading is explicitly allowed. The actual guard is the shared
|
|
44
|
+
* `assertRelativeExtensionPath` (ADR-010) so schema-time validation here and load-time
|
|
45
|
+
* validation in the shared loader use one source of truth.
|
|
43
46
|
*/
|
|
44
47
|
const relativeExtensionPath = z
|
|
45
48
|
.string()
|
|
46
49
|
.min(1)
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
.superRefine((value, ctx) => {
|
|
51
|
+
try {
|
|
52
|
+
assertRelativeExtensionPath(value);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: 'custom',
|
|
57
|
+
message: error instanceof Error ? error.message : 'invalid extension path',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
52
60
|
});
|
|
53
61
|
/**
|
|
54
62
|
* Shared zod schema for an `extensions` block, used by both preset and rule-file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-rule-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
"rules",
|
|
35
36
|
"schemas",
|
|
36
37
|
"src",
|
|
37
38
|
"README.md"
|
|
@@ -48,10 +49,10 @@
|
|
|
48
49
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-rule-engine-v<version> && git push --tags' && exit 1"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
|
-
"@gobing-ai/ts-ai-runner": "^0.
|
|
52
|
-
"@gobing-ai/ts-db": "^0.
|
|
53
|
-
"@gobing-ai/ts-runtime": "^0.
|
|
54
|
-
"@gobing-ai/ts-utils": "^0.
|
|
52
|
+
"@gobing-ai/ts-ai-runner": "^0.3.1",
|
|
53
|
+
"@gobing-ai/ts-db": "^0.3.1",
|
|
54
|
+
"@gobing-ai/ts-runtime": "^0.3.1",
|
|
55
|
+
"@gobing-ai/ts-utils": "^0.3.1",
|
|
55
56
|
"yaml": "^2.7.0",
|
|
56
57
|
"zod": "^4.1.0"
|
|
57
58
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Coverage gate — per-file line coverage must meet threshold.
|
|
2
|
+
rules:
|
|
3
|
+
- id: coverage-gate
|
|
4
|
+
description: "Per-file line coverage meets threshold (read from lcov)"
|
|
5
|
+
severity: error
|
|
6
|
+
evaluator:
|
|
7
|
+
type: coverage-gate
|
|
8
|
+
config:
|
|
9
|
+
lcovPath: coverage/lcov.info
|
|
10
|
+
threshold: 90
|
|
11
|
+
include:
|
|
12
|
+
- "apps/**"
|
|
13
|
+
- "packages/**"
|
|
14
|
+
exclude:
|
|
15
|
+
- "**/*.test.ts"
|
|
16
|
+
- "**/*.test.tsx"
|
|
17
|
+
- "**/*.spec.ts"
|
|
18
|
+
- "**/*.spec.tsx"
|
|
19
|
+
- "**/node_modules/**"
|
|
20
|
+
- "**/drizzle/**"
|
|
21
|
+
- "scripts/**"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# TSDoc export rules — every public export must carry a JSDoc comment.
|
|
2
|
+
rules:
|
|
3
|
+
- id: every-export-has-tsdoc
|
|
4
|
+
description: "Every export (function, class, interface, type, const, enum) must have a /** ... */ JSDoc comment describing what it is and why."
|
|
5
|
+
severity: error
|
|
6
|
+
evaluator:
|
|
7
|
+
type: tsdoc-export
|
|
8
|
+
config:
|
|
9
|
+
kinds:
|
|
10
|
+
- function
|
|
11
|
+
- class
|
|
12
|
+
- interface
|
|
13
|
+
- type
|
|
14
|
+
- const
|
|
15
|
+
- enum
|
|
16
|
+
include:
|
|
17
|
+
- "packages/**/src/**/*.ts"
|
|
18
|
+
- "apps/**/src/**/*.ts"
|
|
19
|
+
exclude:
|
|
20
|
+
- "**/index.ts"
|
|
21
|
+
- "**/node_modules/**"
|
|
22
|
+
- "**/tests/**"
|
|
23
|
+
- "**/*.test.ts"
|
|
24
|
+
- "**/*.test.tsx"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Recommended preset — portable rule categories for TypeScript projects.
|
|
2
|
+
# Use with: spur rule run --preset recommended
|
|
3
|
+
#
|
|
4
|
+
# Bundled with @gobing-ai/ts-rule-engine. A project may override individual rule
|
|
5
|
+
# files by placing same-named files under its local .spur/rules/ root.
|
|
6
|
+
name: recommended
|
|
7
|
+
extends:
|
|
8
|
+
- typescript
|
|
9
|
+
- structure
|
|
10
|
+
- quality
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Test location rules — tests must live in tests/ directories mirroring src/.
|
|
2
|
+
# No __tests__ directories, no .test.ts files under src/.
|
|
3
|
+
rules:
|
|
4
|
+
- id: no-tests-dir
|
|
5
|
+
description: "Tests must live in tests/ directories at the same level as src/, not in __tests__."
|
|
6
|
+
severity: error
|
|
7
|
+
evaluator:
|
|
8
|
+
type: test-location
|
|
9
|
+
config:
|
|
10
|
+
expected: "**/*.test.*"
|
|
11
|
+
forbid:
|
|
12
|
+
- "**/__tests__/**"
|
|
13
|
+
- "**/src/**/*.test.ts"
|
|
14
|
+
- "**/src/**/*.test.tsx"
|
|
15
|
+
requireCorrespondingTest: false
|
|
16
|
+
include:
|
|
17
|
+
- "packages/**/tests/**/*.test.ts"
|
|
18
|
+
- "apps/**/tests/**/*.test.ts"
|
|
19
|
+
- "packages/**/tests/**/*.test.tsx"
|
|
20
|
+
- "apps/**/tests/**/*.test.tsx"
|
|
21
|
+
|
|
22
|
+
- id: require-corresponding-test
|
|
23
|
+
description: "Every source file must have a corresponding test file in tests/."
|
|
24
|
+
severity: warning
|
|
25
|
+
evaluator:
|
|
26
|
+
type: test-location
|
|
27
|
+
config:
|
|
28
|
+
expected: "**/*.test.*"
|
|
29
|
+
requireCorrespondingTest: true
|
|
30
|
+
include:
|
|
31
|
+
- "packages/**/src/**/*.ts"
|
|
32
|
+
- "apps/**/src/**/*.ts"
|
|
33
|
+
exclude:
|
|
34
|
+
- "**/index.ts"
|
|
35
|
+
- "**/*.d.ts"
|
|
36
|
+
- "**/types.ts"
|
|
37
|
+
- "**/node_modules/**"
|
|
38
|
+
- "**/tests/**"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# No Biome suppression comments — disallow biome-ignore comments.
|
|
2
|
+
# Lint issues must be fixed at the source, not silenced.
|
|
3
|
+
include:
|
|
4
|
+
- "packages/**/src/**/*.ts"
|
|
5
|
+
- "packages/**/src/**/*.tsx"
|
|
6
|
+
- "apps/**/src/**/*.ts"
|
|
7
|
+
- "apps/**/src/**/*.tsx"
|
|
8
|
+
- "packages/**/tests/**/*.ts"
|
|
9
|
+
- "packages/**/tests/**/*.tsx"
|
|
10
|
+
- "apps/**/tests/**/*.ts"
|
|
11
|
+
- "apps/**/tests/**/*.tsx"
|
|
12
|
+
exclude:
|
|
13
|
+
- "**/node_modules/**"
|
|
14
|
+
rules:
|
|
15
|
+
- id: no-biome-suppressions
|
|
16
|
+
description: >
|
|
17
|
+
Do not use biome-ignore or biome-ignore-all comments to suppress lint
|
|
18
|
+
rules. Fix the underlying issue instead.
|
|
19
|
+
severity: error
|
|
20
|
+
evaluator:
|
|
21
|
+
type: regex
|
|
22
|
+
config:
|
|
23
|
+
pattern: "biome-ignore"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { dirnamePath, joinPath, NodeSyncFileSystem, type SyncFileSystem } from '@gobing-ai/ts-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directory name holding the rule presets and category folders bundled with this
|
|
5
|
+
* package. Lives at the package root (sibling to `dist/`), shipped via the
|
|
6
|
+
* package `files` allowlist.
|
|
7
|
+
*/
|
|
8
|
+
const BUNDLED_RULES_DIR = 'rules';
|
|
9
|
+
|
|
10
|
+
/** Memoized result so the upward filesystem walk runs at most once per process. */
|
|
11
|
+
let cachedRoot: string | null | undefined;
|
|
12
|
+
const defaultFs = new NodeSyncFileSystem();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the absolute path to the rule presets bundled with
|
|
16
|
+
* `@gobing-ai/ts-rule-engine`.
|
|
17
|
+
*
|
|
18
|
+
* The directory ships portable presets (`recommended`, `spur-dev`) and category
|
|
19
|
+
* folders (`typescript`, `structure`, `quality`) so a consumer gets a working
|
|
20
|
+
* default ruleset without authoring any files. Pass the returned path as the
|
|
21
|
+
* lowest-priority entry to {@link loadPreset}'s `roots`, letting project-local
|
|
22
|
+
* and user-global roots shadow individual files while inheriting the rest.
|
|
23
|
+
*
|
|
24
|
+
* Resolution walks up from this module's compiled location (under `dist/` at
|
|
25
|
+
* runtime, under `src/` in tests) until it finds the bundled `rules/` directory,
|
|
26
|
+
* which makes it robust to the build's `src`→`dist` layout shift. Returns `null`
|
|
27
|
+
* if the directory is absent (e.g. a partial install that excluded the assets).
|
|
28
|
+
*/
|
|
29
|
+
export function bundledRulesRoot(): string | null {
|
|
30
|
+
return bundledRulesRootWithFs(defaultFs);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function bundledRulesRootWithFs(fs: SyncFileSystem): string | null {
|
|
34
|
+
if (cachedRoot !== undefined) return cachedRoot;
|
|
35
|
+
let dir = import.meta.dirname;
|
|
36
|
+
// Walk to filesystem root at most; the package root is only a few levels up.
|
|
37
|
+
while (true) {
|
|
38
|
+
const candidate = joinPath(dir, BUNDLED_RULES_DIR);
|
|
39
|
+
if (fs.stat(candidate)?.isDirectory() === true) {
|
|
40
|
+
cachedRoot = candidate;
|
|
41
|
+
return cachedRoot;
|
|
42
|
+
}
|
|
43
|
+
const parent = dirnamePath(dir);
|
|
44
|
+
if (parent === dir) break;
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
cachedRoot = null;
|
|
48
|
+
return cachedRoot;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* List the relative paths of every bundled rule asset (presets and category rule
|
|
53
|
+
* files), each as a `/`-joined path relative to {@link bundledRulesRoot}.
|
|
54
|
+
*
|
|
55
|
+
* Intended for consumers that copy the bundled rules into a writable location
|
|
56
|
+
* (e.g. a per-user global rules directory) on first run. Returns an empty array
|
|
57
|
+
* when no bundled directory is present.
|
|
58
|
+
*/
|
|
59
|
+
export function listBundledRuleFiles(): string[] {
|
|
60
|
+
const root = bundledRulesRoot();
|
|
61
|
+
if (root === null) return [];
|
|
62
|
+
return walk(defaultFs, root, '').sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Recursively collect YAML/JSON files under `dir`, returning paths relative to the walk origin. */
|
|
66
|
+
function walk(fs: SyncFileSystem, dir: string, relPrefix: string): string[] {
|
|
67
|
+
const acc: string[] = [];
|
|
68
|
+
for (const entry of fs.readDir(dir)) {
|
|
69
|
+
const abs = joinPath(dir, entry);
|
|
70
|
+
const rel = relPrefix.length > 0 ? `${relPrefix}/${entry}` : entry;
|
|
71
|
+
if (fs.stat(abs)?.isDirectory() === true) {
|
|
72
|
+
acc.push(...walk(fs, abs, rel));
|
|
73
|
+
} else if (/\.(ya?ml|json)$/i.test(entry)) {
|
|
74
|
+
acc.push(rel);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return acc;
|
|
78
|
+
}
|
|
@@ -1,41 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Typed registry used by the rule engine host. */
|
|
13
|
-
export class CapabilityRegistry<TCapability> {
|
|
14
|
-
private readonly capabilities = new Map<string, CapabilityEntry<TCapability>>();
|
|
15
|
-
|
|
16
|
-
constructor(private readonly kind: string) {}
|
|
17
|
-
|
|
18
|
-
/** Register or replace a capability. */
|
|
19
|
-
register(name: string, capability: TCapability, origin: CapabilityOrigin = 'extension'): void {
|
|
20
|
-
this.capabilities.set(name, { capability, origin });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Return true when a capability exists. */
|
|
24
|
-
has(name: string): boolean {
|
|
25
|
-
return this.capabilities.has(name);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Get a registered capability or throw a clear error. */
|
|
29
|
-
get(name: string): TCapability {
|
|
30
|
-
const entry = this.capabilities.get(name);
|
|
31
|
-
if (entry === undefined) {
|
|
32
|
-
throw new Error(`Unknown ${this.kind}: ${name}`);
|
|
33
|
-
}
|
|
34
|
-
return entry.capability;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** List registered capability names. */
|
|
38
|
-
list(): string[] {
|
|
39
|
-
return [...this.capabilities.keys()];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility re-export (ADR-010 / task 0008).
|
|
3
|
+
*
|
|
4
|
+
* The capability registry now lives in the shared plugin core
|
|
5
|
+
* (`@gobing-ai/ts-runtime/plugin`). This module re-exports it from the original
|
|
6
|
+
* path so the public barrel and existing importers keep working unchanged. New
|
|
7
|
+
* code should import from `@gobing-ai/ts-runtime/plugin` directly.
|
|
8
|
+
*/
|
|
9
|
+
export { type CapabilityEntry, type CapabilityOrigin, CapabilityRegistry } from '@gobing-ai/ts-runtime/plugin';
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './fixers/fixers';
|
|
|
5
5
|
export * from './fixers/test-stub-fixer';
|
|
6
6
|
export * from './formatters/json';
|
|
7
7
|
export * from './formatters/text';
|
|
8
|
+
export * from './host/bundled-rules';
|
|
8
9
|
export * from './host/capability-registry';
|
|
9
10
|
export * from './host/rule-engine-host';
|
|
10
11
|
export * from './resolvers/test-path-resolver';
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertRelativeExtensionPath } from '@gobing-ai/ts-runtime/plugin';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
|
|
3
4
|
/** Finding severity emitted by the rule engine. */
|
|
@@ -218,16 +219,22 @@ export const ConstraintRuleSchema = z.object({
|
|
|
218
219
|
*
|
|
219
220
|
* Rejects absolute paths and `..` traversal: extension declarations are data, and a
|
|
220
221
|
* path that escapes the declaring file's directory is a trust-boundary violation even
|
|
221
|
-
* when extension loading is explicitly allowed.
|
|
222
|
+
* when extension loading is explicitly allowed. The actual guard is the shared
|
|
223
|
+
* `assertRelativeExtensionPath` (ADR-010) so schema-time validation here and load-time
|
|
224
|
+
* validation in the shared loader use one source of truth.
|
|
222
225
|
*/
|
|
223
226
|
const relativeExtensionPath = z
|
|
224
227
|
.string()
|
|
225
228
|
.min(1)
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
.superRefine((value, ctx) => {
|
|
230
|
+
try {
|
|
231
|
+
assertRelativeExtensionPath(value);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
ctx.addIssue({
|
|
234
|
+
code: 'custom',
|
|
235
|
+
message: error instanceof Error ? error.message : 'invalid extension path',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
231
238
|
});
|
|
232
239
|
|
|
233
240
|
/**
|