@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/CHANGELOG.md +12 -0
- package/README.md +38 -0
- package/dist/cli/main.js +819 -50
- package/dist/cli/templates/playwright-acceptance-runtime.ts.template +170 -0
- package/package.json +3 -2
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
1001
|
-
import * as
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
1184
|
-
function readCoverageReport(
|
|
1185
|
-
if (!existsSync7(
|
|
1186
|
-
throw new Error(`Coverage data not found: ${
|
|
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(
|
|
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
|
|
2136
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1370
2137
|
function findMutationSiteViolations(reportPath, threshold = 50) {
|
|
1371
|
-
const report = JSON.parse(
|
|
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
|
|
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
|
-
...
|
|
2462
|
+
...collectFlagValues2(args2, "--mutate-glob"),
|
|
1696
2463
|
...parseJsonStringArray(flagValue(args2, "--mutate-globs-json"), "--mutate-globs-json")
|
|
1697
2464
|
];
|
|
1698
2465
|
const testIncludes = [
|
|
1699
|
-
...
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2957
|
+
const hookTokens = tokenize2(hookName);
|
|
2191
2958
|
return hookTokens.some((hookToken) => ancestorFolders.some((folder) => {
|
|
2192
|
-
const folderTokens =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
4104
|
-
return
|
|
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(
|
|
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:
|
|
4146
|
-
path:
|
|
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 =
|
|
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(
|
|
4681
|
-
return
|
|
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
|