@poleski/quality-tools 0.1.3 → 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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @poleski/quality-tools
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5bab67a: Add an acceptance spec compiler that turns Gherkin-ish Markdown into Playwright tests.
8
+
9
+ ## 0.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 289c605: Stop host projects from seeing Stryker warnings about the upstream Vitest runner plugin when using the bundled mutation config, and throttle Stryker mutation progress into the existing one-minute heartbeat.
14
+
3
15
  ## 0.1.3
4
16
 
5
17
  ### Patch Changes
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 path2 from "path";
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 path from "path";
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(path.relative(repoRoot, filePath));
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(path.relative(workspacePackage.root, filePath))
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(path2.relative(repoRoot, fn.file)),
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(path3) {
1185
- if (!existsSync7(path3)) {
1186
- throw new Error(`Coverage data not found: ${path3}`);
1410
+ function readCoverageReport(path4) {
1411
+ if (!existsSync7(path4)) {
1412
+ throw new Error(`Coverage data not found: ${path4}`);
1187
1413
  }
1188
- return JSON.parse(readFileSync5(path3, "utf-8"));
1414
+ return JSON.parse(readFileSync5(path4, "utf-8"));
1189
1415
  }
1190
1416
 
1191
1417
  // src/crap/report.ts
@@ -1472,6 +1698,99 @@ function buildMutationEnv(options = {}) {
1472
1698
  };
1473
1699
  }
1474
1700
 
1701
+ // src/mutation/runner/progress.ts
1702
+ var ANSI_PATTERN = new RegExp(
1703
+ `${String.fromCharCode(27)}(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])`,
1704
+ "g"
1705
+ );
1706
+ var PROGRESS_PATTERN = /Mutation testing\s+(?:\[(?<bracketStatus>[^\]]*)\]\s*)?(?<percent>\d+%)\s+\((?<timing>elapsed:[^)]+)\)\s+(?<count>\d+\/\d+)\s+(?:Mutants?|tested)(?:\s+\((?<tailStatus>\d+\s+survived,\s*\d+\s+timed out)\))?/i;
1707
+ var STATUS_TAIL_PATTERN = /(?:^|\s)tested\s+\((?<status>\d+\s+survived,\s*\d+\s+timed out)\)\s*$/i;
1708
+ function cleanProgressText(text) {
1709
+ return text.replace(ANSI_PATTERN, "").trim();
1710
+ }
1711
+ function normalizeStatus(status) {
1712
+ const trimmed = status?.trim();
1713
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
1714
+ }
1715
+ var MutationProgressTracker = class {
1716
+ latest;
1717
+ formatLatest() {
1718
+ if (!this.latest) {
1719
+ return void 0;
1720
+ }
1721
+ return [
1722
+ "Mutation testing",
1723
+ `[${this.latest.status ?? ""}]`,
1724
+ this.latest.percent,
1725
+ `(${this.latest.timing})`,
1726
+ this.latest.count,
1727
+ "Mutants"
1728
+ ].join(" ");
1729
+ }
1730
+ observe(text) {
1731
+ const cleanText = cleanProgressText(text);
1732
+ if (cleanText.length === 0) {
1733
+ return false;
1734
+ }
1735
+ const progressMatch = PROGRESS_PATTERN.exec(cleanText);
1736
+ if (progressMatch?.groups) {
1737
+ this.latest = {
1738
+ count: progressMatch.groups.count,
1739
+ percent: progressMatch.groups.percent,
1740
+ timing: progressMatch.groups.timing,
1741
+ status: normalizeStatus(progressMatch.groups.tailStatus) ?? normalizeStatus(progressMatch.groups.bracketStatus) ?? this.latest?.status
1742
+ };
1743
+ return true;
1744
+ }
1745
+ const statusMatch = STATUS_TAIL_PATTERN.exec(cleanText);
1746
+ if (statusMatch?.groups && this.latest) {
1747
+ this.latest = {
1748
+ ...this.latest,
1749
+ status: normalizeStatus(statusMatch.groups.status) ?? this.latest.status
1750
+ };
1751
+ return true;
1752
+ }
1753
+ return false;
1754
+ }
1755
+ };
1756
+ function createMutationProgressOutputForwarder(tracker, writeOutput) {
1757
+ let pending = "";
1758
+ const handleSegment = (segment, delimiter) => {
1759
+ if (tracker.observe(segment)) {
1760
+ return;
1761
+ }
1762
+ writeOutput(`${segment}${delimiter}`);
1763
+ };
1764
+ const flushPendingProgress = () => {
1765
+ if (tracker.observe(pending)) {
1766
+ pending = "";
1767
+ return true;
1768
+ }
1769
+ return false;
1770
+ };
1771
+ return {
1772
+ flush() {
1773
+ if (pending.length === 0 || flushPendingProgress()) {
1774
+ return;
1775
+ }
1776
+ writeOutput(pending);
1777
+ pending = "";
1778
+ },
1779
+ write(text) {
1780
+ pending += text;
1781
+ let delimiterIndex = pending.search(/[\r\n]/);
1782
+ while (delimiterIndex >= 0) {
1783
+ const segment = pending.slice(0, delimiterIndex);
1784
+ const delimiter = pending[delimiterIndex] === "\n" ? "\n" : "";
1785
+ pending = pending.slice(delimiterIndex + 1);
1786
+ handleSegment(segment, delimiter);
1787
+ delimiterIndex = pending.search(/[\r\n]/);
1788
+ }
1789
+ flushPendingProgress();
1790
+ }
1791
+ };
1792
+ }
1793
+
1475
1794
  // src/mutation/runner/run.ts
1476
1795
  var MUTATION_PROGRESS_INTERVAL_MS = 6e4;
1477
1796
  function formatElapsedDuration(durationMs) {
@@ -1483,18 +1802,35 @@ function formatElapsedDuration(durationMs) {
1483
1802
  function runStryker(args2, env, target) {
1484
1803
  return new Promise((resolve8, reject) => {
1485
1804
  const startedAt = Date.now();
1805
+ const progressTracker = new MutationProgressTracker();
1806
+ const stdoutForwarder = createMutationProgressOutputForwarder(
1807
+ progressTracker,
1808
+ (text) => process.stdout.write(text)
1809
+ );
1810
+ const stderrForwarder = createMutationProgressOutputForwarder(
1811
+ progressTracker,
1812
+ (text) => process.stderr.write(text)
1813
+ );
1486
1814
  const child = spawn(process.execPath, [strykerBinPath(), ...args2], {
1487
1815
  cwd: REPO_ROOT,
1488
1816
  env,
1489
- stdio: "inherit"
1817
+ stdio: ["inherit", "pipe", "pipe"]
1818
+ });
1819
+ child.stdout?.on("data", (chunk) => {
1820
+ stdoutForwarder.write(String(chunk));
1821
+ });
1822
+ child.stderr?.on("data", (chunk) => {
1823
+ stderrForwarder.write(String(chunk));
1490
1824
  });
1491
1825
  const progressTimer = setInterval(() => {
1492
1826
  console.error(
1493
- `[mutation] Still running ${target.relativePath} after ${formatElapsedDuration(Date.now() - startedAt)}...`
1827
+ progressTracker.formatLatest() ?? `[mutation] Still running ${target.relativePath} after ${formatElapsedDuration(Date.now() - startedAt)}...`
1494
1828
  );
1495
1829
  }, MUTATION_PROGRESS_INTERVAL_MS);
1496
1830
  const clearProgressTimer = () => {
1497
1831
  clearInterval(progressTimer);
1832
+ stdoutForwarder.flush();
1833
+ stderrForwarder.flush();
1498
1834
  };
1499
1835
  child.once("error", (error) => {
1500
1836
  clearProgressTimer();
@@ -1555,7 +1891,7 @@ function parseBareMutationTargetArg(args2) {
1555
1891
  }
1556
1892
  return void 0;
1557
1893
  }
1558
- function collectFlagValues(args2, name) {
1894
+ function collectFlagValues2(args2, name) {
1559
1895
  const values = [];
1560
1896
  for (let index = 0; index < args2.length; index += 1) {
1561
1897
  const arg = args2[index];
@@ -1582,11 +1918,11 @@ function parseJsonStringArray(value, flagName) {
1582
1918
  }
1583
1919
  function mutationRunOptions(args2) {
1584
1920
  const mutateGlobs = [
1585
- ...collectFlagValues(args2, "--mutate-glob"),
1921
+ ...collectFlagValues2(args2, "--mutate-glob"),
1586
1922
  ...parseJsonStringArray(flagValue(args2, "--mutate-globs-json"), "--mutate-globs-json")
1587
1923
  ];
1588
1924
  const testIncludes = [
1589
- ...collectFlagValues(args2, "--test-include"),
1925
+ ...collectFlagValues2(args2, "--test-include"),
1590
1926
  ...parseJsonStringArray(flagValue(args2, "--test-includes-json"), "--test-includes-json")
1591
1927
  ];
1592
1928
  return {
@@ -2644,7 +2980,7 @@ function runReachabilityCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES4) {
2644
2980
  }
2645
2981
 
2646
2982
  // src/scrap/analysis/pipeline/run.ts
2647
- import * as fs from "fs";
2983
+ import * as fs2 from "fs";
2648
2984
  import * as ts25 from "typescript";
2649
2985
 
2650
2986
  // src/scrap/test/discovery/files.ts
@@ -3990,8 +4326,8 @@ function compareBlockSummaries(left, right) {
3990
4326
 
3991
4327
  // src/scrap/structure/blocks/groups.ts
3992
4328
  var BLOCK_SEPARATOR = "";
3993
- function blockPathKey(path3) {
3994
- return path3.join(BLOCK_SEPARATOR);
4329
+ function blockPathKey(path4) {
4330
+ return path4.join(BLOCK_SEPARATOR);
3995
4331
  }
3996
4332
  function blockPathFromKey(key) {
3997
4333
  return key.split(BLOCK_SEPARATOR);
@@ -4017,7 +4353,7 @@ function averageScore2(examples) {
4017
4353
  function countExamples2(examples, predicate) {
4018
4354
  return examples.filter(predicate).length;
4019
4355
  }
4020
- function summarizeBlock(path3, examples) {
4356
+ function summarizeBlock(path4, examples) {
4021
4357
  const meanScore = averageScore2(examples);
4022
4358
  const maxScore2 = examples.reduce((max, example) => Math.max(max, example.score), 0);
4023
4359
  const hotExampleCount2 = countExamples2(examples, (example) => example.score >= 8);
@@ -4032,8 +4368,8 @@ function summarizeBlock(path3, examples) {
4032
4368
  hotExampleCount: hotExampleCount2,
4033
4369
  lowAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount <= 1),
4034
4370
  maxScore: maxScore2,
4035
- name: path3[path3.length - 1],
4036
- path: path3,
4371
+ name: path4[path4.length - 1],
4372
+ path: path4,
4037
4373
  remediationMode: remediationMode(examples.length, meanScore, hotExampleCount2, maxScore2),
4038
4374
  zeroAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount === 0)
4039
4375
  };
@@ -4411,7 +4747,7 @@ function analyzeScrapFile(sourceFile) {
4411
4747
  // src/scrap/analysis/pipeline/run.ts
4412
4748
  function analyzeScrap(target) {
4413
4749
  return discoverTestFiles(target).map((filePath) => {
4414
- const source = fs.readFileSync(filePath, "utf-8");
4750
+ const source = fs2.readFileSync(filePath, "utf-8");
4415
4751
  const sourceFile = ts25.createSourceFile(
4416
4752
  filePath,
4417
4753
  source,
@@ -4567,8 +4903,8 @@ function resolveScrapPolicy(args2) {
4567
4903
  }
4568
4904
 
4569
4905
  // src/scrap/report/blocks/format.ts
4570
- function formatBlockPath(path3) {
4571
- return path3.join(" > ");
4906
+ function formatBlockPath(path4) {
4907
+ return path4.join(" > ");
4572
4908
  }
4573
4909
  function interestingBlocks(metric) {
4574
4910
  return metric.blockSummaries.filter((block) => block.remediationMode !== "STABLE").slice(0, 5);
@@ -4786,6 +5122,7 @@ function runScrapCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES5) {
4786
5122
 
4787
5123
  // src/cli/main.ts
4788
5124
  var COMMANDS = {
5125
+ acceptance: runAcceptanceCli,
4789
5126
  boundaries: runBoundariesCli,
4790
5127
  crap: runCrapCli,
4791
5128
  init: runInitCli,
@@ -4799,6 +5136,7 @@ function printHelp() {
4799
5136
 
4800
5137
  Commands:
4801
5138
  init Create a starter quality.config.json
5139
+ acceptance Compile human-authored acceptance specs into executable tests
4802
5140
  organize Check folder structure, naming, and cohesion
4803
5141
  boundaries Check package/layer boundaries
4804
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.1.3",
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",
@@ -43,10 +43,11 @@
43
43
  "scripts": {
44
44
  "build": "esbuild src/cli/main.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/cli/main.js",
45
45
  "ci": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
46
- "release": "pnpm run ci && pnpm publish --access public",
46
+ "release": "pnpm run ci && pnpm publish --no-git-checks",
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",
@@ -13,6 +13,10 @@ const { vitestWrapper } = await import(path.join(vitestRunnerRoot, 'dist/src/vit
13
13
  const STRYKER_SETUP = path.join(vitestRunnerRoot, 'dist/src/stryker-setup.js');
14
14
  const STRYKER_SETUP_SOURCE_MAP = 'stryker-setup.js.map';
15
15
 
16
+ export const strykerValidationSchema = JSON.parse(
17
+ fs.readFileSync(path.join(vitestRunnerRoot, 'dist/schema/vitest-runner-options.json'), 'utf-8'),
18
+ );
19
+
16
20
  function createStrykerSetupSourceMap(setupFilePath) {
17
21
  return JSON.stringify({
18
22
  version: 3,
@@ -21,7 +21,6 @@ module.exports = {
21
21
  testRunner: 'quality-tools-vitest',
22
22
  plugins: [
23
23
  path.join(packageRoot, 'stryker/quality-tools-vitest-runner.mjs'),
24
- '@stryker-mutator/vitest-runner',
25
24
  ],
26
25
  vitest: {
27
26
  configFile: path.isAbsolute(vitestConfig) ? vitestConfig : path.join(hostRoot, vitestConfig),