@gobing-ai/ts-rule-engine 0.2.4 → 0.2.6
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/config/loader.d.ts +15 -5
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +115 -31
- package/dist/evaluators/coverage-gate-evaluator.d.ts +21 -0
- package/dist/evaluators/coverage-gate-evaluator.d.ts.map +1 -0
- package/dist/evaluators/coverage-gate-evaluator.js +103 -0
- package/dist/evaluators/file-utils.d.ts +8 -0
- package/dist/evaluators/file-utils.d.ts.map +1 -1
- package/dist/evaluators/file-utils.js +40 -0
- package/dist/evaluators/test-location-evaluator.d.ts +19 -0
- package/dist/evaluators/test-location-evaluator.d.ts.map +1 -0
- package/dist/evaluators/test-location-evaluator.js +85 -0
- package/dist/evaluators/tsdoc-export-evaluator.d.ts +15 -0
- package/dist/evaluators/tsdoc-export-evaluator.d.ts.map +1 -0
- package/dist/evaluators/tsdoc-export-evaluator.js +77 -0
- package/dist/host/builtins.d.ts.map +1 -1
- package/dist/host/builtins.js +6 -0
- package/package.json +3 -3
- package/src/config/loader.ts +128 -33
- package/src/evaluators/coverage-gate-evaluator.ts +137 -0
- package/src/evaluators/file-utils.ts +38 -0
- package/src/evaluators/test-location-evaluator.ts +115 -0
- package/src/evaluators/tsdoc-export-evaluator.ts +97 -0
- package/src/host/builtins.ts +6 -0
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { type ConstraintRule } from '../types';
|
|
2
2
|
/** Options for loading rule presets. */
|
|
3
3
|
export interface RuleLoaderOptions {
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Ordered rule root directories, highest priority first. Presets, category
|
|
6
|
+
* folders, and rule files are resolved across all roots: the highest-priority
|
|
7
|
+
* root that provides a given relative path wins, and gaps are filled from
|
|
8
|
+
* lower-priority roots. The caller owns root discovery and ordering — this
|
|
9
|
+
* loader stays agnostic to any project layout convention.
|
|
10
|
+
*/
|
|
11
|
+
roots: string[];
|
|
8
12
|
}
|
|
9
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Load and normalize a preset by name, resolving across one or more rule roots.
|
|
15
|
+
*
|
|
16
|
+
* Roots are merged in order: the first root to provide a relative path owns it,
|
|
17
|
+
* so a caller can layer project-local rules over shared/global rules and inherit
|
|
18
|
+
* the rest of a preset's categories from the lower-priority roots.
|
|
19
|
+
*/
|
|
10
20
|
export declare function loadPresetRules(name: string, options: RuleLoaderOptions): Promise<ConstraintRule[]>;
|
|
11
21
|
/** Load a direct rule file from disk. */
|
|
12
22
|
export declare function loadRuleFile(filePath: string): Promise<ConstraintRule[]>;
|
|
@@ -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
|
|
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"}
|
package/dist/config/loader.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
1
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
2
2
|
import { NodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { ConstraintRuleFileSchema, ConstraintRuleSchema, PresetDefinitionSchema, } from '../types.js';
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Load and normalize a preset by name, resolving across one or more rule roots.
|
|
7
|
+
*
|
|
8
|
+
* Roots are merged in order: the first root to provide a relative path owns it,
|
|
9
|
+
* so a caller can layer project-local rules over shared/global rules and inherit
|
|
10
|
+
* the rest of a preset's categories from the lower-priority roots.
|
|
11
|
+
*/
|
|
6
12
|
export async function loadPresetRules(name, options) {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const presetPath = await findDefinitionPath(root, name);
|
|
13
|
+
const merged = await buildMergedRoots(options.roots.map((root) => resolve(root)));
|
|
14
|
+
const presetPath = findMergedPreset(merged, name);
|
|
10
15
|
if (presetPath === null)
|
|
11
16
|
return [];
|
|
12
17
|
const preset = PresetDefinitionSchema.parse(await readStructuredFile(presetPath));
|
|
13
18
|
const rules = [];
|
|
14
19
|
for (const entry of preset.extends) {
|
|
15
|
-
rules.push(...(await loadPresetEntry(
|
|
20
|
+
rules.push(...(await loadPresetEntry(merged, entry, new Set([name]))));
|
|
16
21
|
}
|
|
17
22
|
const disabled = new Set(preset.disable ?? []);
|
|
18
23
|
const normalized = rules.filter((rule) => !disabled.has(rule.id));
|
|
@@ -22,52 +27,131 @@ export async function loadPresetRules(name, options) {
|
|
|
22
27
|
rule.fix = { ...(rule.fix ?? { mode: 'none' }), ...override.fix };
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
await fs.exists(root);
|
|
26
30
|
return normalized;
|
|
27
31
|
}
|
|
28
32
|
/** Load a direct rule file from disk. */
|
|
29
33
|
export async function loadRuleFile(filePath) {
|
|
30
34
|
return normalizeRuleFile(await readStructuredFile(resolve(filePath)), dirname(resolve(filePath)));
|
|
31
35
|
}
|
|
32
|
-
async function loadPresetEntry(
|
|
33
|
-
|
|
36
|
+
async function loadPresetEntry(merged, entry, seen) {
|
|
37
|
+
// Sub-preset reference — recurse (cycle-guarded).
|
|
38
|
+
const presetPath = findMergedPreset(merged, entry);
|
|
34
39
|
if (presetPath !== null && !seen.has(entry)) {
|
|
35
40
|
seen.add(entry);
|
|
36
41
|
const preset = PresetDefinitionSchema.safeParse(await readStructuredFile(presetPath));
|
|
37
42
|
if (preset.success) {
|
|
38
43
|
const rules = [];
|
|
39
44
|
for (const child of preset.data.extends)
|
|
40
|
-
rules.push(...(await loadPresetEntry(
|
|
45
|
+
rules.push(...(await loadPresetEntry(merged, child, seen)));
|
|
41
46
|
return rules.filter((rule) => !(preset.data.disable ?? []).includes(rule.id));
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
rules.push(...(await loadRuleFile(join(categoryDir, file))));
|
|
49
|
+
// Category folder reference — load every winning file under that prefix.
|
|
50
|
+
if (merged.categories.has(entry)) {
|
|
51
|
+
const rules = [];
|
|
52
|
+
for (const absPath of mergedFilesInCategory(merged, entry)) {
|
|
53
|
+
rules.push(...(await loadRuleFile(absPath)));
|
|
54
|
+
}
|
|
55
|
+
return rules;
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
// Sub-path reference — a single winning rule file within a category.
|
|
58
|
+
const subPath = findMergedFile(merged, entry);
|
|
59
|
+
if (subPath !== null)
|
|
60
|
+
return loadRuleFile(subPath);
|
|
61
|
+
return [];
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Build the merged view across ordered roots.
|
|
65
|
+
*
|
|
66
|
+
* Roots are processed in the order supplied (highest priority first). The first
|
|
67
|
+
* root to provide a given relative path owns that file; later roots are shadowed.
|
|
68
|
+
*/
|
|
69
|
+
async function buildMergedRoots(roots) {
|
|
56
70
|
const fs = new NodeFileSystem();
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
const files = new Map();
|
|
72
|
+
const categories = new Set();
|
|
73
|
+
for (const root of roots) {
|
|
74
|
+
for (const absPath of await walkYamlFiles(fs, root)) {
|
|
75
|
+
const relPath = relative(root, absPath).split(sep).join('/');
|
|
76
|
+
const slashIdx = relPath.indexOf('/');
|
|
77
|
+
if (slashIdx > 0)
|
|
78
|
+
categories.add(relPath.slice(0, slashIdx));
|
|
79
|
+
if (!files.has(relPath))
|
|
80
|
+
files.set(relPath, absPath);
|
|
81
|
+
}
|
|
82
|
+
for (const dir of await listImmediateDirs(fs, root))
|
|
83
|
+
categories.add(dir);
|
|
84
|
+
}
|
|
85
|
+
return { files, categories };
|
|
86
|
+
}
|
|
87
|
+
/** Find a preset definition across roots: `<name>.{yaml,yml,json}` or `<name>/index.*`. */
|
|
88
|
+
function findMergedPreset(merged, name) {
|
|
89
|
+
return firstHit(merged, [
|
|
90
|
+
`${name}.yaml`,
|
|
91
|
+
`${name}.yml`,
|
|
92
|
+
`${name}.json`,
|
|
93
|
+
`${name}/index.yaml`,
|
|
94
|
+
`${name}/index.yml`,
|
|
95
|
+
`${name}/index.json`,
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
/** Find a single rule file by sub-path entry (e.g. `typescript/tsdoc-exports`). */
|
|
99
|
+
function findMergedFile(merged, entry) {
|
|
100
|
+
const hasExt = /\.(ya?ml|json)$/i.test(entry);
|
|
101
|
+
return firstHit(merged, hasExt ? [entry] : [`${entry}.yaml`, `${entry}.yml`, `${entry}.json`]);
|
|
102
|
+
}
|
|
103
|
+
/** Return the winning absolute path for the first matching relative candidate. */
|
|
104
|
+
function firstHit(merged, relCandidates) {
|
|
105
|
+
for (const rel of relCandidates) {
|
|
106
|
+
const hit = merged.files.get(rel);
|
|
107
|
+
if (hit !== undefined)
|
|
108
|
+
return hit;
|
|
68
109
|
}
|
|
69
110
|
return null;
|
|
70
111
|
}
|
|
112
|
+
/** Winning files under a category prefix, sorted by relative path. */
|
|
113
|
+
function mergedFilesInCategory(merged, category) {
|
|
114
|
+
const prefix = `${category}/`;
|
|
115
|
+
return [...merged.files.entries()]
|
|
116
|
+
.filter(([relPath]) => relPath.startsWith(prefix))
|
|
117
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
118
|
+
.map(([, absPath]) => absPath);
|
|
119
|
+
}
|
|
120
|
+
/** Recursively collect YAML/JSON file paths under a directory, skipping root-level `presets/`. */
|
|
121
|
+
async function walkYamlFiles(fs, dir, depth = 0) {
|
|
122
|
+
const stat = await fs.stat(dir);
|
|
123
|
+
if (stat === null || !stat.isDirectory())
|
|
124
|
+
return [];
|
|
125
|
+
const acc = [];
|
|
126
|
+
for (const entry of (await fs.readDir(dir)).sort()) {
|
|
127
|
+
if (depth === 0 && entry === 'presets')
|
|
128
|
+
continue;
|
|
129
|
+
const fullPath = join(dir, entry);
|
|
130
|
+
const entryStat = await fs.stat(fullPath);
|
|
131
|
+
if (entryStat?.isDirectory()) {
|
|
132
|
+
acc.push(...(await walkYamlFiles(fs, fullPath, depth + 1)));
|
|
133
|
+
}
|
|
134
|
+
else if (entryStat?.isFile() && /\.(ya?ml|json)$/i.test(entry)) {
|
|
135
|
+
acc.push(fullPath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return acc;
|
|
139
|
+
}
|
|
140
|
+
/** List immediate subdirectory names of a root (excluding `presets`). */
|
|
141
|
+
async function listImmediateDirs(fs, dir) {
|
|
142
|
+
const stat = await fs.stat(dir);
|
|
143
|
+
if (stat === null || !stat.isDirectory())
|
|
144
|
+
return [];
|
|
145
|
+
const dirs = [];
|
|
146
|
+
for (const entry of await fs.readDir(dir)) {
|
|
147
|
+
if (entry === 'presets')
|
|
148
|
+
continue;
|
|
149
|
+
const entryStat = await fs.stat(join(dir, entry));
|
|
150
|
+
if (entryStat?.isDirectory())
|
|
151
|
+
dirs.push(entry);
|
|
152
|
+
}
|
|
153
|
+
return dirs;
|
|
154
|
+
}
|
|
71
155
|
async function readStructuredFile(path) {
|
|
72
156
|
const content = await new NodeFileSystem().readFile(path);
|
|
73
157
|
return extname(path) === '.json' ? JSON.parse(content) : parse(content);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Coverage gate evaluator — reads an lcov tracefile and enforces per-file line
|
|
4
|
+
* coverage thresholds.
|
|
5
|
+
*
|
|
6
|
+
* Config (`evaluator.config`):
|
|
7
|
+
* - `lcovPath`: path to lcov.info (default: `coverage/lcov.info`)
|
|
8
|
+
* - `threshold`: default line-coverage percentage (default: `90`)
|
|
9
|
+
* - `include` / `exclude`: glob patterns scoping the check
|
|
10
|
+
* - `exemptions`: per-file threshold overrides with a reason
|
|
11
|
+
*
|
|
12
|
+
* A missing lcov file is reported as a single non-blocking finding rather than an
|
|
13
|
+
* error, so the gate degrades gracefully when coverage was not generated.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CoverageGateEvaluator implements RuleEvaluator {
|
|
16
|
+
private readonly fs;
|
|
17
|
+
constructor();
|
|
18
|
+
/** Evaluate per-file coverage against the configured threshold. */
|
|
19
|
+
evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=coverage-gate-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coverage-gate-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/coverage-gate-evaluator.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAyBlB;;;;;;;;;;;;GAYG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACvD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;;IAMpC,mEAAmE;IAC7D,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CA0C5F"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
import { NodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
3
|
+
import { createFinding, } from '../types.js';
|
|
4
|
+
import { matchesGlob } from './file-utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Coverage gate evaluator — reads an lcov tracefile and enforces per-file line
|
|
7
|
+
* coverage thresholds.
|
|
8
|
+
*
|
|
9
|
+
* Config (`evaluator.config`):
|
|
10
|
+
* - `lcovPath`: path to lcov.info (default: `coverage/lcov.info`)
|
|
11
|
+
* - `threshold`: default line-coverage percentage (default: `90`)
|
|
12
|
+
* - `include` / `exclude`: glob patterns scoping the check
|
|
13
|
+
* - `exemptions`: per-file threshold overrides with a reason
|
|
14
|
+
*
|
|
15
|
+
* A missing lcov file is reported as a single non-blocking finding rather than an
|
|
16
|
+
* error, so the gate degrades gracefully when coverage was not generated.
|
|
17
|
+
*/
|
|
18
|
+
export class CoverageGateEvaluator {
|
|
19
|
+
fs;
|
|
20
|
+
constructor() {
|
|
21
|
+
this.fs = new NodeFileSystem();
|
|
22
|
+
}
|
|
23
|
+
/** Evaluate per-file coverage against the configured threshold. */
|
|
24
|
+
async evaluate(rule, context) {
|
|
25
|
+
const config = (rule.evaluator.config ?? {});
|
|
26
|
+
const lcovPath = config.lcovPath
|
|
27
|
+
? resolve(context.workdir, config.lcovPath)
|
|
28
|
+
: resolve(context.workdir, 'coverage', 'lcov.info');
|
|
29
|
+
if (!(await this.fs.exists(lcovPath))) {
|
|
30
|
+
return {
|
|
31
|
+
findings: [
|
|
32
|
+
createFinding(rule, 'No lcov file found — coverage not generated. Skipping gate.', lcovPath, {
|
|
33
|
+
code: 'coverage:missing-lcov',
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
fixes: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const coverage = parseLcov(await this.fs.readFile(lcovPath));
|
|
40
|
+
const threshold = config.threshold ?? 90;
|
|
41
|
+
const exemptions = new Map((config.exemptions ?? []).map((entry) => [entry.path, entry]));
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const [rawPath, cov] of coverage) {
|
|
44
|
+
if (cov.linesFound === 0)
|
|
45
|
+
continue;
|
|
46
|
+
const filePath = normalizeLcovSourcePath(context.workdir, rawPath);
|
|
47
|
+
if (config.include !== undefined && !config.include.some((p) => matchesGlob(filePath, p)))
|
|
48
|
+
continue;
|
|
49
|
+
if (config.exclude?.some((p) => matchesGlob(filePath, p)))
|
|
50
|
+
continue;
|
|
51
|
+
if (isAlwaysExcluded(filePath))
|
|
52
|
+
continue;
|
|
53
|
+
const pct = Math.round((cov.linesHit / cov.linesFound) * 100);
|
|
54
|
+
const exemption = exemptions.get(filePath);
|
|
55
|
+
const effectiveThreshold = exemption?.threshold ?? threshold;
|
|
56
|
+
if (pct >= effectiveThreshold)
|
|
57
|
+
continue;
|
|
58
|
+
const message = exemption
|
|
59
|
+
? `${pct}% line coverage (exemption: ${effectiveThreshold}%, reason: ${exemption.reason})`
|
|
60
|
+
: `${pct}% line coverage (threshold: ${effectiveThreshold}%)`;
|
|
61
|
+
findings.push(createFinding(rule, message, filePath, { code: 'coverage:below-threshold' }));
|
|
62
|
+
}
|
|
63
|
+
return { findings, fixes: [] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Parse an lcov tracefile into a map of source path → coverage counts. */
|
|
67
|
+
function parseLcov(raw) {
|
|
68
|
+
const result = new Map();
|
|
69
|
+
let file = null;
|
|
70
|
+
let linesFound = 0;
|
|
71
|
+
let linesHit = 0;
|
|
72
|
+
for (const line of raw.split('\n')) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (trimmed.startsWith('SF:')) {
|
|
75
|
+
file = trimmed.slice(3);
|
|
76
|
+
linesFound = 0;
|
|
77
|
+
linesHit = 0;
|
|
78
|
+
}
|
|
79
|
+
else if (trimmed.startsWith('LF:')) {
|
|
80
|
+
linesFound = Number(trimmed.slice(3));
|
|
81
|
+
}
|
|
82
|
+
else if (trimmed.startsWith('LH:')) {
|
|
83
|
+
linesHit = Number(trimmed.slice(3));
|
|
84
|
+
}
|
|
85
|
+
else if (trimmed === 'end_of_record' && file !== null) {
|
|
86
|
+
result.set(file, { linesFound, linesHit });
|
|
87
|
+
file = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
/** Standard exclusions applied regardless of config (tests, generated, deps). */
|
|
93
|
+
function isAlwaysExcluded(filePath) {
|
|
94
|
+
return (filePath.includes('node_modules') ||
|
|
95
|
+
filePath.includes('.test.') ||
|
|
96
|
+
filePath.includes('.spec.') ||
|
|
97
|
+
filePath.includes('__tests__'));
|
|
98
|
+
}
|
|
99
|
+
/** Normalize an lcov `SF:` path to a workdir-relative forward-slash path. */
|
|
100
|
+
function normalizeLcovSourcePath(workdir, filePath) {
|
|
101
|
+
const normalized = isAbsolute(filePath) ? relative(workdir, filePath) : filePath;
|
|
102
|
+
return normalized.replaceAll('\\', '/');
|
|
103
|
+
}
|
|
@@ -20,4 +20,12 @@ export declare function relativeToWorkdir(workdir: string, path: string): string
|
|
|
20
20
|
export declare function relativeParent(path: string): string;
|
|
21
21
|
/** Return true when a path matches any supplied fragment or suffix. */
|
|
22
22
|
export declare function matchesAny(path: string, patterns: string[] | undefined): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Segment-aware glob matching with `**` (any depth) and `*` (single segment).
|
|
25
|
+
*
|
|
26
|
+
* Stricter than {@link matchesAny}: it anchors the whole path, so `apps/**` does
|
|
27
|
+
* not match `vendor/apps/x`. Used by evaluators that enforce path policies
|
|
28
|
+
* (coverage scoping, test-file location) where a loose match would change findings.
|
|
29
|
+
*/
|
|
30
|
+
export declare function matchesGlob(path: string, pattern: string): boolean;
|
|
23
31
|
//# sourceMappingURL=file-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/evaluators/file-utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,cAAc,EAAW,MAAM,uBAAuB,CAAC;AAEjF,yCAAyC;AACzC,MAAM,WAAW,sBAAsB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,EAAE,CAAC,EAAE,UAAU,CAAC;CACnB;AAID,qFAAqF;AACrF,wBAAsB,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWtF;AAED,gDAAgD;AAChD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,iBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnH;AAED,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAMhF"}
|
|
1
|
+
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/evaluators/file-utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,cAAc,EAAW,MAAM,uBAAuB,CAAC;AAEjF,yCAAyC;AACzC,MAAM,WAAW,sBAAsB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,EAAE,CAAC,EAAE,UAAU,CAAC;CACnB;AAID,qFAAqF;AACrF,wBAAsB,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWtF;AAED,gDAAgD;AAChD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,iBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnH;AAED,sDAAsD;AACtD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAMhF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAUlE"}
|
|
@@ -33,3 +33,43 @@ export function matchesAny(path, patterns) {
|
|
|
33
33
|
return clean.length === 0 || path.includes(clean) || path.endsWith(clean);
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Segment-aware glob matching with `**` (any depth) and `*` (single segment).
|
|
38
|
+
*
|
|
39
|
+
* Stricter than {@link matchesAny}: it anchors the whole path, so `apps/**` does
|
|
40
|
+
* not match `vendor/apps/x`. Used by evaluators that enforce path policies
|
|
41
|
+
* (coverage scoping, test-file location) where a loose match would change findings.
|
|
42
|
+
*/
|
|
43
|
+
export function matchesGlob(path, pattern) {
|
|
44
|
+
const normalized = path.replaceAll('\\', '/');
|
|
45
|
+
if (pattern.startsWith('**/')) {
|
|
46
|
+
const suffix = pattern.slice(3);
|
|
47
|
+
if (suffix.indexOf('*') === -1) {
|
|
48
|
+
return normalized.endsWith(suffix) || normalized.endsWith(`/${suffix}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (pattern === normalized)
|
|
52
|
+
return true;
|
|
53
|
+
return matchSegments(normalized.split('/'), pattern.split('/'), 0, 0);
|
|
54
|
+
}
|
|
55
|
+
/** Recursive segment-level glob matcher backing {@link matchesGlob}. */
|
|
56
|
+
function matchSegments(file, pattern, fi, pi) {
|
|
57
|
+
if (pi >= pattern.length)
|
|
58
|
+
return fi >= file.length;
|
|
59
|
+
if (fi >= file.length)
|
|
60
|
+
return pattern.slice(pi).every((segment) => segment === '**');
|
|
61
|
+
const pat = pattern[pi] ?? '';
|
|
62
|
+
if (pat === '**') {
|
|
63
|
+
return matchSegments(file, pattern, fi, pi + 1) || matchSegments(file, pattern, fi + 1, pi);
|
|
64
|
+
}
|
|
65
|
+
if (!matchSegment(file[fi] ?? '', pat))
|
|
66
|
+
return false;
|
|
67
|
+
return matchSegments(file, pattern, fi + 1, pi + 1);
|
|
68
|
+
}
|
|
69
|
+
/** Match one path segment against a pattern segment where `*` matches any run of non-`/` chars. */
|
|
70
|
+
function matchSegment(segment, pattern) {
|
|
71
|
+
if (pattern.indexOf('*') === -1)
|
|
72
|
+
return segment === pattern;
|
|
73
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replaceAll('*', '[^/]*');
|
|
74
|
+
return new RegExp(`^${escaped}$`).test(segment);
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Evaluator that enforces where test files live and, optionally, that every
|
|
4
|
+
* source file has a corresponding test.
|
|
5
|
+
*
|
|
6
|
+
* Config (`evaluator.config`):
|
|
7
|
+
* - `expected`: glob the test files must match (required)
|
|
8
|
+
* - `forbid`: globs where tests must not live (e.g. `**\/__tests__/**`)
|
|
9
|
+
* - `requireCorrespondingTest`: when true, flags source files (from `rule.include`)
|
|
10
|
+
* that lack a test at the TypeScript-conventional path
|
|
11
|
+
*
|
|
12
|
+
* Discovery walks the workdir and applies `**` globs precisely, so it stays
|
|
13
|
+
* self-contained (no `rg --files` shell-out).
|
|
14
|
+
*/
|
|
15
|
+
export declare class TestLocationEvaluator implements RuleEvaluator {
|
|
16
|
+
/** Evaluate test-file placement and optional coverage of source files. */
|
|
17
|
+
evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=test-location-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-location-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/test-location-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAUlB;;;;;;;;;;;;GAYG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IACvD,0EAA0E;IACpE,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAiE5F"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createFinding, } from '../types.js';
|
|
2
|
+
import { discoverFiles, matchesGlob } from './file-utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Evaluator that enforces where test files live and, optionally, that every
|
|
5
|
+
* source file has a corresponding test.
|
|
6
|
+
*
|
|
7
|
+
* Config (`evaluator.config`):
|
|
8
|
+
* - `expected`: glob the test files must match (required)
|
|
9
|
+
* - `forbid`: globs where tests must not live (e.g. `**\/__tests__/**`)
|
|
10
|
+
* - `requireCorrespondingTest`: when true, flags source files (from `rule.include`)
|
|
11
|
+
* that lack a test at the TypeScript-conventional path
|
|
12
|
+
*
|
|
13
|
+
* Discovery walks the workdir and applies `**` globs precisely, so it stays
|
|
14
|
+
* self-contained (no `rg --files` shell-out).
|
|
15
|
+
*/
|
|
16
|
+
export class TestLocationEvaluator {
|
|
17
|
+
/** Evaluate test-file placement and optional coverage of source files. */
|
|
18
|
+
async evaluate(rule, context) {
|
|
19
|
+
const config = (rule.evaluator.config ?? {});
|
|
20
|
+
const expected = config.expected;
|
|
21
|
+
if (typeof expected !== 'string' || expected.length === 0) {
|
|
22
|
+
throw new Error('test-location evaluator requires a non-empty "expected" config');
|
|
23
|
+
}
|
|
24
|
+
const forbid = config.forbid ?? [];
|
|
25
|
+
const include = rule.include ?? ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx'];
|
|
26
|
+
const exclude = rule.exclude ?? [];
|
|
27
|
+
const allFiles = await discoverFiles({ workdir: context.workdir });
|
|
28
|
+
const findings = [];
|
|
29
|
+
const testPatterns = config.requireCorrespondingTest ? [expected] : include;
|
|
30
|
+
const testFiles = allFiles.filter((file) => testPatterns.some((pattern) => matchesGlob(file, pattern)));
|
|
31
|
+
for (const file of testFiles) {
|
|
32
|
+
if (exclude.some((pattern) => matchesGlob(file, pattern)))
|
|
33
|
+
continue;
|
|
34
|
+
const violated = forbid.find((pattern) => matchesGlob(file, pattern));
|
|
35
|
+
if (violated !== undefined) {
|
|
36
|
+
findings.push(createFinding(rule, `Test file "${file}" is in a forbidden location (matches "${violated}")`, file, {
|
|
37
|
+
code: 'test-location:forbidden',
|
|
38
|
+
}));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!matchesGlob(file, expected)) {
|
|
42
|
+
findings.push(createFinding(rule, `Test file "${file}" does not match expected pattern "${expected}"`, file, {
|
|
43
|
+
code: 'test-location:unexpected',
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (config.requireCorrespondingTest) {
|
|
48
|
+
const srcPatterns = rule.include ?? ['**/*.ts', '**/*.tsx'];
|
|
49
|
+
const testSet = new Set(testFiles);
|
|
50
|
+
for (const srcFile of allFiles) {
|
|
51
|
+
if (!srcPatterns.some((pattern) => matchesGlob(srcFile, pattern)))
|
|
52
|
+
continue;
|
|
53
|
+
if (exclude.some((pattern) => matchesGlob(srcFile, pattern)))
|
|
54
|
+
continue;
|
|
55
|
+
const testPath = resolveTestPath(srcFile);
|
|
56
|
+
if (testPath === srcFile)
|
|
57
|
+
continue;
|
|
58
|
+
if (!testSet.has(testPath)) {
|
|
59
|
+
findings.push(createFinding(rule, `Source file "${srcFile}" has no corresponding test → ${testPath}`, srcFile, {
|
|
60
|
+
code: 'test-location:missing',
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { findings, fixes: [] };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Map a TypeScript source path to its conventional test path.
|
|
70
|
+
*
|
|
71
|
+
* packages/core/src/foo/bar.ts → packages/core/tests/foo/bar.test.ts
|
|
72
|
+
* src/foo/bar.ts → tests/foo/bar.test.ts
|
|
73
|
+
*/
|
|
74
|
+
function resolveTestPath(srcRelPath) {
|
|
75
|
+
if (srcRelPath.includes('.test.') || srcRelPath.includes('.spec.'))
|
|
76
|
+
return srcRelPath;
|
|
77
|
+
const srcIdx = srcRelPath.indexOf('/src/');
|
|
78
|
+
if (srcIdx !== -1) {
|
|
79
|
+
const pkg = srcRelPath.slice(0, srcIdx);
|
|
80
|
+
const rel = srcRelPath.slice(srcIdx + '/src/'.length).replace(/\.(ts|tsx|js|jsx)$/, '.test.ts');
|
|
81
|
+
return `${pkg}/tests/${rel}`;
|
|
82
|
+
}
|
|
83
|
+
const withoutExt = srcRelPath.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
84
|
+
return `tests/${withoutExt.replace(/^src\//, '')}.test.ts`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ConstraintRule, type RuleContext, type RuleEvaluationResult, type RuleEvaluator } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Evaluator that flags exported declarations missing a JSDoc block.
|
|
4
|
+
*
|
|
5
|
+
* Self-contained: it scans matching source files line-by-line and checks whether
|
|
6
|
+
* the line immediately preceding an export ends a JSDoc comment. Config
|
|
7
|
+
* (`evaluator.config.kinds`) selects which export kinds to check; `rule.include`
|
|
8
|
+
* and `rule.exclude` scope the files using full `**` globs.
|
|
9
|
+
*/
|
|
10
|
+
export declare class TsdocExportEvaluator implements RuleEvaluator {
|
|
11
|
+
constructor();
|
|
12
|
+
/** Evaluate exports under the configured kinds for a preceding JSDoc block. */
|
|
13
|
+
evaluate(rule: ConstraintRule, context: RuleContext): Promise<RuleEvaluationResult>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=tsdoc-export-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsdoc-export-evaluator.d.ts","sourceRoot":"","sources":["../../src/evaluators/tsdoc-export-evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACrB,MAAM,UAAU,CAAC;AAwBlB;;;;;;;GAOG;AACH,qBAAa,oBAAqB,YAAW,aAAa;;IAItD,+EAA+E;IACzE,QAAQ,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CA8B5F"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createFinding, } from '../types.js';
|
|
2
|
+
import { discoverFiles, matchesGlob, readWorkdirFile } from './file-utils.js';
|
|
3
|
+
/** Export kinds this evaluator can check for a preceding JSDoc block. */
|
|
4
|
+
const VALID_KINDS = ['function', 'class', 'type', 'const', 'enum', 'interface'];
|
|
5
|
+
const KIND_PATTERN = {
|
|
6
|
+
function: /^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/,
|
|
7
|
+
class: /^export\s+(?:abstract\s+)?class\s+([A-Za-z0-9_$]+)/,
|
|
8
|
+
interface: /^export\s+interface\s+([A-Za-z0-9_$]+)/,
|
|
9
|
+
type: /^export\s+type\s+([A-Za-z0-9_$]+)/,
|
|
10
|
+
const: /^export\s+const\s+([A-Za-z0-9_$]+)/,
|
|
11
|
+
enum: /^export\s+(?:const\s+)?enum\s+([A-Za-z0-9_$]+)/,
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Evaluator that flags exported declarations missing a JSDoc block.
|
|
15
|
+
*
|
|
16
|
+
* Self-contained: it scans matching source files line-by-line and checks whether
|
|
17
|
+
* the line immediately preceding an export ends a JSDoc comment. Config
|
|
18
|
+
* (`evaluator.config.kinds`) selects which export kinds to check; `rule.include`
|
|
19
|
+
* and `rule.exclude` scope the files using full `**` globs.
|
|
20
|
+
*/
|
|
21
|
+
export class TsdocExportEvaluator {
|
|
22
|
+
constructor() {
|
|
23
|
+
// V8 function coverage requires explicit constructor
|
|
24
|
+
}
|
|
25
|
+
/** Evaluate exports under the configured kinds for a preceding JSDoc block. */
|
|
26
|
+
async evaluate(rule, context) {
|
|
27
|
+
const kinds = rule.evaluator.config?.kinds ?? [...VALID_KINDS];
|
|
28
|
+
for (const kind of kinds) {
|
|
29
|
+
if (!VALID_KINDS.includes(kind)) {
|
|
30
|
+
throw new Error(`Unknown export kind "${kind}"; expected one of: ${VALID_KINDS.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const requested = new Set(kinds);
|
|
34
|
+
const include = rule.include ?? ['**/*.ts', '**/*.tsx'];
|
|
35
|
+
const exclude = rule.exclude ?? [];
|
|
36
|
+
const files = await discoverFiles({ workdir: context.workdir, include: ['.ts', '.tsx'] });
|
|
37
|
+
const findings = [];
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (!include.some((pattern) => matchesGlob(file, pattern)))
|
|
40
|
+
continue;
|
|
41
|
+
if (exclude.some((pattern) => matchesGlob(file, pattern)))
|
|
42
|
+
continue;
|
|
43
|
+
const lines = (await readWorkdirFile(context.workdir, file)).split('\n');
|
|
44
|
+
for (const site of findExports(lines, requested)) {
|
|
45
|
+
if (!precededByJsdoc(lines, site.line)) {
|
|
46
|
+
findings.push(createFinding(rule, `Exported ${site.kind} "${site.name}" is missing a JSDoc comment`, file, {
|
|
47
|
+
line: site.line,
|
|
48
|
+
code: 'tsdoc:missing',
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { findings, fixes: [] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Find exported declarations of the requested kinds within a file's lines. */
|
|
57
|
+
function findExports(lines, requested) {
|
|
58
|
+
const sites = [];
|
|
59
|
+
for (const [index, raw] of lines.entries()) {
|
|
60
|
+
const line = raw.trimStart();
|
|
61
|
+
if (!line.startsWith('export'))
|
|
62
|
+
continue;
|
|
63
|
+
for (const kind of requested) {
|
|
64
|
+
const match = KIND_PATTERN[kind].exec(line);
|
|
65
|
+
if (match) {
|
|
66
|
+
sites.push({ kind, name: match[1] ?? 'unknown', line: index + 1 });
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return sites;
|
|
72
|
+
}
|
|
73
|
+
/** True when the line immediately above a declaration closes a JSDoc block. */
|
|
74
|
+
function precededByJsdoc(lines, declarationLine) {
|
|
75
|
+
const prev = lines[declarationLine - 2]?.trim();
|
|
76
|
+
return prev !== undefined && (prev.endsWith('*/') || prev.startsWith('/**'));
|
|
77
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/host/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/host/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAY7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,IAAI,CAgBvF"}
|
package/dist/host/builtins.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { AgentDetectionEvaluator } from '../evaluators/agent-detection-evaluator.js';
|
|
2
|
+
import { CoverageGateEvaluator } from '../evaluators/coverage-gate-evaluator.js';
|
|
2
3
|
import { ExitCodeEvaluator } from '../evaluators/exit-code-evaluator.js';
|
|
3
4
|
import { ForbiddenImportEvaluator } from '../evaluators/forbidden-import-evaluator.js';
|
|
4
5
|
import { PathEvaluator } from '../evaluators/path-evaluator.js';
|
|
5
6
|
import { RegexEvaluator } from '../evaluators/regex-evaluator.js';
|
|
6
7
|
import { SecretsScannerEvaluator } from '../evaluators/secrets-scanner-evaluator.js';
|
|
8
|
+
import { TestLocationEvaluator } from '../evaluators/test-location-evaluator.js';
|
|
9
|
+
import { TsdocExportEvaluator } from '../evaluators/tsdoc-export-evaluator.js';
|
|
7
10
|
import { JsonFormatter } from '../formatters/json.js';
|
|
8
11
|
import { TextFormatter } from '../formatters/text.js';
|
|
9
12
|
/** Register bundled evaluators and formatters on a host. */
|
|
@@ -18,6 +21,9 @@ export function registerBuiltins(host, executor) {
|
|
|
18
21
|
host.evaluators.register('exit-code', new ExitCodeEvaluator(executor), 'builtin');
|
|
19
22
|
host.evaluators.register('secrets-scanner', new SecretsScannerEvaluator(), 'builtin');
|
|
20
23
|
host.evaluators.register('agent-detection', new AgentDetectionEvaluator(), 'builtin');
|
|
24
|
+
host.evaluators.register('coverage-gate', new CoverageGateEvaluator(), 'builtin');
|
|
25
|
+
host.evaluators.register('tsdoc-export', new TsdocExportEvaluator(), 'builtin');
|
|
26
|
+
host.evaluators.register('test-location', new TestLocationEvaluator(), 'builtin');
|
|
21
27
|
host.formatters.register('text', new TextFormatter(), 'builtin');
|
|
22
28
|
host.formatters.register('json', new JsonFormatter(), 'builtin');
|
|
23
29
|
}
|