@poleski/quality-tools 0.2.0 → 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
@@ -5,149 +5,544 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { glob } from "glob";
7
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
+
8
315
  // src/acceptance/parser.ts
9
316
  var FEATURE_PATTERN = /^#{0,6}\s*Feature:\s*(.+)$/;
317
+ var BACKGROUND_PATTERN = /^#{0,6}\s*Background:\s*$/;
10
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*$/;
11
321
  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) => {
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) => {
17
346
  const lineNumber = index + 1;
18
347
  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
- });
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)) {
37
357
  return;
38
358
  }
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
- }
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] ?? ""]))
51
436
  });
52
- if (!feature) {
53
- throw new Error(`${sourcePath}: Expected a Feature heading`);
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`);
54
443
  }
55
- if (scenarios.length === 0) {
56
- throw new Error(`${sourcePath}: Expected at least one Scenario`);
444
+ }
445
+ function parseStepLine(state, line, lineNumber) {
446
+ const stepMatch = STEP_PATTERN.exec(line);
447
+ if (!stepMatch) {
448
+ return false;
57
449
  }
58
- const emptyScenario = scenarios.find((scenario) => scenario.steps.length === 0);
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);
59
494
  if (emptyScenario) {
60
- throw new Error(`${sourcePath}:${emptyScenario.line} Scenario "${emptyScenario.name}" must contain at least one step`);
495
+ throw new Error(`${state.sourcePath}:${emptyScenario.line} Scenario "${emptyScenario.name}" must contain at least one step`);
61
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) {
62
507
  return {
63
- sourcePath,
64
- feature,
65
- scenarios
508
+ sourcePath: state.sourcePath,
509
+ feature: state.feature,
510
+ ...state.background ? { background: state.background } : {},
511
+ scenarios: state.scenarios
66
512
  };
67
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
+ }
68
520
 
69
521
  // src/acceptance/playwright/generator.ts
70
- function generatePlaywrightAcceptanceSpec(documents, options) {
71
- const sections = documents.flatMap((document) => generateDocumentSections(document));
522
+ import { readFileSync } from "node:fs";
523
+ var runtimeTemplateUrl = new URL("./templates/playwright-acceptance-runtime.ts.template", import.meta.url);
524
+ function generatePlaywrightAcceptanceSpec(options) {
72
525
  return [
73
- "/* Generated by quality-tools acceptance compile. Do not edit. */",
74
- "/* eslint-disable playwright/expect-expect */",
526
+ "/* Generated by quality-tools acceptance generate. Do not edit. */",
75
527
  "import { test } from '@playwright/test';",
76
528
  `import { acceptanceSteps, createAcceptanceContext } from ${quote(options.stepsImportPath)};`,
529
+ `import { loadAcceptanceIr, runAcceptanceFeature } from ${quote(options.runtimeImportPath)};`,
77
530
  "",
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}`];",
531
+ `const feature = loadAcceptanceIr(${quote(options.irImportPath)});`,
90
532
  "",
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),
533
+ "runAcceptanceFeature(test, feature, {",
534
+ " acceptanceSteps,",
535
+ " createAcceptanceContext",
107
536
  "});",
108
537
  ""
109
- ];
538
+ ].join("\n");
110
539
  }
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
- ];
540
+ function generatePlaywrightAcceptanceRuntime() {
541
+ return readFileSync(runtimeTemplateUrl, "utf8");
143
542
  }
144
543
  function quote(value) {
145
544
  return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r/g, "\\r").replace(/\n/g, "\\n")}'`;
146
545
  }
147
- function indentLines(lines, spaces) {
148
- const prefix = " ".repeat(spaces);
149
- return lines.map((line) => line === "" ? line : `${prefix}${line}`);
150
- }
151
546
 
152
547
  // src/shared/flagValue.ts
153
548
  function flagValue(args2, name) {
@@ -181,51 +576,191 @@ function cleanCliArgs(args2) {
181
576
  }
182
577
 
183
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
+ ];
184
594
  async function runAcceptanceCli(rawArgs, options = {}) {
185
595
  const args2 = cleanCliArgs(rawArgs);
186
596
  const [command2, ...commandArgs] = args2;
187
- if (command2 !== "compile") {
188
- throw new Error("Usage: quality-tools acceptance compile --spec <glob> --steps <path> --out <path>");
597
+ const cwd = options.cwd ?? process.cwd();
598
+ if (isHelpCommand(command2)) {
599
+ console.log(acceptanceUsage());
600
+ return;
189
601
  }
190
- await compileAcceptance(commandArgs, options.cwd ?? process.cwd());
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");
191
612
  }
192
- async function compileAcceptance(args2, cwd) {
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) {
193
634
  const options = parseCompileOptions(args2);
194
635
  const specFiles = await findSpecFiles(cwd, options.specPatterns);
195
636
  if (specFiles.length === 0) {
196
637
  throw new Error(`No acceptance specs matched: ${options.specPatterns.join(", ")}`);
197
638
  }
198
- const documents = specFiles.map((specFile) => {
199
- const source = fs.readFileSync(specFile, "utf8");
200
- return parseAcceptanceMarkdown(source, toPosixPath(path.relative(cwd, specFile)));
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);
201
654
  });
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
655
  }
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>");
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>");
214
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));
215
683
  return {
216
684
  specPatterns,
217
- stepsPath,
218
- outPath
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")
219
691
  };
220
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
+ }
221
756
  async function findSpecFiles(cwd, patterns) {
222
757
  const files = await Promise.all(
223
758
  patterns.map((pattern) => glob(pattern, { absolute: true, cwd, nodir: true }))
224
759
  );
225
- return files.flat().sort((left, right) => left.localeCompare(right));
760
+ return [...new Set(files.flat())].sort((left, right) => left.localeCompare(right));
226
761
  }
227
- function createStepsImportPath(outPath, stepsPath) {
228
- const relativePath = toPosixPath(path.relative(path.dirname(outPath), stepsPath));
762
+ function createExtensionlessImportPath(outPath, targetPath) {
763
+ const relativePath = toPosixPath(path.relative(path.dirname(outPath), targetPath));
229
764
  const extension = path.extname(relativePath);
230
765
  const extensionlessPath = extension ? relativePath.slice(0, -extension.length) : relativePath;
231
766
  if (extensionlessPath.startsWith(".")) {
@@ -253,9 +788,15 @@ function requireFlagValue(args2, flag) {
253
788
  }
254
789
  return value;
255
790
  }
791
+ function isFlagValue(args2, index, flags) {
792
+ return flags.includes(args2[index - 1] ?? "");
793
+ }
256
794
  function toPosixPath(value) {
257
795
  return value.split(path.sep).join(path.posix.sep);
258
796
  }
797
+ function sourcePathSlug(sourcePath) {
798
+ return sourcePath.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
799
+ }
259
800
 
260
801
  // src/shared/resolve/repoRoot.ts
261
802
  import { resolve as resolve3 } from "node:path";
@@ -364,12 +905,12 @@ import { existsSync as existsSync4, statSync } from "fs";
364
905
  import { isAbsolute, resolve as resolve4 } from "path";
365
906
 
366
907
  // src/shared/util/workspacePackages.ts
367
- import { existsSync as existsSync3, readFileSync } from "fs";
908
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
368
909
  import { basename, dirname as dirname4, join as join3 } from "path";
369
910
  import { globSync } from "glob";
370
911
  function workspacePackageFromPackageJson(repoRoot, relativePackageJson) {
371
912
  const packageJsonPath = join3(repoRoot, relativePackageJson);
372
- const manifest = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
913
+ const manifest = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
373
914
  const relativeRoot = toPosix(dirname4(relativePackageJson));
374
915
  return {
375
916
  ...manifest.name ? { manifestName: manifest.name } : {},
@@ -383,7 +924,7 @@ function pnpmWorkspaceGlobs(repoRoot) {
383
924
  if (!existsSync3(workspacePath)) {
384
925
  return [];
385
926
  }
386
- const lines = readFileSync(workspacePath, "utf-8").split(/\r?\n/);
927
+ const lines = readFileSync2(workspacePath, "utf-8").split(/\r?\n/);
387
928
  const packageGlobs = [];
388
929
  let inPackagesBlock = false;
389
930
  for (const line of lines) {
@@ -409,7 +950,7 @@ function packageJsonWorkspaceGlobs(repoRoot) {
409
950
  if (!existsSync3(packageJsonPath)) {
410
951
  return [];
411
952
  }
412
- const manifest = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
953
+ const manifest = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
413
954
  if (Array.isArray(manifest.workspaces)) {
414
955
  return manifest.workspaces;
415
956
  }
@@ -442,7 +983,7 @@ function listWorkspacePackages(repoRoot) {
442
983
  const packages = [];
443
984
  const packageJsons = discoverWorkspacePackageJsons(repoRoot);
444
985
  if (shouldIncludeRootPackage(repoRoot, packageJsons)) {
445
- const packageJson = JSON.parse(readFileSync(join3(repoRoot, "package.json"), "utf-8"));
986
+ const packageJson = JSON.parse(readFileSync2(join3(repoRoot, "package.json"), "utf-8"));
446
987
  packages.push({
447
988
  ...packageJson.name ? { manifestName: packageJson.name } : {},
448
989
  name: packageJson.name?.split("/").pop() ?? "root",
@@ -560,7 +1101,7 @@ import { basename as basename3, join as join8 } from "path";
560
1101
  import { basename as basename2 } from "path";
561
1102
 
562
1103
  // src/organize/cohesion/imports/parse.ts
563
- import { readFileSync as readFileSync2 } from "fs";
1104
+ import { readFileSync as readFileSync3 } from "fs";
564
1105
  import * as ts2 from "typescript";
565
1106
 
566
1107
  // src/organize/cohesion/imports/scriptKind.ts
@@ -601,7 +1142,7 @@ function extractImports(sourceFile) {
601
1142
  }
602
1143
  function parseFileImports(filePath, fileName) {
603
1144
  try {
604
- const fileContent = readFileSync2(filePath, "utf-8");
1145
+ const fileContent = readFileSync3(filePath, "utf-8");
605
1146
  const scriptKind = getScriptKind(fileName);
606
1147
  const sourceFile = ts2.createSourceFile(
607
1148
  fileName,
@@ -638,7 +1179,7 @@ function resolveImportTarget(fromFile, specifier, candidatePaths) {
638
1179
  }
639
1180
 
640
1181
  // src/config/quality.ts
641
- import { readFileSync as readFileSync3 } from "fs";
1182
+ import { readFileSync as readFileSync4 } from "fs";
642
1183
  import { isAbsolute as isAbsolute2, join as join6, matchesGlob, relative as relative2, resolve as resolve5 } from "path";
643
1184
 
644
1185
  // src/config/patterns.ts
@@ -676,7 +1217,7 @@ var DEFAULT_REPORTS_DIR = "reports/quality-tools";
676
1217
  function loadQualityConfig(repoRoot) {
677
1218
  const configPath = join6(repoRoot, CONFIG_FILE);
678
1219
  try {
679
- return JSON.parse(readFileSync3(configPath, "utf-8"));
1220
+ return JSON.parse(readFileSync4(configPath, "utf-8"));
680
1221
  } catch {
681
1222
  return {};
682
1223
  }
@@ -1223,7 +1764,7 @@ function extractFunctions(sourceFile) {
1223
1764
  }
1224
1765
 
1225
1766
  // src/crap/analysis/fileSelection.ts
1226
- import { readFileSync as readFileSync4 } from "fs";
1767
+ import { readFileSync as readFileSync5 } from "fs";
1227
1768
  import * as path2 from "path";
1228
1769
  import * as ts7 from "typescript";
1229
1770
  function matchesFilterScope(relativePath, filterScope) {
@@ -1254,7 +1795,7 @@ function shouldIncludeFile(filePath, filterScope, repoRoot) {
1254
1795
  function createSourceFile3(filePath) {
1255
1796
  return ts7.createSourceFile(
1256
1797
  filePath,
1257
- readFileSync4(filePath, "utf-8"),
1798
+ readFileSync5(filePath, "utf-8"),
1258
1799
  ts7.ScriptTarget.Latest,
1259
1800
  true,
1260
1801
  filePath.endsWith(".tsx") ? ts7.ScriptKind.TSX : ts7.ScriptKind.TS
@@ -1406,12 +1947,12 @@ function createCoverageProfiles(repoRoot, target) {
1406
1947
  }
1407
1948
 
1408
1949
  // src/crap/coverage/read.ts
1409
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
1950
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1410
1951
  function readCoverageReport(path4) {
1411
1952
  if (!existsSync7(path4)) {
1412
1953
  throw new Error(`Coverage data not found: ${path4}`);
1413
1954
  }
1414
- return JSON.parse(readFileSync5(path4, "utf-8"));
1955
+ return JSON.parse(readFileSync6(path4, "utf-8"));
1415
1956
  }
1416
1957
 
1417
1958
  // src/crap/report.ts
@@ -1592,9 +2133,9 @@ function copySharedMutationReports(reportKey, repoRoot = process.cwd()) {
1592
2133
  }
1593
2134
 
1594
2135
  // src/mutation/reporting/check.ts
1595
- import { readFileSync as readFileSync6 } from "fs";
2136
+ import { readFileSync as readFileSync7 } from "fs";
1596
2137
  function findMutationSiteViolations(reportPath, threshold = 50) {
1597
- const report = JSON.parse(readFileSync6(reportPath, "utf-8"));
2138
+ const report = JSON.parse(readFileSync7(reportPath, "utf-8"));
1598
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);
1599
2140
  }
1600
2141
  function reportMutationSiteViolations(reportPath, threshold = 50) {
@@ -1952,7 +2493,7 @@ import { join as join16 } from "path";
1952
2493
  import { relative as relative7 } from "path";
1953
2494
 
1954
2495
  // src/organize/rules.ts
1955
- import { readFileSync as readFileSync7 } from "fs";
2496
+ import { readFileSync as readFileSync8 } from "fs";
1956
2497
  import { join as join14 } from "path";
1957
2498
  var DEFAULT_CONFIG2 = {
1958
2499
  lowInfoNames: {
@@ -1979,7 +2520,7 @@ function mergeConfig(defaults, overrides) {
1979
2520
  function loadOrganizeConfig(repoRoot, packageName) {
1980
2521
  const configPath = join14(repoRoot, CONFIG_FILE3);
1981
2522
  try {
1982
- const rawConfig = JSON.parse(readFileSync7(configPath, "utf-8"));
2523
+ const rawConfig = JSON.parse(readFileSync8(configPath, "utf-8"));
1983
2524
  const defaultConfig = rawConfig.defaults?.organize;
1984
2525
  const packageConfig = packageName ? rawConfig.packages?.[packageName]?.organize : void 0;
1985
2526
  const mergedDefaults = defaultConfig ? mergeConfig(DEFAULT_CONFIG2, defaultConfig) : DEFAULT_CONFIG2;
@@ -2164,7 +2705,7 @@ function shouldStartNewToken(previous, current, next) {
2164
2705
  }
2165
2706
 
2166
2707
  // src/organize/naming/tokenize.ts
2167
- function tokenize(name) {
2708
+ function tokenize2(name) {
2168
2709
  const withoutExtension = stripExtension(name);
2169
2710
  const characters = Array.from(withoutExtension);
2170
2711
  const tokens = [];
@@ -2195,7 +2736,7 @@ function tokenize(name) {
2195
2736
  function buildPrefixGroups(fileNames) {
2196
2737
  const groups = /* @__PURE__ */ new Map();
2197
2738
  for (const fileName of fileNames) {
2198
- const tokens = tokenize(fileName);
2739
+ const tokens = tokenize2(fileName);
2199
2740
  if (tokens.length > 0) {
2200
2741
  const prefix = tokens[0];
2201
2742
  if (!groups.has(prefix)) {
@@ -2209,7 +2750,7 @@ function buildPrefixGroups(fileNames) {
2209
2750
  function countFirstTokens(fileNames) {
2210
2751
  const tokenCounts = /* @__PURE__ */ new Map();
2211
2752
  for (const fileName of fileNames) {
2212
- const tokens = tokenize(fileName);
2753
+ const tokens = tokenize2(fileName);
2213
2754
  if (tokens.length > 0) {
2214
2755
  const token = tokens[0];
2215
2756
  tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1);
@@ -2413,9 +2954,9 @@ function isConventionalEntryFile(filePath, ancestorFolders) {
2413
2954
  return false;
2414
2955
  }
2415
2956
  const hookName = fileStem.slice(3);
2416
- const hookTokens = tokenize(hookName);
2957
+ const hookTokens = tokenize2(hookName);
2417
2958
  return hookTokens.some((hookToken) => ancestorFolders.some((folder) => {
2418
- const folderTokens = tokenize(folder);
2959
+ const folderTokens = tokenize2(folder);
2419
2960
  return folderTokens.includes(hookToken);
2420
2961
  }));
2421
2962
  }
@@ -2426,13 +2967,13 @@ function pathRedundancy(filePath, ancestorFolders) {
2426
2967
  return 0;
2427
2968
  }
2428
2969
  const fileName = basename5(filePath);
2429
- const fileTokens = tokenize(fileName);
2970
+ const fileTokens = tokenize2(fileName);
2430
2971
  if (fileTokens.length === 0) {
2431
2972
  return 0;
2432
2973
  }
2433
2974
  const ancestorTokens = /* @__PURE__ */ new Set();
2434
2975
  for (const folder of ancestorFolders) {
2435
- const folderTokens = tokenize(folder);
2976
+ const folderTokens = tokenize2(folder);
2436
2977
  for (const token of folderTokens) {
2437
2978
  ancestorTokens.add(token);
2438
2979
  }
@@ -2458,7 +2999,7 @@ function computeAverageRedundancy(fileNames, ancestorFolders) {
2458
2999
  }
2459
3000
 
2460
3001
  // src/organize/analyze/issues.ts
2461
- import { readFileSync as readFileSync8 } from "fs";
3002
+ import { readFileSync as readFileSync9 } from "fs";
2462
3003
 
2463
3004
  // src/organize/metric/naming/details.ts
2464
3005
  var LOW_INFO_NAME_DETAILS = {
@@ -2600,7 +3141,7 @@ function collectFileIssues(fileNames, directoryPath, ancestorFolders, lowInfoNam
2600
3141
  }
2601
3142
  try {
2602
3143
  const filePath = `${directoryPath}/${fileName}`;
2603
- const fileContent = readFileSync8(filePath, "utf-8");
3144
+ const fileContent = readFileSync9(filePath, "utf-8");
2604
3145
  const barrelIssue = checkBarrelFile(fileName, fileContent);
2605
3146
  if (barrelIssue) {
2606
3147
  issues.push(barrelIssue);
@@ -2653,7 +3194,7 @@ function analyze(target) {
2653
3194
  }
2654
3195
 
2655
3196
  // src/organize/compare/baseline.ts
2656
- import { readFileSync as readFileSync9 } from "fs";
3197
+ import { readFileSync as readFileSync10 } from "fs";
2657
3198
 
2658
3199
  // src/organize/compare/verdict.ts
2659
3200
  function verdictFromDeltas(fileFanOutDelta, folderFanOutDelta, clusterCountDelta, issueCountDelta, redundancyDelta) {
@@ -2689,7 +3230,7 @@ function baselineMetricsByPath(baseline2) {
2689
3230
  return new Map(baseline2.map((metric) => [metric.directoryPath, metric]));
2690
3231
  }
2691
3232
  function compareBaseline(current, baselinePath) {
2692
- const baselineData = JSON.parse(readFileSync9(baselinePath, "utf-8"));
3233
+ const baselineData = JSON.parse(readFileSync10(baselinePath, "utf-8"));
2693
3234
  const previousByPath = baselineMetricsByPath(baselineData);
2694
3235
  const comparisons = /* @__PURE__ */ new Map();
2695
3236
  for (const metric of current) {
@@ -2984,7 +3525,7 @@ import * as fs2 from "fs";
2984
3525
  import * as ts25 from "typescript";
2985
3526
 
2986
3527
  // src/scrap/test/discovery/files.ts
2987
- import { readFileSync as readFileSync10 } from "fs";
3528
+ import { readFileSync as readFileSync11 } from "fs";
2988
3529
 
2989
3530
  // src/scrap/test/discovery/globs.ts
2990
3531
  import { globSync as globSync2 } from "glob";
@@ -3092,7 +3633,7 @@ function discoverTestFiles(target) {
3092
3633
  return packageNamesForTarget(target, REPO_ROOT).flatMap((packageName) => discoverPackageTestFiles(packageName, REPO_ROOT)).filter((filePath) => isInsideTarget(target, REPO_ROOT, filePath));
3093
3634
  }
3094
3635
  function readBaselineMetrics(baselinePath) {
3095
- return JSON.parse(readFileSync10(baselinePath, "utf-8"));
3636
+ return JSON.parse(readFileSync11(baselinePath, "utf-8"));
3096
3637
  }
3097
3638
  function baselineMetricsByPath2(baseline2) {
3098
3639
  return new Map(