@poleski/quality-tools 0.1.4 → 0.2.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/cli/main.js CHANGED
@@ -1,5 +1,549 @@
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/dryChecker.ts
9
+ var NEAR_DUPLICATE_THRESHOLD = 0.72;
10
+ var POSSIBLE_SYNONYM_THRESHOLD = 0.45;
11
+ var IGNORED_TOKENS = /* @__PURE__ */ new Set([
12
+ "a",
13
+ "an",
14
+ "and",
15
+ "are",
16
+ "be",
17
+ "i",
18
+ "in",
19
+ "is",
20
+ "of",
21
+ "the",
22
+ "then",
23
+ "to",
24
+ "when"
25
+ ]);
26
+ function analyzeAcceptanceIrDryness(document, options = {}) {
27
+ const occurrences = document.scenarios.flatMap(
28
+ (scenario, scenarioIndex) => scenario.steps.map((step, stepIndex) => ({
29
+ step,
30
+ location: createLocation(scenario, scenarioIndex, step, stepIndex)
31
+ }))
32
+ );
33
+ const scenarioShapeGroups = findRepeatedScenarioShapes(document.scenarios);
34
+ const repeatedStepPatterns = findRepeatedStepPatterns(occurrences);
35
+ const findings = [
36
+ ...findDuplicateInScenario(document.scenarios),
37
+ ...options.includeExact ? findExactDuplicates(occurrences) : [],
38
+ ...findPlaceholderVariants(occurrences),
39
+ ...repeatedStepPatterns,
40
+ ...options.includeSimilar ? findSimilarSteps(occurrences) : []
41
+ ];
42
+ return {
43
+ schema_version: 1,
44
+ source_path: document.source_path,
45
+ feature_name: document.feature.name,
46
+ summary: {
47
+ scenarios: document.scenarios.length,
48
+ step_occurrences: occurrences.length,
49
+ unique_steps: new Set(occurrences.map((occurrence) => occurrence.step.text)).size,
50
+ repeated_step_patterns: repeatedStepPatterns.length,
51
+ repeated_scenario_shapes: scenarioShapeGroups.length,
52
+ findings: findings.length
53
+ },
54
+ repeated_scenario_shapes: scenarioShapeGroups,
55
+ findings
56
+ };
57
+ }
58
+ function findDuplicateInScenario(scenarios) {
59
+ return scenarios.flatMap((scenario, scenarioIndex) => {
60
+ const groups = groupOccurrences(scenario.steps.map((step, stepIndex) => ({
61
+ step,
62
+ location: createLocation(scenario, scenarioIndex, step, stepIndex)
63
+ })), (occurrence) => occurrence.step.text);
64
+ return [...groups.entries()].filter(([, members]) => containsDuplicateWithoutInterveningAction(scenario.steps, members)).map(([text, members]) => createFinding({
65
+ kind: "duplicate-in-scenario",
66
+ confidence: "high",
67
+ canonicalCandidate: text,
68
+ members,
69
+ reason: "the same step text appears more than once in one scenario",
70
+ suggestedAction: "Review the scenario and remove the repeated step if it is accidental."
71
+ }));
72
+ });
73
+ }
74
+ function containsDuplicateWithoutInterveningAction(steps, members) {
75
+ const stepIndexes = members.map((member) => member.location.step_index).sort((left, right) => left - right);
76
+ return stepIndexes.some((stepIndex, index) => {
77
+ const nextStepIndex = stepIndexes[index + 1];
78
+ return nextStepIndex !== void 0 && !hasInterveningAction(steps, stepIndex, nextStepIndex);
79
+ });
80
+ }
81
+ function hasInterveningAction(steps, leftIndex, rightIndex) {
82
+ const effectiveKeywords = getEffectiveKeywords(steps);
83
+ return steps.slice(leftIndex + 1, rightIndex).some((_, offset) => {
84
+ const effectiveKeyword = effectiveKeywords[leftIndex + offset + 1];
85
+ return effectiveKeyword === "Given" || effectiveKeyword === "When";
86
+ });
87
+ }
88
+ function getEffectiveKeywords(steps) {
89
+ let currentKeyword = "Given";
90
+ return steps.map((step) => {
91
+ if (step.keyword !== "And" && step.keyword !== "But") {
92
+ currentKeyword = step.keyword;
93
+ }
94
+ return currentKeyword;
95
+ });
96
+ }
97
+ function findExactDuplicates(occurrences) {
98
+ return [...groupOccurrences(occurrences, (occurrence) => occurrence.step.text).entries()].filter(([, members]) => members.length > 1).map(([text, members]) => createFinding({
99
+ kind: "exact-duplicate",
100
+ confidence: "medium",
101
+ canonicalCandidate: text,
102
+ members,
103
+ reason: "the same step text appears more than once in the feature",
104
+ suggestedAction: "Keep repeated setup vocabulary when intentional; normalize only accidental drift."
105
+ }));
106
+ }
107
+ function findPlaceholderVariants(occurrences) {
108
+ return [...groupOccurrences(occurrences, (occurrence) => normalizePlaceholders(occurrence.step.text)).entries()].filter(([, members]) => members.length > 1 && new Set(members.map((member) => member.step.text)).size > 1).map(([text, members]) => createFinding({
109
+ kind: "placeholder-variant",
110
+ confidence: "high",
111
+ canonicalCandidate: text,
112
+ patternCandidate: patternFromPlaceholderText(text),
113
+ members,
114
+ reason: "step text is identical after replacing placeholder names with generic slots",
115
+ suggestedAction: "Normalize the Gherkin if the different placeholder names do not add reader meaning."
116
+ }));
117
+ }
118
+ function findRepeatedStepPatterns(occurrences) {
119
+ return [...groupOccurrences(occurrences, (occurrence) => normalizeStepPattern(occurrence.step.text)).entries()].filter(([, members]) => members.length > 1 && new Set(members.map((member) => member.step.text)).size > 1).map(([pattern, members]) => createFinding({
120
+ kind: "repeated-step-pattern",
121
+ confidence: "high",
122
+ canonicalCandidate: pattern,
123
+ patternCandidate: pattern,
124
+ members,
125
+ reason: "step text follows the same generalized pattern with different example values",
126
+ suggestedAction: "Consider a Scenario Outline, data table, or shared Background if the repeated shape is intentional."
127
+ }));
128
+ }
129
+ function findSimilarSteps(occurrences) {
130
+ return findSimilarStepCandidates(occurrences).map(createSimilarStepFinding);
131
+ }
132
+ function findSimilarStepCandidates(occurrences) {
133
+ return occurrences.flatMap(
134
+ (left, leftIndex) => occurrences.slice(leftIndex + 1).map((right) => createSimilarStepCandidate(left, right)).filter((candidate) => candidate !== void 0)
135
+ );
136
+ }
137
+ function createSimilarStepCandidate(left, right) {
138
+ if (!shouldCompareSimilarSteps(left, right)) {
139
+ return void 0;
140
+ }
141
+ const score = tokenSimilarity(left.step.text, right.step.text);
142
+ return score >= POSSIBLE_SYNONYM_THRESHOLD ? { left, right, score } : void 0;
143
+ }
144
+ function shouldCompareSimilarSteps(left, right) {
145
+ return left.step.text !== right.step.text && normalizePlaceholders(left.step.text) !== normalizePlaceholders(right.step.text);
146
+ }
147
+ function createSimilarStepFinding(candidate) {
148
+ return createFinding({
149
+ kind: similarStepKind(candidate.score),
150
+ confidence: similarStepConfidence(candidate.score),
151
+ canonicalCandidate: candidate.left.step.text,
152
+ members: [candidate.left, candidate.right],
153
+ reason: "step texts share similar normalized tokens",
154
+ score: Number(candidate.score.toFixed(3)),
155
+ suggestedAction: "Review whether these steps express the same behavior before changing feature wording."
156
+ });
157
+ }
158
+ function similarStepKind(score) {
159
+ return score >= NEAR_DUPLICATE_THRESHOLD ? "near-duplicate" : "possible-synonym";
160
+ }
161
+ function similarStepConfidence(score) {
162
+ return score >= NEAR_DUPLICATE_THRESHOLD ? "medium" : "low";
163
+ }
164
+ function createLocation(scenario, scenarioIndex, step, stepIndex) {
165
+ return {
166
+ section: "scenario",
167
+ scenario_index: scenarioIndex,
168
+ scenario_name: scenario.name,
169
+ step_index: stepIndex,
170
+ keyword: step.keyword,
171
+ line: step.line
172
+ };
173
+ }
174
+ function findRepeatedScenarioShapes(scenarios) {
175
+ const exactShapeGroups = [...groupOccurrences(
176
+ scenarios.map((scenario, scenarioIndex) => ({ scenario, scenarioIndex })),
177
+ ({ scenario }) => scenario.steps.map((step) => normalizeStepPattern(step.text)).join("\n")
178
+ ).entries()].filter(([, members]) => members.length > 1).map(([, members]) => createScenarioShapeGroup(members, members[0]?.scenario.steps.length ?? 0, "high"));
179
+ const prefixShapeGroups = [...groupOccurrences(
180
+ scenarios.map((scenario, scenarioIndex) => ({
181
+ scenario,
182
+ scenarioIndex,
183
+ prefix: longestSetupPrefix(scenario)
184
+ })).filter((entry) => entry.prefix.length >= 3),
185
+ ({ prefix }) => prefix.join("\n")
186
+ ).entries()].filter(([, members]) => members.length > 1).map(([, members]) => createScenarioShapeGroup(members, members[0]?.prefix.length ?? 0, "medium"));
187
+ return dedupeScenarioShapeGroups([...exactShapeGroups, ...prefixShapeGroups]);
188
+ }
189
+ function createScenarioShapeGroup(members, sharedStepCount, confidence) {
190
+ const pattern = members[0]?.prefix ?? members[0]?.scenario.steps.slice(0, sharedStepCount).map((step) => normalizeStepPattern(step.text)) ?? [];
191
+ return {
192
+ confidence,
193
+ scenario_count: members.length,
194
+ shared_step_count: sharedStepCount,
195
+ pattern,
196
+ scenarios: members.map(({ scenario, scenarioIndex }) => ({
197
+ scenario_index: scenarioIndex,
198
+ scenario_name: scenario.name,
199
+ line: scenario.line,
200
+ examples: scenario.examples.length
201
+ })),
202
+ reason: confidence === "high" ? "multiple scenarios have the same generalized step sequence" : "multiple scenarios share the same setup or action prefix before diverging into specific assertions",
203
+ suggested_action: confidence === "high" ? "Consider collapsing these scenarios into a Scenario Outline or a data-driven runner." : "Consider moving the shared prefix into Background or a host fixture if it does not need to be repeated in every scenario."
204
+ };
205
+ }
206
+ function longestSetupPrefix(scenario) {
207
+ const normalizedSteps = scenario.steps.map((step) => normalizeStepPattern(step.text));
208
+ const assertionIndex = scenario.steps.findIndex((step) => step.keyword === "Then");
209
+ const prefixLength = assertionIndex === -1 ? normalizedSteps.length : assertionIndex;
210
+ return normalizedSteps.slice(0, prefixLength);
211
+ }
212
+ function dedupeScenarioShapeGroups(groups) {
213
+ const seen = /* @__PURE__ */ new Set();
214
+ const sorted = groups.sort(
215
+ (left, right) => right.scenario_count - left.scenario_count || right.shared_step_count - left.shared_step_count || left.confidence.localeCompare(right.confidence)
216
+ );
217
+ return sorted.filter((group) => {
218
+ const scenarioKey = group.scenarios.map((scenario) => scenario.scenario_index).join(",");
219
+ const key = `${scenarioKey}:${group.shared_step_count}:${group.pattern.join("\n")}`;
220
+ if (seen.has(key)) {
221
+ return false;
222
+ }
223
+ seen.add(key);
224
+ return true;
225
+ });
226
+ }
227
+ function createFinding(input) {
228
+ return {
229
+ kind: input.kind,
230
+ confidence: input.confidence,
231
+ canonical_candidate: input.canonicalCandidate,
232
+ ...input.patternCandidate ? { pattern_candidate: input.patternCandidate } : {},
233
+ ...input.score === void 0 ? {} : { score: input.score },
234
+ members: [...groupOccurrences(input.members, (member) => member.step.text).entries()].map(([text, members]) => ({
235
+ text,
236
+ locations: members.map((member) => member.location)
237
+ })),
238
+ reason: input.reason,
239
+ suggested_action: input.suggestedAction
240
+ };
241
+ }
242
+ function groupOccurrences(values, keySelector) {
243
+ const groups = /* @__PURE__ */ new Map();
244
+ values.forEach((value) => {
245
+ const key = keySelector(value);
246
+ groups.set(key, [...groups.get(key) ?? [], value]);
247
+ });
248
+ return groups;
249
+ }
250
+ function normalizePlaceholders(text) {
251
+ let index = 0;
252
+ return text.replace(/<[^>]+>/g, () => {
253
+ index += 1;
254
+ return `<_${index}>`;
255
+ });
256
+ }
257
+ function normalizeStepPattern(text) {
258
+ return normalizePlaceholders(text).replace(/\b\d+\b/g, "<number>").replace(/\bexamples\/[A-Za-z0-9_-]+\b/g, "<workspace>").replace(/\b[A-Za-z0-9._/-]+\.[A-Za-z0-9]+\b/g, "<path>").replace(/\b(File|Folder|Package|Symbol|Namespace|Function|Callable|Method|Constructor|Prototype|Class|Interface|Record|Delegate|Property|Event|Type|Struct|Union|Enum|Alias|Template|Typedef|Variable|Constant|Global|Field|Parameter|Local|Godot class_name)\b/g, "<node-type>").replace(/\b(Include|Imports|References|Calls|Type imports|Inherits|Using|Call|Implements|Loads|Nests|Contains|Overrides|TypeScript Alias Import)\b/g, "<edge-type>").replace(/\s+/g, " ").trim();
259
+ }
260
+ function patternFromPlaceholderText(text) {
261
+ const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
262
+ return `^${escaped.replace(/<_[0-9]+>/g, "(.+)")}$`;
263
+ }
264
+ function tokenSimilarity(left, right) {
265
+ const leftTokens = tokenize(left);
266
+ const rightTokens = tokenize(right);
267
+ const union = /* @__PURE__ */ new Set([...leftTokens, ...rightTokens]);
268
+ if (union.size === 0) {
269
+ return 0;
270
+ }
271
+ const intersection = [...leftTokens].filter((token) => rightTokens.has(token));
272
+ return intersection.length / union.size;
273
+ }
274
+ function tokenize(text) {
275
+ return new Set(
276
+ text.replace(/<[^>]+>/g, " ").toLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length > 0 && !IGNORED_TOKENS.has(token))
277
+ );
278
+ }
279
+
280
+ // src/acceptance/ir.ts
281
+ function toAcceptanceIr(document) {
282
+ return {
283
+ schema_version: 1,
284
+ source_path: document.sourcePath,
285
+ feature: {
286
+ name: document.feature.name,
287
+ line: document.feature.line
288
+ },
289
+ ...document.background ? {
290
+ background: {
291
+ line: document.background.line,
292
+ steps: document.background.steps.map(toAcceptanceIrStep)
293
+ }
294
+ } : {},
295
+ scenarios: document.scenarios.map((scenario) => ({
296
+ name: scenario.name,
297
+ line: scenario.line,
298
+ steps: scenario.steps.map(toAcceptanceIrStep),
299
+ examples: scenario.examples.map((example) => ({
300
+ line: example.line,
301
+ values: example.values
302
+ }))
303
+ }))
304
+ };
305
+ }
306
+ function toAcceptanceIrStep(documentStep) {
307
+ return {
308
+ keyword: documentStep.keyword,
309
+ text: documentStep.text,
310
+ line: documentStep.line,
311
+ parameters: documentStep.parameters
312
+ };
313
+ }
314
+
315
+ // src/acceptance/parser.ts
316
+ var FEATURE_PATTERN = /^#{0,6}\s*Feature:\s*(.+)$/;
317
+ var BACKGROUND_PATTERN = /^#{0,6}\s*Background:\s*$/;
318
+ var SCENARIO_PATTERN = /^#{0,6}\s*Scenario:\s*(.+)$/;
319
+ var SCENARIO_OUTLINE_PATTERN = /^#{0,6}\s*Scenario Outline:\s*(.+)$/;
320
+ var EXAMPLES_PATTERN = /^#{0,6}\s*Examples:\s*$/;
321
+ var STEP_PATTERN = /^(Given|When|Then|And|But)\s+(.+)$/;
322
+ var PARAMETER_PATTERN = /<([A-Za-z0-9_]+)>/g;
323
+ var LINE_PARSERS = [
324
+ parseFeatureHeading,
325
+ parseBackgroundHeading,
326
+ parseScenarioHeading,
327
+ parseExamplesHeading,
328
+ parseExamplesRowLine,
329
+ parseStepLine
330
+ ];
331
+ function parseAcceptanceFeature(featureSource, sourcePath) {
332
+ const state = createParserState(sourcePath);
333
+ parseFeatureLines(state, featureSource);
334
+ validateDocument(state);
335
+ return createDocument(state);
336
+ }
337
+ function createParserState(sourcePath) {
338
+ return {
339
+ sourcePath,
340
+ scenarios: [],
341
+ scenariosWithExamples: /* @__PURE__ */ new Set()
342
+ };
343
+ }
344
+ function parseFeatureLines(state, featureSource) {
345
+ featureSource.split(/\r?\n/).forEach((rawLine, index) => {
346
+ const lineNumber = index + 1;
347
+ const line = rawLine.trim();
348
+ parseFeatureLine(state, line, lineNumber);
349
+ });
350
+ }
351
+ function parseFeatureLine(state, line, lineNumber) {
352
+ if (line === "") {
353
+ return;
354
+ }
355
+ for (const parseLine of LINE_PARSERS) {
356
+ if (parseLine(state, line, lineNumber)) {
357
+ return;
358
+ }
359
+ }
360
+ }
361
+ function parseFeatureHeading(state, line, lineNumber) {
362
+ const featureMatch = FEATURE_PATTERN.exec(line);
363
+ if (!featureMatch) {
364
+ return false;
365
+ }
366
+ state.feature = {
367
+ name: featureMatch[1].trim(),
368
+ line: lineNumber
369
+ };
370
+ clearActiveTargets(state);
371
+ return true;
372
+ }
373
+ function parseBackgroundHeading(state, line, lineNumber) {
374
+ if (!BACKGROUND_PATTERN.test(line)) {
375
+ return false;
376
+ }
377
+ if (state.background) {
378
+ throw new Error(`${state.sourcePath}:${lineNumber} Feature has more than one Background`);
379
+ }
380
+ state.background = {
381
+ line: lineNumber,
382
+ steps: []
383
+ };
384
+ state.stepTarget = state.background;
385
+ clearExamplesTarget(state);
386
+ return true;
387
+ }
388
+ function parseScenarioHeading(state, line, lineNumber) {
389
+ const scenarioMatch = SCENARIO_PATTERN.exec(line) ?? SCENARIO_OUTLINE_PATTERN.exec(line);
390
+ if (!scenarioMatch) {
391
+ return false;
392
+ }
393
+ const scenario = {
394
+ name: scenarioMatch[1].trim(),
395
+ line: lineNumber,
396
+ steps: [],
397
+ examples: []
398
+ };
399
+ state.scenarios.push(scenario);
400
+ state.stepTarget = scenario;
401
+ clearExamplesTarget(state);
402
+ return true;
403
+ }
404
+ function parseExamplesHeading(state, line, lineNumber) {
405
+ if (!EXAMPLES_PATTERN.test(line)) {
406
+ return false;
407
+ }
408
+ const scenario = state.scenarios.at(-1);
409
+ if (!scenario) {
410
+ throw new Error(`${state.sourcePath}:${lineNumber} Examples appear before a Scenario`);
411
+ }
412
+ state.examplesTarget = scenario;
413
+ state.scenariosWithExamples.add(scenario);
414
+ state.stepTarget = void 0;
415
+ state.examplesHeaders = void 0;
416
+ return true;
417
+ }
418
+ function parseExamplesRowLine(state, line, lineNumber) {
419
+ if (!line.startsWith("|")) {
420
+ return false;
421
+ }
422
+ if (!state.examplesTarget) {
423
+ return true;
424
+ }
425
+ const values = parseExamplesRow(line);
426
+ if (!state.examplesHeaders) {
427
+ setExamplesHeaders(state, values, lineNumber);
428
+ return true;
429
+ }
430
+ if (values.length !== state.examplesHeaders.length) {
431
+ throw new Error(`${state.sourcePath}:${lineNumber} Examples row has ${values.length} cells; expected ${state.examplesHeaders.length}`);
432
+ }
433
+ state.examplesTarget.examples.push({
434
+ line: lineNumber,
435
+ values: Object.fromEntries(state.examplesHeaders.map((header, cellIndex) => [header, values[cellIndex] ?? ""]))
436
+ });
437
+ return true;
438
+ }
439
+ function setExamplesHeaders(state, values, lineNumber) {
440
+ state.examplesHeaders = values;
441
+ if (state.examplesHeaders.length === 0) {
442
+ throw new Error(`${state.sourcePath}:${lineNumber} Examples header must contain at least one column`);
443
+ }
444
+ }
445
+ function parseStepLine(state, line, lineNumber) {
446
+ const stepMatch = STEP_PATTERN.exec(line);
447
+ if (!stepMatch) {
448
+ return false;
449
+ }
450
+ if (!state.stepTarget) {
451
+ throw new Error(`${state.sourcePath}:${lineNumber} Step appears before a Scenario or Background`);
452
+ }
453
+ const text = stepMatch[2].trim();
454
+ state.stepTarget.steps.push({
455
+ keyword: stepMatch[1],
456
+ text,
457
+ line: lineNumber,
458
+ parameters: extractParameters(text)
459
+ });
460
+ return true;
461
+ }
462
+ function clearActiveTargets(state) {
463
+ state.stepTarget = void 0;
464
+ clearExamplesTarget(state);
465
+ }
466
+ function clearExamplesTarget(state) {
467
+ state.examplesTarget = void 0;
468
+ state.examplesHeaders = void 0;
469
+ }
470
+ function validateDocument(state) {
471
+ assertFeatureExists(state);
472
+ assertHasScenarios(state);
473
+ assertBackgroundHasSteps(state);
474
+ assertScenariosHaveSteps(state);
475
+ assertExamplesHaveRows(state);
476
+ }
477
+ function assertFeatureExists(state) {
478
+ if (!state.feature) {
479
+ throw new Error(`${state.sourcePath}: Expected a Feature heading`);
480
+ }
481
+ }
482
+ function assertHasScenarios(state) {
483
+ if (state.scenarios.length === 0) {
484
+ throw new Error(`${state.sourcePath}: Expected at least one Scenario`);
485
+ }
486
+ }
487
+ function assertBackgroundHasSteps(state) {
488
+ if (state.background && state.background.steps.length === 0) {
489
+ throw new Error(`${state.sourcePath}:${state.background.line} Background must contain at least one step`);
490
+ }
491
+ }
492
+ function assertScenariosHaveSteps(state) {
493
+ const emptyScenario = state.scenarios.find((scenario) => scenario.steps.length === 0);
494
+ if (emptyScenario) {
495
+ throw new Error(`${state.sourcePath}:${emptyScenario.line} Scenario "${emptyScenario.name}" must contain at least one step`);
496
+ }
497
+ }
498
+ function assertExamplesHaveRows(state) {
499
+ const emptyExamples = state.scenarios.find(
500
+ (scenario) => state.scenariosWithExamples.has(scenario) && scenario.examples.length === 0
501
+ );
502
+ if (emptyExamples) {
503
+ throw new Error(`${state.sourcePath}:${emptyExamples.line} Scenario "${emptyExamples.name}" has Examples without rows`);
504
+ }
505
+ }
506
+ function createDocument(state) {
507
+ return {
508
+ sourcePath: state.sourcePath,
509
+ feature: state.feature,
510
+ ...state.background ? { background: state.background } : {},
511
+ scenarios: state.scenarios
512
+ };
513
+ }
514
+ function extractParameters(text) {
515
+ return [...text.matchAll(PARAMETER_PATTERN)].map((match) => match[1] ?? "");
516
+ }
517
+ function parseExamplesRow(line) {
518
+ return line.slice(1, line.endsWith("|") ? -1 : void 0).split("|").map((cell) => cell.trim());
519
+ }
520
+
521
+ // src/acceptance/playwright/generator.ts
522
+ import { readFileSync } from "node:fs";
523
+ var runtimeTemplateUrl = new URL("./templates/playwright-acceptance-runtime.ts.template", import.meta.url);
524
+ function generatePlaywrightAcceptanceSpec(options) {
525
+ return [
526
+ "/* Generated by quality-tools acceptance generate. Do not edit. */",
527
+ "import { test } from '@playwright/test';",
528
+ `import { acceptanceSteps, createAcceptanceContext } from ${quote(options.stepsImportPath)};`,
529
+ `import { loadAcceptanceIr, runAcceptanceFeature } from ${quote(options.runtimeImportPath)};`,
530
+ "",
531
+ `const feature = loadAcceptanceIr(${quote(options.irImportPath)});`,
532
+ "",
533
+ "runAcceptanceFeature(test, feature, {",
534
+ " acceptanceSteps,",
535
+ " createAcceptanceContext",
536
+ "});",
537
+ ""
538
+ ].join("\n");
539
+ }
540
+ function generatePlaywrightAcceptanceRuntime() {
541
+ return readFileSync(runtimeTemplateUrl, "utf8");
542
+ }
543
+ function quote(value) {
544
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n")}'`;
545
+ }
546
+
3
547
  // src/shared/flagValue.ts
4
548
  function flagValue(args2, name) {
5
549
  const inlineFlag = args2.find((arg) => arg.startsWith(`${name}=`));
@@ -31,6 +575,229 @@ function cleanCliArgs(args2) {
31
575
  return args2.filter((arg) => arg !== "--");
32
576
  }
33
577
 
578
+ // src/acceptance/command.ts
579
+ var ACCEPTANCE_COMMANDS = /* @__PURE__ */ new Map([
580
+ ["parse", parseCommand],
581
+ ["dry-check", dryCheckCommand],
582
+ ["generate", generateCommand],
583
+ ["compile", compileCommand]
584
+ ]);
585
+ var COMPILE_VALUE_FLAGS = [
586
+ "--steps",
587
+ "--ir",
588
+ "--dry",
589
+ "--spec",
590
+ "--out-dir",
591
+ "--ir-dir",
592
+ "--dry-report-dir"
593
+ ];
594
+ async function runAcceptanceCli(rawArgs, options = {}) {
595
+ const args2 = cleanCliArgs(rawArgs);
596
+ const [command2, ...commandArgs] = args2;
597
+ const cwd = options.cwd ?? process.cwd();
598
+ if (isHelpCommand(command2)) {
599
+ console.log(acceptanceUsage());
600
+ return;
601
+ }
602
+ await requireAcceptanceCommand(command2)(commandArgs, cwd);
603
+ }
604
+ function acceptanceUsage() {
605
+ return [
606
+ "Usage:",
607
+ " quality-tools acceptance parse <feature-file> <json-output>",
608
+ " quality-tools acceptance dry-check [--include-exact] [--include-similar] <json-ir> <report-output>",
609
+ " quality-tools acceptance generate <json-ir> <generated-test-output> --steps <path>",
610
+ " quality-tools acceptance compile <spec-glob> <generated-output-dir> --steps <path> [--ir <dir>] [--dry <dir>]"
611
+ ].join("\n");
612
+ }
613
+ function parseCommand(args2, cwd) {
614
+ const [featurePath, jsonOutputPath, ...extraArgs] = args2;
615
+ if (!featurePath || !jsonOutputPath || extraArgs.length > 0) {
616
+ throw new Error("Usage: quality-tools acceptance parse <feature-file> <json-output>");
617
+ }
618
+ const ir = parseIrFile(cwd, featurePath);
619
+ writeJsonFile(path.resolve(cwd, jsonOutputPath), ir);
620
+ }
621
+ function dryCheckCommand(args2, cwd) {
622
+ const options = parseDryCheckOptions(args2);
623
+ const report = analyzeAcceptanceIrDryness(readIrFile(cwd, options.irPath), {
624
+ includeExact: options.includeExact,
625
+ includeSimilar: options.includeSimilar
626
+ });
627
+ writeJsonFile(path.resolve(cwd, options.reportPath), report);
628
+ }
629
+ function generateCommand(args2, cwd) {
630
+ const options = parseGenerateOptions(args2);
631
+ writeGeneratedPlaywrightSpec(cwd, options.irPath, options.outPath, options.stepsPath);
632
+ }
633
+ async function compileCommand(args2, cwd) {
634
+ const options = parseCompileOptions(args2);
635
+ const specFiles = await findSpecFiles(cwd, options.specPatterns);
636
+ if (specFiles.length === 0) {
637
+ throw new Error(`No acceptance specs matched: ${options.specPatterns.join(", ")}`);
638
+ }
639
+ const generatedDir = path.resolve(cwd, options.generatedDir);
640
+ writePlaywrightRuntime(generatedDir);
641
+ specFiles.forEach((specFile) => {
642
+ const ir = parseIrFile(cwd, specFile);
643
+ const slug = sourcePathSlug(ir.source_path);
644
+ const irPath = path.join(path.resolve(cwd, options.irDir), `${slug}.json`);
645
+ const generatedPath = path.join(generatedDir, `${slug}.spec.ts`);
646
+ writeJsonFile(irPath, ir);
647
+ if (options.dryDir) {
648
+ writeJsonFile(path.join(path.resolve(cwd, options.dryDir), `${slug}.json`), analyzeAcceptanceIrDryness(ir, {
649
+ includeExact: options.includeExact,
650
+ includeSimilar: options.includeSimilar
651
+ }));
652
+ }
653
+ writeGeneratedPlaywrightSpec(cwd, irPath, generatedPath, options.stepsPath);
654
+ });
655
+ }
656
+ function parseDryCheckOptions(args2) {
657
+ const positional = args2.filter((arg) => !arg.startsWith("--"));
658
+ if (positional.length !== 2) {
659
+ throw new Error("Usage: quality-tools acceptance dry-check [--include-exact] [--include-similar] <json-ir> <report-output>");
660
+ }
661
+ return {
662
+ irPath: positional[0] ?? "",
663
+ reportPath: positional[1] ?? "",
664
+ includeExact: args2.includes("--include-exact"),
665
+ includeSimilar: args2.includes("--include-similar")
666
+ };
667
+ }
668
+ function parseGenerateOptions(args2) {
669
+ const positional = args2.filter((arg, index) => arg !== "--steps" && args2[index - 1] !== "--steps" && !arg.startsWith("--"));
670
+ if (positional.length !== 2) {
671
+ throw new Error("Usage: quality-tools acceptance generate <json-ir> <generated-test-output> --steps <path>");
672
+ }
673
+ return {
674
+ irPath: positional[0] ?? "",
675
+ outPath: positional[1] ?? "",
676
+ stepsPath: requireFlagValue(args2, "--steps")
677
+ };
678
+ }
679
+ function parseCompileOptions(args2) {
680
+ const positional = collectPositionalArgs(args2, COMPILE_VALUE_FLAGS);
681
+ const specPatterns = resolveCompileSpecPatterns(args2, positional);
682
+ const generatedDir = requireCompileGeneratedDir(specPatterns, resolveGeneratedDir(args2, positional));
683
+ return {
684
+ specPatterns,
685
+ stepsPath: requireFlagValue(args2, "--steps"),
686
+ generatedDir,
687
+ irDir: resolveIrDir(args2, generatedDir),
688
+ dryDir: resolveDryDir(args2),
689
+ includeExact: args2.includes("--include-exact"),
690
+ includeSimilar: args2.includes("--include-similar")
691
+ };
692
+ }
693
+ function isHelpCommand(command2) {
694
+ return !command2 || command2 === "--help" || command2 === "-h";
695
+ }
696
+ function requireAcceptanceCommand(command2) {
697
+ const runner = command2 ? ACCEPTANCE_COMMANDS.get(command2) : void 0;
698
+ if (!runner) {
699
+ throw new Error(acceptanceUsage());
700
+ }
701
+ return runner;
702
+ }
703
+ function collectPositionalArgs(args2, valueFlags) {
704
+ return args2.filter(
705
+ (arg, index) => !arg.startsWith("--") && !isFlagValue(args2, index, valueFlags)
706
+ );
707
+ }
708
+ function resolveCompileSpecPatterns(args2, positional) {
709
+ const specFlags = collectFlagValues(args2, "--spec");
710
+ return specFlags.length > 0 ? specFlags : positional.slice(0, 1);
711
+ }
712
+ function resolveGeneratedDir(args2, positional) {
713
+ return collectFlagValues(args2, "--out-dir").at(0) ?? positional[1];
714
+ }
715
+ function requireCompileGeneratedDir(specPatterns, generatedDir) {
716
+ if (specPatterns.length === 0 || !generatedDir) {
717
+ throw new Error("Usage: quality-tools acceptance compile <spec-glob> <generated-output-dir> --steps <path> [--ir <dir>] [--dry <dir>]");
718
+ }
719
+ return generatedDir;
720
+ }
721
+ function resolveIrDir(args2, generatedDir) {
722
+ return collectFlagValues(args2, "--ir").at(0) ?? collectFlagValues(args2, "--ir-dir").at(0) ?? path.join(generatedDir, "..", "generated-ir");
723
+ }
724
+ function resolveDryDir(args2) {
725
+ return collectFlagValues(args2, "--dry").at(0) ?? collectFlagValues(args2, "--dry-report-dir").at(0);
726
+ }
727
+ function parseIrFile(cwd, featurePath) {
728
+ const resolvedPath = path.resolve(cwd, featurePath);
729
+ const source = fs.readFileSync(resolvedPath, "utf8");
730
+ return toAcceptanceIr(parseAcceptanceFeature(source, toPosixPath(path.relative(cwd, resolvedPath))));
731
+ }
732
+ function readIrFile(cwd, irPath) {
733
+ return JSON.parse(fs.readFileSync(path.resolve(cwd, irPath), "utf8"));
734
+ }
735
+ function writeGeneratedPlaywrightSpec(cwd, irPath, outPath, stepsPath) {
736
+ const resolvedOutPath = path.resolve(cwd, outPath);
737
+ const generatedDir = path.dirname(resolvedOutPath);
738
+ writePlaywrightRuntime(generatedDir);
739
+ writeFile(resolvedOutPath, generatePlaywrightAcceptanceSpec({
740
+ irImportPath: toPosixPath(path.relative(cwd, path.resolve(cwd, irPath))),
741
+ runtimeImportPath: createExtensionlessImportPath(resolvedOutPath, path.join(generatedDir, "runtime.ts")),
742
+ stepsImportPath: createExtensionlessImportPath(resolvedOutPath, path.resolve(cwd, stepsPath))
743
+ }));
744
+ }
745
+ function writePlaywrightRuntime(generatedDir) {
746
+ writeFile(path.join(generatedDir, "runtime.ts"), generatePlaywrightAcceptanceRuntime());
747
+ }
748
+ function writeJsonFile(filePath, data) {
749
+ writeFile(filePath, `${JSON.stringify(data, null, 2)}
750
+ `);
751
+ }
752
+ function writeFile(filePath, content) {
753
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
754
+ fs.writeFileSync(filePath, content);
755
+ }
756
+ async function findSpecFiles(cwd, patterns) {
757
+ const files = await Promise.all(
758
+ patterns.map((pattern) => glob(pattern, { absolute: true, cwd, nodir: true }))
759
+ );
760
+ return [...new Set(files.flat())].sort((left, right) => left.localeCompare(right));
761
+ }
762
+ function createExtensionlessImportPath(outPath, targetPath) {
763
+ const relativePath = toPosixPath(path.relative(path.dirname(outPath), targetPath));
764
+ const extension = path.extname(relativePath);
765
+ const extensionlessPath = extension ? relativePath.slice(0, -extension.length) : relativePath;
766
+ if (extensionlessPath.startsWith(".")) {
767
+ return extensionlessPath;
768
+ }
769
+ return `./${extensionlessPath}`;
770
+ }
771
+ function collectFlagValues(args2, flag) {
772
+ const values = [];
773
+ args2.forEach((arg, index) => {
774
+ if (arg === flag) {
775
+ const value = args2[index + 1];
776
+ if (!value || value.startsWith("--")) {
777
+ throw new Error(`Missing value for ${flag}`);
778
+ }
779
+ values.push(value);
780
+ }
781
+ });
782
+ return values;
783
+ }
784
+ function requireFlagValue(args2, flag) {
785
+ const value = collectFlagValues(args2, flag).at(0);
786
+ if (!value) {
787
+ throw new Error(`Missing required ${flag} <path>`);
788
+ }
789
+ return value;
790
+ }
791
+ function isFlagValue(args2, index, flags) {
792
+ return flags.includes(args2[index - 1] ?? "");
793
+ }
794
+ function toPosixPath(value) {
795
+ return value.split(path.sep).join(path.posix.sep);
796
+ }
797
+ function sourcePathSlug(sourcePath) {
798
+ return sourcePath.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
799
+ }
800
+
34
801
  // src/shared/resolve/repoRoot.ts
35
802
  import { resolve as resolve3 } from "node:path";
36
803
 
@@ -138,12 +905,12 @@ import { existsSync as existsSync4, statSync } from "fs";
138
905
  import { isAbsolute, resolve as resolve4 } from "path";
139
906
 
140
907
  // src/shared/util/workspacePackages.ts
141
- import { existsSync as existsSync3, readFileSync } from "fs";
908
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
142
909
  import { basename, dirname as dirname4, join as join3 } from "path";
143
910
  import { globSync } from "glob";
144
911
  function workspacePackageFromPackageJson(repoRoot, relativePackageJson) {
145
912
  const packageJsonPath = join3(repoRoot, relativePackageJson);
146
- const manifest = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
913
+ const manifest = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
147
914
  const relativeRoot = toPosix(dirname4(relativePackageJson));
148
915
  return {
149
916
  ...manifest.name ? { manifestName: manifest.name } : {},
@@ -157,7 +924,7 @@ function pnpmWorkspaceGlobs(repoRoot) {
157
924
  if (!existsSync3(workspacePath)) {
158
925
  return [];
159
926
  }
160
- const lines = readFileSync(workspacePath, "utf-8").split(/\r?\n/);
927
+ const lines = readFileSync2(workspacePath, "utf-8").split(/\r?\n/);
161
928
  const packageGlobs = [];
162
929
  let inPackagesBlock = false;
163
930
  for (const line of lines) {
@@ -183,7 +950,7 @@ function packageJsonWorkspaceGlobs(repoRoot) {
183
950
  if (!existsSync3(packageJsonPath)) {
184
951
  return [];
185
952
  }
186
- const manifest = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
953
+ const manifest = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
187
954
  if (Array.isArray(manifest.workspaces)) {
188
955
  return manifest.workspaces;
189
956
  }
@@ -216,7 +983,7 @@ function listWorkspacePackages(repoRoot) {
216
983
  const packages = [];
217
984
  const packageJsons = discoverWorkspacePackageJsons(repoRoot);
218
985
  if (shouldIncludeRootPackage(repoRoot, packageJsons)) {
219
- const packageJson = JSON.parse(readFileSync(join3(repoRoot, "package.json"), "utf-8"));
986
+ const packageJson = JSON.parse(readFileSync2(join3(repoRoot, "package.json"), "utf-8"));
220
987
  packages.push({
221
988
  ...packageJson.name ? { manifestName: packageJson.name } : {},
222
989
  name: packageJson.name?.split("/").pop() ?? "root",
@@ -334,7 +1101,7 @@ import { basename as basename3, join as join8 } from "path";
334
1101
  import { basename as basename2 } from "path";
335
1102
 
336
1103
  // src/organize/cohesion/imports/parse.ts
337
- import { readFileSync as readFileSync2 } from "fs";
1104
+ import { readFileSync as readFileSync3 } from "fs";
338
1105
  import * as ts2 from "typescript";
339
1106
 
340
1107
  // src/organize/cohesion/imports/scriptKind.ts
@@ -375,7 +1142,7 @@ function extractImports(sourceFile) {
375
1142
  }
376
1143
  function parseFileImports(filePath, fileName) {
377
1144
  try {
378
- const fileContent = readFileSync2(filePath, "utf-8");
1145
+ const fileContent = readFileSync3(filePath, "utf-8");
379
1146
  const scriptKind = getScriptKind(fileName);
380
1147
  const sourceFile = ts2.createSourceFile(
381
1148
  fileName,
@@ -412,7 +1179,7 @@ function resolveImportTarget(fromFile, specifier, candidatePaths) {
412
1179
  }
413
1180
 
414
1181
  // src/config/quality.ts
415
- import { readFileSync as readFileSync3 } from "fs";
1182
+ import { readFileSync as readFileSync4 } from "fs";
416
1183
  import { isAbsolute as isAbsolute2, join as join6, matchesGlob, relative as relative2, resolve as resolve5 } from "path";
417
1184
 
418
1185
  // src/config/patterns.ts
@@ -450,7 +1217,7 @@ var DEFAULT_REPORTS_DIR = "reports/quality-tools";
450
1217
  function loadQualityConfig(repoRoot) {
451
1218
  const configPath = join6(repoRoot, CONFIG_FILE);
452
1219
  try {
453
- return JSON.parse(readFileSync3(configPath, "utf-8"));
1220
+ return JSON.parse(readFileSync4(configPath, "utf-8"));
454
1221
  } catch {
455
1222
  return {};
456
1223
  }
@@ -872,7 +1639,7 @@ function runBoundariesCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES) {
872
1639
 
873
1640
  // src/crap/analysis/run.ts
874
1641
  import { existsSync as existsSync6 } from "fs";
875
- import * as path2 from "path";
1642
+ import * as path3 from "path";
876
1643
 
877
1644
  // src/crap/analysis/calculate.ts
878
1645
  function calculateCrap(complexity, coverage) {
@@ -997,8 +1764,8 @@ function extractFunctions(sourceFile) {
997
1764
  }
998
1765
 
999
1766
  // src/crap/analysis/fileSelection.ts
1000
- import { readFileSync as readFileSync4 } from "fs";
1001
- import * as path from "path";
1767
+ import { readFileSync as readFileSync5 } from "fs";
1768
+ import * as path2 from "path";
1002
1769
  import * as ts7 from "typescript";
1003
1770
  function matchesFilterScope(relativePath, filterScope) {
1004
1771
  if (!filterScope) {
@@ -1010,7 +1777,7 @@ function matchesFilterScope(relativePath, filterScope) {
1010
1777
  return relativePath.startsWith(`${filterScope}/`);
1011
1778
  }
1012
1779
  function shouldIncludeFile(filePath, filterScope, repoRoot) {
1013
- const relativePath = toPosix(path.relative(repoRoot, filePath));
1780
+ const relativePath = toPosix(path2.relative(repoRoot, filePath));
1014
1781
  if (!matchesFilterScope(relativePath, filterScope)) {
1015
1782
  return false;
1016
1783
  }
@@ -1022,13 +1789,13 @@ function shouldIncludeFile(filePath, filterScope, repoRoot) {
1022
1789
  repoRoot,
1023
1790
  workspacePackage.name,
1024
1791
  "crap",
1025
- toPosix(path.relative(workspacePackage.root, filePath))
1792
+ toPosix(path2.relative(workspacePackage.root, filePath))
1026
1793
  );
1027
1794
  }
1028
1795
  function createSourceFile3(filePath) {
1029
1796
  return ts7.createSourceFile(
1030
1797
  filePath,
1031
- readFileSync4(filePath, "utf-8"),
1798
+ readFileSync5(filePath, "utf-8"),
1032
1799
  ts7.ScriptTarget.Latest,
1033
1800
  true,
1034
1801
  filePath.endsWith(".tsx") ? ts7.ScriptKind.TSX : ts7.ScriptKind.TS
@@ -1058,7 +1825,7 @@ function analyzeCoverageEntry(filePath, fileCoverage, repoRoot, threshold) {
1058
1825
  complexity: fn.complexity,
1059
1826
  coverage: Math.round(coverage),
1060
1827
  crap: Math.round(crap * 100) / 100,
1061
- file: toPosix(path2.relative(repoRoot, fn.file)),
1828
+ file: toPosix(path3.relative(repoRoot, fn.file)),
1062
1829
  line: fn.line,
1063
1830
  name: fn.name
1064
1831
  };
@@ -1180,12 +1947,12 @@ function createCoverageProfiles(repoRoot, target) {
1180
1947
  }
1181
1948
 
1182
1949
  // src/crap/coverage/read.ts
1183
- 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}`);
1950
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1951
+ function readCoverageReport(path4) {
1952
+ if (!existsSync7(path4)) {
1953
+ throw new Error(`Coverage data not found: ${path4}`);
1187
1954
  }
1188
- return JSON.parse(readFileSync5(path3, "utf-8"));
1955
+ return JSON.parse(readFileSync6(path4, "utf-8"));
1189
1956
  }
1190
1957
 
1191
1958
  // src/crap/report.ts
@@ -1366,9 +2133,9 @@ function copySharedMutationReports(reportKey, repoRoot = process.cwd()) {
1366
2133
  }
1367
2134
 
1368
2135
  // src/mutation/reporting/check.ts
1369
- import { readFileSync as readFileSync6 } from "fs";
2136
+ import { readFileSync as readFileSync7 } from "fs";
1370
2137
  function findMutationSiteViolations(reportPath, threshold = 50) {
1371
- const report = JSON.parse(readFileSync6(reportPath, "utf-8"));
2138
+ const report = JSON.parse(readFileSync7(reportPath, "utf-8"));
1372
2139
  return Object.entries(report.files ?? {}).map(([file, entry]) => ({ count: (entry.mutants ?? []).length, file })).filter((entry) => entry.count > threshold).sort((left, right) => right.count - left.count);
1373
2140
  }
1374
2141
  function reportMutationSiteViolations(reportPath, threshold = 50) {
@@ -1665,7 +2432,7 @@ function parseBareMutationTargetArg(args2) {
1665
2432
  }
1666
2433
  return void 0;
1667
2434
  }
1668
- function collectFlagValues(args2, name) {
2435
+ function collectFlagValues2(args2, name) {
1669
2436
  const values = [];
1670
2437
  for (let index = 0; index < args2.length; index += 1) {
1671
2438
  const arg = args2[index];
@@ -1692,11 +2459,11 @@ function parseJsonStringArray(value, flagName) {
1692
2459
  }
1693
2460
  function mutationRunOptions(args2) {
1694
2461
  const mutateGlobs = [
1695
- ...collectFlagValues(args2, "--mutate-glob"),
2462
+ ...collectFlagValues2(args2, "--mutate-glob"),
1696
2463
  ...parseJsonStringArray(flagValue(args2, "--mutate-globs-json"), "--mutate-globs-json")
1697
2464
  ];
1698
2465
  const testIncludes = [
1699
- ...collectFlagValues(args2, "--test-include"),
2466
+ ...collectFlagValues2(args2, "--test-include"),
1700
2467
  ...parseJsonStringArray(flagValue(args2, "--test-includes-json"), "--test-includes-json")
1701
2468
  ];
1702
2469
  return {
@@ -1726,7 +2493,7 @@ import { join as join16 } from "path";
1726
2493
  import { relative as relative7 } from "path";
1727
2494
 
1728
2495
  // src/organize/rules.ts
1729
- import { readFileSync as readFileSync7 } from "fs";
2496
+ import { readFileSync as readFileSync8 } from "fs";
1730
2497
  import { join as join14 } from "path";
1731
2498
  var DEFAULT_CONFIG2 = {
1732
2499
  lowInfoNames: {
@@ -1753,7 +2520,7 @@ function mergeConfig(defaults, overrides) {
1753
2520
  function loadOrganizeConfig(repoRoot, packageName) {
1754
2521
  const configPath = join14(repoRoot, CONFIG_FILE3);
1755
2522
  try {
1756
- const rawConfig = JSON.parse(readFileSync7(configPath, "utf-8"));
2523
+ const rawConfig = JSON.parse(readFileSync8(configPath, "utf-8"));
1757
2524
  const defaultConfig = rawConfig.defaults?.organize;
1758
2525
  const packageConfig = packageName ? rawConfig.packages?.[packageName]?.organize : void 0;
1759
2526
  const mergedDefaults = defaultConfig ? mergeConfig(DEFAULT_CONFIG2, defaultConfig) : DEFAULT_CONFIG2;
@@ -1938,7 +2705,7 @@ function shouldStartNewToken(previous, current, next) {
1938
2705
  }
1939
2706
 
1940
2707
  // src/organize/naming/tokenize.ts
1941
- function tokenize(name) {
2708
+ function tokenize2(name) {
1942
2709
  const withoutExtension = stripExtension(name);
1943
2710
  const characters = Array.from(withoutExtension);
1944
2711
  const tokens = [];
@@ -1969,7 +2736,7 @@ function tokenize(name) {
1969
2736
  function buildPrefixGroups(fileNames) {
1970
2737
  const groups = /* @__PURE__ */ new Map();
1971
2738
  for (const fileName of fileNames) {
1972
- const tokens = tokenize(fileName);
2739
+ const tokens = tokenize2(fileName);
1973
2740
  if (tokens.length > 0) {
1974
2741
  const prefix = tokens[0];
1975
2742
  if (!groups.has(prefix)) {
@@ -1983,7 +2750,7 @@ function buildPrefixGroups(fileNames) {
1983
2750
  function countFirstTokens(fileNames) {
1984
2751
  const tokenCounts = /* @__PURE__ */ new Map();
1985
2752
  for (const fileName of fileNames) {
1986
- const tokens = tokenize(fileName);
2753
+ const tokens = tokenize2(fileName);
1987
2754
  if (tokens.length > 0) {
1988
2755
  const token = tokens[0];
1989
2756
  tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1);
@@ -2187,9 +2954,9 @@ function isConventionalEntryFile(filePath, ancestorFolders) {
2187
2954
  return false;
2188
2955
  }
2189
2956
  const hookName = fileStem.slice(3);
2190
- const hookTokens = tokenize(hookName);
2957
+ const hookTokens = tokenize2(hookName);
2191
2958
  return hookTokens.some((hookToken) => ancestorFolders.some((folder) => {
2192
- const folderTokens = tokenize(folder);
2959
+ const folderTokens = tokenize2(folder);
2193
2960
  return folderTokens.includes(hookToken);
2194
2961
  }));
2195
2962
  }
@@ -2200,13 +2967,13 @@ function pathRedundancy(filePath, ancestorFolders) {
2200
2967
  return 0;
2201
2968
  }
2202
2969
  const fileName = basename5(filePath);
2203
- const fileTokens = tokenize(fileName);
2970
+ const fileTokens = tokenize2(fileName);
2204
2971
  if (fileTokens.length === 0) {
2205
2972
  return 0;
2206
2973
  }
2207
2974
  const ancestorTokens = /* @__PURE__ */ new Set();
2208
2975
  for (const folder of ancestorFolders) {
2209
- const folderTokens = tokenize(folder);
2976
+ const folderTokens = tokenize2(folder);
2210
2977
  for (const token of folderTokens) {
2211
2978
  ancestorTokens.add(token);
2212
2979
  }
@@ -2232,7 +2999,7 @@ function computeAverageRedundancy(fileNames, ancestorFolders) {
2232
2999
  }
2233
3000
 
2234
3001
  // src/organize/analyze/issues.ts
2235
- import { readFileSync as readFileSync8 } from "fs";
3002
+ import { readFileSync as readFileSync9 } from "fs";
2236
3003
 
2237
3004
  // src/organize/metric/naming/details.ts
2238
3005
  var LOW_INFO_NAME_DETAILS = {
@@ -2374,7 +3141,7 @@ function collectFileIssues(fileNames, directoryPath, ancestorFolders, lowInfoNam
2374
3141
  }
2375
3142
  try {
2376
3143
  const filePath = `${directoryPath}/${fileName}`;
2377
- const fileContent = readFileSync8(filePath, "utf-8");
3144
+ const fileContent = readFileSync9(filePath, "utf-8");
2378
3145
  const barrelIssue = checkBarrelFile(fileName, fileContent);
2379
3146
  if (barrelIssue) {
2380
3147
  issues.push(barrelIssue);
@@ -2427,7 +3194,7 @@ function analyze(target) {
2427
3194
  }
2428
3195
 
2429
3196
  // src/organize/compare/baseline.ts
2430
- import { readFileSync as readFileSync9 } from "fs";
3197
+ import { readFileSync as readFileSync10 } from "fs";
2431
3198
 
2432
3199
  // src/organize/compare/verdict.ts
2433
3200
  function verdictFromDeltas(fileFanOutDelta, folderFanOutDelta, clusterCountDelta, issueCountDelta, redundancyDelta) {
@@ -2463,7 +3230,7 @@ function baselineMetricsByPath(baseline2) {
2463
3230
  return new Map(baseline2.map((metric) => [metric.directoryPath, metric]));
2464
3231
  }
2465
3232
  function compareBaseline(current, baselinePath) {
2466
- const baselineData = JSON.parse(readFileSync9(baselinePath, "utf-8"));
3233
+ const baselineData = JSON.parse(readFileSync10(baselinePath, "utf-8"));
2467
3234
  const previousByPath = baselineMetricsByPath(baselineData);
2468
3235
  const comparisons = /* @__PURE__ */ new Map();
2469
3236
  for (const metric of current) {
@@ -2754,11 +3521,11 @@ function runReachabilityCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES4) {
2754
3521
  }
2755
3522
 
2756
3523
  // src/scrap/analysis/pipeline/run.ts
2757
- import * as fs from "fs";
3524
+ import * as fs2 from "fs";
2758
3525
  import * as ts25 from "typescript";
2759
3526
 
2760
3527
  // src/scrap/test/discovery/files.ts
2761
- import { readFileSync as readFileSync10 } from "fs";
3528
+ import { readFileSync as readFileSync11 } from "fs";
2762
3529
 
2763
3530
  // src/scrap/test/discovery/globs.ts
2764
3531
  import { globSync as globSync2 } from "glob";
@@ -2866,7 +3633,7 @@ function discoverTestFiles(target) {
2866
3633
  return packageNamesForTarget(target, REPO_ROOT).flatMap((packageName) => discoverPackageTestFiles(packageName, REPO_ROOT)).filter((filePath) => isInsideTarget(target, REPO_ROOT, filePath));
2867
3634
  }
2868
3635
  function readBaselineMetrics(baselinePath) {
2869
- return JSON.parse(readFileSync10(baselinePath, "utf-8"));
3636
+ return JSON.parse(readFileSync11(baselinePath, "utf-8"));
2870
3637
  }
2871
3638
  function baselineMetricsByPath2(baseline2) {
2872
3639
  return new Map(
@@ -4100,8 +4867,8 @@ function compareBlockSummaries(left, right) {
4100
4867
 
4101
4868
  // src/scrap/structure/blocks/groups.ts
4102
4869
  var BLOCK_SEPARATOR = "";
4103
- function blockPathKey(path3) {
4104
- return path3.join(BLOCK_SEPARATOR);
4870
+ function blockPathKey(path4) {
4871
+ return path4.join(BLOCK_SEPARATOR);
4105
4872
  }
4106
4873
  function blockPathFromKey(key) {
4107
4874
  return key.split(BLOCK_SEPARATOR);
@@ -4127,7 +4894,7 @@ function averageScore2(examples) {
4127
4894
  function countExamples2(examples, predicate) {
4128
4895
  return examples.filter(predicate).length;
4129
4896
  }
4130
- function summarizeBlock(path3, examples) {
4897
+ function summarizeBlock(path4, examples) {
4131
4898
  const meanScore = averageScore2(examples);
4132
4899
  const maxScore2 = examples.reduce((max, example) => Math.max(max, example.score), 0);
4133
4900
  const hotExampleCount2 = countExamples2(examples, (example) => example.score >= 8);
@@ -4142,8 +4909,8 @@ function summarizeBlock(path3, examples) {
4142
4909
  hotExampleCount: hotExampleCount2,
4143
4910
  lowAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount <= 1),
4144
4911
  maxScore: maxScore2,
4145
- name: path3[path3.length - 1],
4146
- path: path3,
4912
+ name: path4[path4.length - 1],
4913
+ path: path4,
4147
4914
  remediationMode: remediationMode(examples.length, meanScore, hotExampleCount2, maxScore2),
4148
4915
  zeroAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount === 0)
4149
4916
  };
@@ -4521,7 +5288,7 @@ function analyzeScrapFile(sourceFile) {
4521
5288
  // src/scrap/analysis/pipeline/run.ts
4522
5289
  function analyzeScrap(target) {
4523
5290
  return discoverTestFiles(target).map((filePath) => {
4524
- const source = fs.readFileSync(filePath, "utf-8");
5291
+ const source = fs2.readFileSync(filePath, "utf-8");
4525
5292
  const sourceFile = ts25.createSourceFile(
4526
5293
  filePath,
4527
5294
  source,
@@ -4677,8 +5444,8 @@ function resolveScrapPolicy(args2) {
4677
5444
  }
4678
5445
 
4679
5446
  // src/scrap/report/blocks/format.ts
4680
- function formatBlockPath(path3) {
4681
- return path3.join(" > ");
5447
+ function formatBlockPath(path4) {
5448
+ return path4.join(" > ");
4682
5449
  }
4683
5450
  function interestingBlocks(metric) {
4684
5451
  return metric.blockSummaries.filter((block) => block.remediationMode !== "STABLE").slice(0, 5);
@@ -4896,6 +5663,7 @@ function runScrapCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES5) {
4896
5663
 
4897
5664
  // src/cli/main.ts
4898
5665
  var COMMANDS = {
5666
+ acceptance: runAcceptanceCli,
4899
5667
  boundaries: runBoundariesCli,
4900
5668
  crap: runCrapCli,
4901
5669
  init: runInitCli,
@@ -4909,6 +5677,7 @@ function printHelp() {
4909
5677
 
4910
5678
  Commands:
4911
5679
  init Create a starter quality.config.json
5680
+ acceptance Compile human-authored acceptance specs into executable tests
4912
5681
  organize Check folder structure, naming, and cohesion
4913
5682
  boundaries Check package/layer boundaries
4914
5683
  reachability Check dead surfaces and dead ends