@poleski/quality-tools 0.1.4 → 0.2.0
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/CHANGELOG.md +6 -0
- package/README.md +4 -0
- package/dist/cli/main.js +249 -21
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ pnpm add -D link:/absolute/path/to/quality-tools
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
pnpm exec quality-tools organize .
|
|
27
|
+
pnpm exec quality-tools acceptance compile --spec "tests/acceptance/specs/**/*.md" --steps "tests/acceptance/steps.ts" --out "tests/playwright/generated/acceptance.spec.ts"
|
|
27
28
|
pnpm exec quality-tools boundaries . --strict
|
|
28
29
|
pnpm exec quality-tools reachability . --strict
|
|
29
30
|
pnpm exec quality-tools scrap ./tests
|
|
@@ -244,12 +245,15 @@ root, then falls back to the bundled base config.
|
|
|
244
245
|
## Other Tools
|
|
245
246
|
|
|
246
247
|
```bash
|
|
248
|
+
pnpm exec quality-tools acceptance compile --spec "tests/acceptance/specs/**/*.md" --steps "tests/acceptance/steps.ts" --out "tests/playwright/generated/acceptance.spec.ts"
|
|
247
249
|
pnpm exec quality-tools organize ./src
|
|
248
250
|
pnpm exec quality-tools boundaries parser --strict
|
|
249
251
|
pnpm exec quality-tools reachability parser --strict
|
|
250
252
|
pnpm exec quality-tools scrap ./tests --strict
|
|
251
253
|
```
|
|
252
254
|
|
|
255
|
+
- `acceptance` compiles human-authored Gherkin-ish Markdown specs into
|
|
256
|
+
executable Playwright specs that import host-owned step bindings.
|
|
253
257
|
- `organize` checks directory size, depth, naming, barrels, and cohesion. Use
|
|
254
258
|
`--write-baseline` and `--compare <path>` for baseline workflows.
|
|
255
259
|
- `boundaries` checks configured layers, entrypoints, dead surfaces, and dead
|
package/dist/cli/main.js
CHANGED
|
@@ -1,5 +1,154 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/acceptance/command.ts
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { glob } from "glob";
|
|
7
|
+
|
|
8
|
+
// src/acceptance/parser.ts
|
|
9
|
+
var FEATURE_PATTERN = /^#{0,6}\s*Feature:\s*(.+)$/;
|
|
10
|
+
var SCENARIO_PATTERN = /^#{0,6}\s*Scenario:\s*(.+)$/;
|
|
11
|
+
var STEP_PATTERN = /^(Given|When|Then|And|But)\s+(.+)$/;
|
|
12
|
+
function parseAcceptanceMarkdown(markdown, sourcePath) {
|
|
13
|
+
const lines = markdown.split(/\r?\n/);
|
|
14
|
+
let feature;
|
|
15
|
+
const scenarios = [];
|
|
16
|
+
lines.forEach((rawLine, index) => {
|
|
17
|
+
const lineNumber = index + 1;
|
|
18
|
+
const line = rawLine.trim();
|
|
19
|
+
if (line === "") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const featureMatch = FEATURE_PATTERN.exec(line);
|
|
23
|
+
if (featureMatch) {
|
|
24
|
+
feature = {
|
|
25
|
+
name: featureMatch[1].trim(),
|
|
26
|
+
line: lineNumber
|
|
27
|
+
};
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const scenarioMatch = SCENARIO_PATTERN.exec(line);
|
|
31
|
+
if (scenarioMatch) {
|
|
32
|
+
scenarios.push({
|
|
33
|
+
name: scenarioMatch[1].trim(),
|
|
34
|
+
line: lineNumber,
|
|
35
|
+
steps: []
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const stepMatch = STEP_PATTERN.exec(line);
|
|
40
|
+
if (stepMatch) {
|
|
41
|
+
const scenario = scenarios.at(-1);
|
|
42
|
+
if (!scenario) {
|
|
43
|
+
throw new Error(`${sourcePath}:${lineNumber} Step appears before a Scenario`);
|
|
44
|
+
}
|
|
45
|
+
scenario.steps.push({
|
|
46
|
+
keyword: stepMatch[1],
|
|
47
|
+
text: stepMatch[2].trim(),
|
|
48
|
+
line: lineNumber
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (!feature) {
|
|
53
|
+
throw new Error(`${sourcePath}: Expected a Feature heading`);
|
|
54
|
+
}
|
|
55
|
+
if (scenarios.length === 0) {
|
|
56
|
+
throw new Error(`${sourcePath}: Expected at least one Scenario`);
|
|
57
|
+
}
|
|
58
|
+
const emptyScenario = scenarios.find((scenario) => scenario.steps.length === 0);
|
|
59
|
+
if (emptyScenario) {
|
|
60
|
+
throw new Error(`${sourcePath}:${emptyScenario.line} Scenario "${emptyScenario.name}" must contain at least one step`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
sourcePath,
|
|
64
|
+
feature,
|
|
65
|
+
scenarios
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/acceptance/playwright/generator.ts
|
|
70
|
+
function generatePlaywrightAcceptanceSpec(documents, options) {
|
|
71
|
+
const sections = documents.flatMap((document) => generateDocumentSections(document));
|
|
72
|
+
return [
|
|
73
|
+
"/* Generated by quality-tools acceptance compile. Do not edit. */",
|
|
74
|
+
"/* eslint-disable playwright/expect-expect */",
|
|
75
|
+
"import { test } from '@playwright/test';",
|
|
76
|
+
`import { acceptanceSteps, createAcceptanceContext } from ${quote(options.stepsImportPath)};`,
|
|
77
|
+
"",
|
|
78
|
+
"type AcceptanceContext = Awaited<ReturnType<typeof createAcceptanceContext>> & { cleanup?: () => unknown | Promise<unknown> };",
|
|
79
|
+
"type AcceptanceRuntimeStep = { keyword: string; text: string; sourcePath: string; line: number };",
|
|
80
|
+
"type AcceptanceStepImplementation = (context: AcceptanceContext, step: AcceptanceRuntimeStep) => unknown | Promise<unknown>;",
|
|
81
|
+
"type AcceptanceStepRegistry = Record<string, AcceptanceStepImplementation>;",
|
|
82
|
+
"",
|
|
83
|
+
"async function runAcceptanceStep(",
|
|
84
|
+
" context: AcceptanceContext,",
|
|
85
|
+
" stepText: string,",
|
|
86
|
+
" step: AcceptanceRuntimeStep",
|
|
87
|
+
"): Promise<void> {",
|
|
88
|
+
" const registry = acceptanceSteps as AcceptanceStepRegistry;",
|
|
89
|
+
" const implementation = registry[stepText] ?? registry[`${step.keyword} ${stepText}`];",
|
|
90
|
+
"",
|
|
91
|
+
" if (!implementation) {",
|
|
92
|
+
' throw new Error(`Missing acceptance step "${step.keyword} ${step.text}" at ${step.sourcePath}:${step.line}`);',
|
|
93
|
+
" }",
|
|
94
|
+
"",
|
|
95
|
+
" await implementation(context, step);",
|
|
96
|
+
"}",
|
|
97
|
+
"",
|
|
98
|
+
...sections,
|
|
99
|
+
""
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
function generateDocumentSections(document) {
|
|
103
|
+
const scenarios = document.scenarios.flatMap((scenario) => generateScenario(document.sourcePath, scenario));
|
|
104
|
+
return [
|
|
105
|
+
`test.describe(${quote(document.feature.name)}, () => {`,
|
|
106
|
+
...indentLines(scenarios, 2),
|
|
107
|
+
"});",
|
|
108
|
+
""
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
function generateScenario(sourcePath, scenario) {
|
|
112
|
+
const steps = indentLines(scenario.steps.flatMap((step) => generateStep(sourcePath, step)), 4);
|
|
113
|
+
return [
|
|
114
|
+
`test(${quote(scenario.name)}, async ({}, testInfo) => {`,
|
|
115
|
+
" const context = await createAcceptanceContext({",
|
|
116
|
+
" testInfo,",
|
|
117
|
+
` sourcePath: ${quote(sourcePath)},`,
|
|
118
|
+
` scenario: ${quote(scenario.name)}`,
|
|
119
|
+
" });",
|
|
120
|
+
"",
|
|
121
|
+
" try {",
|
|
122
|
+
...steps,
|
|
123
|
+
" } finally {",
|
|
124
|
+
" await context.cleanup?.();",
|
|
125
|
+
" }",
|
|
126
|
+
"});"
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
function generateStep(sourcePath, step) {
|
|
130
|
+
const label = `${step.keyword} ${step.text}`;
|
|
131
|
+
return [
|
|
132
|
+
`// ${sourcePath}:${step.line}`,
|
|
133
|
+
`await test.step(${quote(label)}, async () => {`,
|
|
134
|
+
` await runAcceptanceStep(context, ${quote(step.text)}, {`,
|
|
135
|
+
` keyword: ${quote(step.keyword)},`,
|
|
136
|
+
` text: ${quote(step.text)},`,
|
|
137
|
+
` sourcePath: ${quote(sourcePath)},`,
|
|
138
|
+
` line: ${step.line}`,
|
|
139
|
+
" });",
|
|
140
|
+
"});",
|
|
141
|
+
""
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
function quote(value) {
|
|
145
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n")}'`;
|
|
146
|
+
}
|
|
147
|
+
function indentLines(lines, spaces) {
|
|
148
|
+
const prefix = " ".repeat(spaces);
|
|
149
|
+
return lines.map((line) => line === "" ? line : `${prefix}${line}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
3
152
|
// src/shared/flagValue.ts
|
|
4
153
|
function flagValue(args2, name) {
|
|
5
154
|
const inlineFlag = args2.find((arg) => arg.startsWith(`${name}=`));
|
|
@@ -31,6 +180,83 @@ function cleanCliArgs(args2) {
|
|
|
31
180
|
return args2.filter((arg) => arg !== "--");
|
|
32
181
|
}
|
|
33
182
|
|
|
183
|
+
// src/acceptance/command.ts
|
|
184
|
+
async function runAcceptanceCli(rawArgs, options = {}) {
|
|
185
|
+
const args2 = cleanCliArgs(rawArgs);
|
|
186
|
+
const [command2, ...commandArgs] = args2;
|
|
187
|
+
if (command2 !== "compile") {
|
|
188
|
+
throw new Error("Usage: quality-tools acceptance compile --spec <glob> --steps <path> --out <path>");
|
|
189
|
+
}
|
|
190
|
+
await compileAcceptance(commandArgs, options.cwd ?? process.cwd());
|
|
191
|
+
}
|
|
192
|
+
async function compileAcceptance(args2, cwd) {
|
|
193
|
+
const options = parseCompileOptions(args2);
|
|
194
|
+
const specFiles = await findSpecFiles(cwd, options.specPatterns);
|
|
195
|
+
if (specFiles.length === 0) {
|
|
196
|
+
throw new Error(`No acceptance specs matched: ${options.specPatterns.join(", ")}`);
|
|
197
|
+
}
|
|
198
|
+
const documents = specFiles.map((specFile) => {
|
|
199
|
+
const source = fs.readFileSync(specFile, "utf8");
|
|
200
|
+
return parseAcceptanceMarkdown(source, toPosixPath(path.relative(cwd, specFile)));
|
|
201
|
+
});
|
|
202
|
+
const outPath = path.resolve(cwd, options.outPath);
|
|
203
|
+
const stepsImportPath = createStepsImportPath(outPath, path.resolve(cwd, options.stepsPath));
|
|
204
|
+
const generated = generatePlaywrightAcceptanceSpec(documents, { stepsImportPath });
|
|
205
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
206
|
+
fs.writeFileSync(outPath, generated);
|
|
207
|
+
}
|
|
208
|
+
function parseCompileOptions(args2) {
|
|
209
|
+
const specPatterns = collectFlagValues(args2, "--spec");
|
|
210
|
+
const stepsPath = requireFlagValue(args2, "--steps");
|
|
211
|
+
const outPath = requireFlagValue(args2, "--out");
|
|
212
|
+
if (specPatterns.length === 0) {
|
|
213
|
+
throw new Error("Missing required --spec <glob>");
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
specPatterns,
|
|
217
|
+
stepsPath,
|
|
218
|
+
outPath
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function findSpecFiles(cwd, patterns) {
|
|
222
|
+
const files = await Promise.all(
|
|
223
|
+
patterns.map((pattern) => glob(pattern, { absolute: true, cwd, nodir: true }))
|
|
224
|
+
);
|
|
225
|
+
return files.flat().sort((left, right) => left.localeCompare(right));
|
|
226
|
+
}
|
|
227
|
+
function createStepsImportPath(outPath, stepsPath) {
|
|
228
|
+
const relativePath = toPosixPath(path.relative(path.dirname(outPath), stepsPath));
|
|
229
|
+
const extension = path.extname(relativePath);
|
|
230
|
+
const extensionlessPath = extension ? relativePath.slice(0, -extension.length) : relativePath;
|
|
231
|
+
if (extensionlessPath.startsWith(".")) {
|
|
232
|
+
return extensionlessPath;
|
|
233
|
+
}
|
|
234
|
+
return `./${extensionlessPath}`;
|
|
235
|
+
}
|
|
236
|
+
function collectFlagValues(args2, flag) {
|
|
237
|
+
const values = [];
|
|
238
|
+
args2.forEach((arg, index) => {
|
|
239
|
+
if (arg === flag) {
|
|
240
|
+
const value = args2[index + 1];
|
|
241
|
+
if (!value || value.startsWith("--")) {
|
|
242
|
+
throw new Error(`Missing value for ${flag}`);
|
|
243
|
+
}
|
|
244
|
+
values.push(value);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return values;
|
|
248
|
+
}
|
|
249
|
+
function requireFlagValue(args2, flag) {
|
|
250
|
+
const value = collectFlagValues(args2, flag).at(0);
|
|
251
|
+
if (!value) {
|
|
252
|
+
throw new Error(`Missing required ${flag} <path>`);
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
function toPosixPath(value) {
|
|
257
|
+
return value.split(path.sep).join(path.posix.sep);
|
|
258
|
+
}
|
|
259
|
+
|
|
34
260
|
// src/shared/resolve/repoRoot.ts
|
|
35
261
|
import { resolve as resolve3 } from "node:path";
|
|
36
262
|
|
|
@@ -872,7 +1098,7 @@ function runBoundariesCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES) {
|
|
|
872
1098
|
|
|
873
1099
|
// src/crap/analysis/run.ts
|
|
874
1100
|
import { existsSync as existsSync6 } from "fs";
|
|
875
|
-
import * as
|
|
1101
|
+
import * as path3 from "path";
|
|
876
1102
|
|
|
877
1103
|
// src/crap/analysis/calculate.ts
|
|
878
1104
|
function calculateCrap(complexity, coverage) {
|
|
@@ -998,7 +1224,7 @@ function extractFunctions(sourceFile) {
|
|
|
998
1224
|
|
|
999
1225
|
// src/crap/analysis/fileSelection.ts
|
|
1000
1226
|
import { readFileSync as readFileSync4 } from "fs";
|
|
1001
|
-
import * as
|
|
1227
|
+
import * as path2 from "path";
|
|
1002
1228
|
import * as ts7 from "typescript";
|
|
1003
1229
|
function matchesFilterScope(relativePath, filterScope) {
|
|
1004
1230
|
if (!filterScope) {
|
|
@@ -1010,7 +1236,7 @@ function matchesFilterScope(relativePath, filterScope) {
|
|
|
1010
1236
|
return relativePath.startsWith(`${filterScope}/`);
|
|
1011
1237
|
}
|
|
1012
1238
|
function shouldIncludeFile(filePath, filterScope, repoRoot) {
|
|
1013
|
-
const relativePath = toPosix(
|
|
1239
|
+
const relativePath = toPosix(path2.relative(repoRoot, filePath));
|
|
1014
1240
|
if (!matchesFilterScope(relativePath, filterScope)) {
|
|
1015
1241
|
return false;
|
|
1016
1242
|
}
|
|
@@ -1022,7 +1248,7 @@ function shouldIncludeFile(filePath, filterScope, repoRoot) {
|
|
|
1022
1248
|
repoRoot,
|
|
1023
1249
|
workspacePackage.name,
|
|
1024
1250
|
"crap",
|
|
1025
|
-
toPosix(
|
|
1251
|
+
toPosix(path2.relative(workspacePackage.root, filePath))
|
|
1026
1252
|
);
|
|
1027
1253
|
}
|
|
1028
1254
|
function createSourceFile3(filePath) {
|
|
@@ -1058,7 +1284,7 @@ function analyzeCoverageEntry(filePath, fileCoverage, repoRoot, threshold) {
|
|
|
1058
1284
|
complexity: fn.complexity,
|
|
1059
1285
|
coverage: Math.round(coverage),
|
|
1060
1286
|
crap: Math.round(crap * 100) / 100,
|
|
1061
|
-
file: toPosix(
|
|
1287
|
+
file: toPosix(path3.relative(repoRoot, fn.file)),
|
|
1062
1288
|
line: fn.line,
|
|
1063
1289
|
name: fn.name
|
|
1064
1290
|
};
|
|
@@ -1181,11 +1407,11 @@ function createCoverageProfiles(repoRoot, target) {
|
|
|
1181
1407
|
|
|
1182
1408
|
// src/crap/coverage/read.ts
|
|
1183
1409
|
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1184
|
-
function readCoverageReport(
|
|
1185
|
-
if (!existsSync7(
|
|
1186
|
-
throw new Error(`Coverage data not found: ${
|
|
1410
|
+
function readCoverageReport(path4) {
|
|
1411
|
+
if (!existsSync7(path4)) {
|
|
1412
|
+
throw new Error(`Coverage data not found: ${path4}`);
|
|
1187
1413
|
}
|
|
1188
|
-
return JSON.parse(readFileSync5(
|
|
1414
|
+
return JSON.parse(readFileSync5(path4, "utf-8"));
|
|
1189
1415
|
}
|
|
1190
1416
|
|
|
1191
1417
|
// src/crap/report.ts
|
|
@@ -1665,7 +1891,7 @@ function parseBareMutationTargetArg(args2) {
|
|
|
1665
1891
|
}
|
|
1666
1892
|
return void 0;
|
|
1667
1893
|
}
|
|
1668
|
-
function
|
|
1894
|
+
function collectFlagValues2(args2, name) {
|
|
1669
1895
|
const values = [];
|
|
1670
1896
|
for (let index = 0; index < args2.length; index += 1) {
|
|
1671
1897
|
const arg = args2[index];
|
|
@@ -1692,11 +1918,11 @@ function parseJsonStringArray(value, flagName) {
|
|
|
1692
1918
|
}
|
|
1693
1919
|
function mutationRunOptions(args2) {
|
|
1694
1920
|
const mutateGlobs = [
|
|
1695
|
-
...
|
|
1921
|
+
...collectFlagValues2(args2, "--mutate-glob"),
|
|
1696
1922
|
...parseJsonStringArray(flagValue(args2, "--mutate-globs-json"), "--mutate-globs-json")
|
|
1697
1923
|
];
|
|
1698
1924
|
const testIncludes = [
|
|
1699
|
-
...
|
|
1925
|
+
...collectFlagValues2(args2, "--test-include"),
|
|
1700
1926
|
...parseJsonStringArray(flagValue(args2, "--test-includes-json"), "--test-includes-json")
|
|
1701
1927
|
];
|
|
1702
1928
|
return {
|
|
@@ -2754,7 +2980,7 @@ function runReachabilityCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES4) {
|
|
|
2754
2980
|
}
|
|
2755
2981
|
|
|
2756
2982
|
// src/scrap/analysis/pipeline/run.ts
|
|
2757
|
-
import * as
|
|
2983
|
+
import * as fs2 from "fs";
|
|
2758
2984
|
import * as ts25 from "typescript";
|
|
2759
2985
|
|
|
2760
2986
|
// src/scrap/test/discovery/files.ts
|
|
@@ -4100,8 +4326,8 @@ function compareBlockSummaries(left, right) {
|
|
|
4100
4326
|
|
|
4101
4327
|
// src/scrap/structure/blocks/groups.ts
|
|
4102
4328
|
var BLOCK_SEPARATOR = "";
|
|
4103
|
-
function blockPathKey(
|
|
4104
|
-
return
|
|
4329
|
+
function blockPathKey(path4) {
|
|
4330
|
+
return path4.join(BLOCK_SEPARATOR);
|
|
4105
4331
|
}
|
|
4106
4332
|
function blockPathFromKey(key) {
|
|
4107
4333
|
return key.split(BLOCK_SEPARATOR);
|
|
@@ -4127,7 +4353,7 @@ function averageScore2(examples) {
|
|
|
4127
4353
|
function countExamples2(examples, predicate) {
|
|
4128
4354
|
return examples.filter(predicate).length;
|
|
4129
4355
|
}
|
|
4130
|
-
function summarizeBlock(
|
|
4356
|
+
function summarizeBlock(path4, examples) {
|
|
4131
4357
|
const meanScore = averageScore2(examples);
|
|
4132
4358
|
const maxScore2 = examples.reduce((max, example) => Math.max(max, example.score), 0);
|
|
4133
4359
|
const hotExampleCount2 = countExamples2(examples, (example) => example.score >= 8);
|
|
@@ -4142,8 +4368,8 @@ function summarizeBlock(path3, examples) {
|
|
|
4142
4368
|
hotExampleCount: hotExampleCount2,
|
|
4143
4369
|
lowAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount <= 1),
|
|
4144
4370
|
maxScore: maxScore2,
|
|
4145
|
-
name:
|
|
4146
|
-
path:
|
|
4371
|
+
name: path4[path4.length - 1],
|
|
4372
|
+
path: path4,
|
|
4147
4373
|
remediationMode: remediationMode(examples.length, meanScore, hotExampleCount2, maxScore2),
|
|
4148
4374
|
zeroAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount === 0)
|
|
4149
4375
|
};
|
|
@@ -4521,7 +4747,7 @@ function analyzeScrapFile(sourceFile) {
|
|
|
4521
4747
|
// src/scrap/analysis/pipeline/run.ts
|
|
4522
4748
|
function analyzeScrap(target) {
|
|
4523
4749
|
return discoverTestFiles(target).map((filePath) => {
|
|
4524
|
-
const source =
|
|
4750
|
+
const source = fs2.readFileSync(filePath, "utf-8");
|
|
4525
4751
|
const sourceFile = ts25.createSourceFile(
|
|
4526
4752
|
filePath,
|
|
4527
4753
|
source,
|
|
@@ -4677,8 +4903,8 @@ function resolveScrapPolicy(args2) {
|
|
|
4677
4903
|
}
|
|
4678
4904
|
|
|
4679
4905
|
// src/scrap/report/blocks/format.ts
|
|
4680
|
-
function formatBlockPath(
|
|
4681
|
-
return
|
|
4906
|
+
function formatBlockPath(path4) {
|
|
4907
|
+
return path4.join(" > ");
|
|
4682
4908
|
}
|
|
4683
4909
|
function interestingBlocks(metric) {
|
|
4684
4910
|
return metric.blockSummaries.filter((block) => block.remediationMode !== "STABLE").slice(0, 5);
|
|
@@ -4896,6 +5122,7 @@ function runScrapCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES5) {
|
|
|
4896
5122
|
|
|
4897
5123
|
// src/cli/main.ts
|
|
4898
5124
|
var COMMANDS = {
|
|
5125
|
+
acceptance: runAcceptanceCli,
|
|
4899
5126
|
boundaries: runBoundariesCli,
|
|
4900
5127
|
crap: runCrapCli,
|
|
4901
5128
|
init: runInitCli,
|
|
@@ -4909,6 +5136,7 @@ function printHelp() {
|
|
|
4909
5136
|
|
|
4910
5137
|
Commands:
|
|
4911
5138
|
init Create a starter quality.config.json
|
|
5139
|
+
acceptance Compile human-authored acceptance specs into executable tests
|
|
4912
5140
|
organize Check folder structure, naming, and cohesion
|
|
4913
5141
|
boundaries Check package/layer boundaries
|
|
4914
5142
|
reachability Check dead surfaces and dead ends
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poleski/quality-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Portable TypeScript quality checks for project structure, complexity, mutation, and test health",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"changeset": "changeset",
|
|
48
48
|
"cli": "tsx src/cli/main.ts",
|
|
49
49
|
"quality-tools": "pnpm run cli",
|
|
50
|
+
"acceptance": "tsx src/cli/acceptance.ts",
|
|
50
51
|
"boundaries": "tsx src/cli/boundaries.ts",
|
|
51
52
|
"reachability": "tsx src/cli/reachability.ts",
|
|
52
53
|
"crap": "tsx src/cli/crap.ts",
|