@howlil/ez-agents 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +84 -20
  3. package/agents/ez-observer-agent.md +260 -0
  4. package/agents/ez-release-agent.md +333 -0
  5. package/agents/ez-requirements-agent.md +377 -0
  6. package/agents/ez-scrum-master-agent.md +242 -0
  7. package/agents/ez-tech-lead-agent.md +267 -0
  8. package/bin/install.js +3221 -3230
  9. package/commands/ez/arch-review.md +102 -0
  10. package/commands/ez/execute-phase.md +11 -0
  11. package/commands/ez/export-session.md +79 -0
  12. package/commands/ez/gather-requirements.md +117 -0
  13. package/commands/ez/git-workflow.md +72 -0
  14. package/commands/ez/hotfix.md +120 -0
  15. package/commands/ez/import-session.md +82 -0
  16. package/commands/ez/join-discord.md +18 -18
  17. package/commands/ez/list-sessions.md +96 -0
  18. package/commands/ez/package-manager.md +316 -0
  19. package/commands/ez/plan-phase.md +9 -1
  20. package/commands/ez/preflight.md +79 -0
  21. package/commands/ez/progress.md +13 -1
  22. package/commands/ez/release.md +153 -0
  23. package/commands/ez/resume.md +107 -0
  24. package/commands/ez/standup.md +85 -0
  25. package/ez-agents/bin/ez-tools.cjs +1095 -716
  26. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  27. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  28. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  29. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  30. package/ez-agents/bin/lib/config.cjs +190 -190
  31. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  32. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  33. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  34. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  35. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  36. package/ez-agents/bin/lib/file-access.cjs +207 -0
  37. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  38. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  39. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  40. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  41. package/ez-agents/bin/lib/git-utils.cjs +118 -0
  42. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  43. package/ez-agents/bin/lib/index.cjs +157 -113
  44. package/ez-agents/bin/lib/init.cjs +757 -757
  45. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  46. package/ez-agents/bin/lib/logger.cjs +124 -124
  47. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  48. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  49. package/ez-agents/bin/lib/milestone.cjs +241 -241
  50. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  51. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  52. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  53. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  54. package/ez-agents/bin/lib/phase.cjs +925 -925
  55. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  56. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  57. package/ez-agents/bin/lib/retry.cjs +119 -119
  58. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  59. package/ez-agents/bin/lib/safe-exec.cjs +128 -128
  60. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  61. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  62. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  63. package/ez-agents/bin/lib/session-export.cjs +251 -0
  64. package/ez-agents/bin/lib/session-import.cjs +262 -0
  65. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  66. package/ez-agents/bin/lib/state.cjs +736 -736
  67. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  68. package/ez-agents/bin/lib/template.cjs +223 -223
  69. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  70. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  71. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  72. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  73. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  74. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  75. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  76. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  77. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  78. package/ez-agents/bin/lib/verify.cjs +15 -1
  79. package/ez-agents/references/checkpoints.md +776 -776
  80. package/ez-agents/references/continuation-format.md +249 -249
  81. package/ez-agents/references/metrics-schema.md +118 -0
  82. package/ez-agents/references/planning-config.md +140 -0
  83. package/ez-agents/references/questioning.md +162 -162
  84. package/ez-agents/references/tdd.md +263 -263
  85. package/ez-agents/references/tier-strategy.md +103 -0
  86. package/ez-agents/templates/bdd-feature.md +173 -0
  87. package/ez-agents/templates/codebase/concerns.md +310 -310
  88. package/ez-agents/templates/codebase/conventions.md +307 -307
  89. package/ez-agents/templates/codebase/integrations.md +280 -280
  90. package/ez-agents/templates/codebase/stack.md +186 -186
  91. package/ez-agents/templates/codebase/testing.md +480 -480
  92. package/ez-agents/templates/config.json +37 -37
  93. package/ez-agents/templates/continue-here.md +78 -78
  94. package/ez-agents/templates/discussion.md +68 -0
  95. package/ez-agents/templates/incident-runbook.md +205 -0
  96. package/ez-agents/templates/milestone-archive.md +123 -123
  97. package/ez-agents/templates/milestone.md +115 -115
  98. package/ez-agents/templates/release-checklist.md +133 -0
  99. package/ez-agents/templates/requirements.md +231 -231
  100. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  101. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  102. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  103. package/ez-agents/templates/research-project/STACK.md +120 -120
  104. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  105. package/ez-agents/templates/retrospective.md +54 -54
  106. package/ez-agents/templates/roadmap.md +202 -202
  107. package/ez-agents/templates/rollback-plan.md +201 -0
  108. package/ez-agents/templates/summary-minimal.md +41 -41
  109. package/ez-agents/templates/summary-standard.md +48 -48
  110. package/ez-agents/templates/summary.md +248 -248
  111. package/ez-agents/templates/user-setup.md +311 -311
  112. package/ez-agents/templates/verification-report.md +322 -322
  113. package/ez-agents/workflows/add-phase.md +112 -112
  114. package/ez-agents/workflows/add-tests.md +351 -351
  115. package/ez-agents/workflows/add-todo.md +158 -158
  116. package/ez-agents/workflows/arch-review.md +54 -0
  117. package/ez-agents/workflows/audit-milestone.md +332 -332
  118. package/ez-agents/workflows/autonomous.md +131 -30
  119. package/ez-agents/workflows/check-todos.md +177 -177
  120. package/ez-agents/workflows/cleanup.md +152 -152
  121. package/ez-agents/workflows/complete-milestone.md +766 -766
  122. package/ez-agents/workflows/diagnose-issues.md +219 -219
  123. package/ez-agents/workflows/discovery-phase.md +289 -289
  124. package/ez-agents/workflows/discuss-phase.md +762 -762
  125. package/ez-agents/workflows/execute-phase.md +513 -468
  126. package/ez-agents/workflows/execute-plan.md +483 -483
  127. package/ez-agents/workflows/export-session.md +255 -0
  128. package/ez-agents/workflows/gather-requirements.md +206 -0
  129. package/ez-agents/workflows/health.md +159 -159
  130. package/ez-agents/workflows/help.md +584 -492
  131. package/ez-agents/workflows/hotfix.md +291 -0
  132. package/ez-agents/workflows/import-session.md +303 -0
  133. package/ez-agents/workflows/insert-phase.md +130 -130
  134. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  135. package/ez-agents/workflows/map-codebase.md +316 -316
  136. package/ez-agents/workflows/new-milestone.md +339 -10
  137. package/ez-agents/workflows/new-project.md +293 -299
  138. package/ez-agents/workflows/node-repair.md +92 -92
  139. package/ez-agents/workflows/pause-work.md +122 -122
  140. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  141. package/ez-agents/workflows/plan-phase.md +673 -651
  142. package/ez-agents/workflows/progress.md +372 -382
  143. package/ez-agents/workflows/quick.md +610 -610
  144. package/ez-agents/workflows/release.md +253 -0
  145. package/ez-agents/workflows/remove-phase.md +155 -155
  146. package/ez-agents/workflows/research-phase.md +74 -74
  147. package/ez-agents/workflows/resume-project.md +307 -307
  148. package/ez-agents/workflows/resume-session.md +215 -0
  149. package/ez-agents/workflows/set-profile.md +81 -81
  150. package/ez-agents/workflows/settings.md +242 -242
  151. package/ez-agents/workflows/standup.md +64 -0
  152. package/ez-agents/workflows/stats.md +57 -57
  153. package/ez-agents/workflows/transition.md +544 -544
  154. package/ez-agents/workflows/ui-phase.md +290 -290
  155. package/ez-agents/workflows/ui-review.md +157 -157
  156. package/ez-agents/workflows/update.md +320 -320
  157. package/ez-agents/workflows/validate-phase.md +167 -167
  158. package/ez-agents/workflows/verify-phase.md +243 -243
  159. package/ez-agents/workflows/verify-work.md +584 -584
  160. package/package.json +10 -4
  161. package/scripts/build-hooks.js +43 -43
  162. package/scripts/run-tests.cjs +29 -29
@@ -0,0 +1,622 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * BDD Validator — INVEST criteria checker and MoSCoW tagging utilities
5
+ *
6
+ * Validates Gherkin .feature files against:
7
+ * - INVEST criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable)
8
+ * - MoSCoW priority tagging (@must/@should/@could/@wont)
9
+ * - Tier tagging (@mvp/@medium/@enterprise)
10
+ * - Structural correctness (Given/When/Then format)
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ // ─────────────────────────────────────────────
19
+ // Traceability
20
+ // ─────────────────────────────────────────────
21
+
22
+ /**
23
+ * Generate a deterministic scenario ID using FNV-1a hash
24
+ * @param {string} featureName
25
+ * @param {string} scenarioName
26
+ * @returns {string} ID like "SC-A1B2C3"
27
+ */
28
+ function generateScenarioId(featureName, scenarioName) {
29
+ const input = `${featureName}::${scenarioName}`.toLowerCase().replace(/\s+/g, '-');
30
+ // Simple non-crypto hash (FNV-1a)
31
+ let hash = 0x811c9dc5;
32
+ for (const char of input) {
33
+ hash ^= char.charCodeAt(0);
34
+ hash = (hash * 0x01000193) >>> 0;
35
+ }
36
+ return `SC-${hash.toString(16).toUpperCase().slice(0, 6)}`;
37
+ }
38
+
39
+ // ─────────────────────────────────────────────
40
+ // Parser
41
+ // ─────────────────────────────────────────────
42
+
43
+ /**
44
+ * Parse a .feature file into structured object
45
+ * @param {string} content - Raw file content
46
+ * @returns {{ feature: object, scenarios: object[], errors: string[] }}
47
+ */
48
+ function parseFeatureFile(content) {
49
+ const lines = content.split('\n');
50
+ const result = { feature: null, scenarios: [], errors: [] };
51
+
52
+ let currentScenario = null;
53
+ let currentStep = null;
54
+ let pendingTags = [];
55
+
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i].trim();
58
+
59
+ // Skip blank lines and comments
60
+ if (!line || line.startsWith('#')) continue;
61
+
62
+ // Tags
63
+ if (line.startsWith('@')) {
64
+ const tags = line.split(/\s+/).filter(t => t.startsWith('@'));
65
+ pendingTags.push(...tags);
66
+ continue;
67
+ }
68
+
69
+ // Feature declaration
70
+ if (line.startsWith('Feature:')) {
71
+ result.feature = {
72
+ name: line.replace('Feature:', '').trim(),
73
+ tags: pendingTags.slice(),
74
+ lineNumber: i + 1
75
+ };
76
+ pendingTags = [];
77
+ continue;
78
+ }
79
+
80
+ // Background
81
+ if (line.startsWith('Background:')) {
82
+ currentScenario = { type: 'background', steps: [] };
83
+ pendingTags = [];
84
+ continue;
85
+ }
86
+
87
+ // Scenario
88
+ if (line.startsWith('Scenario:') || line.startsWith('Scenario Outline:')) {
89
+ if (currentScenario && currentScenario.type !== 'background') {
90
+ result.scenarios.push(currentScenario);
91
+ }
92
+ const scenarioName = line.replace(/^Scenario(?: Outline)?:/, '').trim();
93
+ const featureName = result.feature ? result.feature.name : 'unknown';
94
+ currentScenario = {
95
+ type: line.startsWith('Scenario Outline:') ? 'outline' : 'scenario',
96
+ name: scenarioName,
97
+ id: generateScenarioId(featureName, scenarioName),
98
+ tags: pendingTags.slice(),
99
+ steps: [],
100
+ lineNumber: i + 1
101
+ };
102
+ pendingTags = [];
103
+ currentStep = null;
104
+ continue;
105
+ }
106
+
107
+ // Steps
108
+ const stepMatch = line.match(/^(Given|When|Then|And|But)\s+(.+)$/);
109
+ if (stepMatch && currentScenario) {
110
+ const stepType = stepMatch[1];
111
+ // Resolve And/But to actual type based on previous step
112
+ let resolvedType = stepType;
113
+ if ((stepType === 'And' || stepType === 'But') && currentStep) {
114
+ resolvedType = currentStep.resolvedType;
115
+ } else if (stepType === 'And' || stepType === 'But') {
116
+ resolvedType = 'Given'; // fallback
117
+ }
118
+ currentStep = {
119
+ keyword: stepType,
120
+ resolvedType,
121
+ text: stepMatch[2],
122
+ lineNumber: i + 1
123
+ };
124
+ currentScenario.steps.push(currentStep);
125
+ continue;
126
+ }
127
+ }
128
+
129
+ // Push last scenario
130
+ if (currentScenario && currentScenario.type !== 'background') {
131
+ result.scenarios.push(currentScenario);
132
+ }
133
+
134
+ if (!result.feature) {
135
+ result.errors.push('No Feature: declaration found');
136
+ }
137
+
138
+ return result;
139
+ }
140
+
141
+ // ─────────────────────────────────────────────
142
+ // MoSCoW Validation
143
+ // ─────────────────────────────────────────────
144
+
145
+ const MOSCOW_TAGS = ['@must', '@should', '@could', '@wont'];
146
+ const TIER_TAGS = ['@mvp', '@medium', '@enterprise'];
147
+
148
+ /**
149
+ * Validate MoSCoW tags on a scenario
150
+ * @param {object} scenario
151
+ * @returns {{ valid: boolean, moscow: string|null, tier: string|null, issues: string[] }}
152
+ */
153
+ function validateMosCowTags(scenario) {
154
+ const issues = [];
155
+ const tags = scenario.tags || [];
156
+
157
+ const moscowTag = tags.find(t => MOSCOW_TAGS.includes(t));
158
+ const tierTag = tags.find(t => TIER_TAGS.includes(t));
159
+
160
+ if (!moscowTag) {
161
+ issues.push(`Scenario "${scenario.name}" missing MoSCoW tag (@must/@should/@could/@wont)`);
162
+ }
163
+
164
+ if (!tierTag && moscowTag !== '@wont') {
165
+ issues.push(`Scenario "${scenario.name}" missing tier tag (@mvp/@medium/@enterprise)`);
166
+ }
167
+
168
+ // Check consistency
169
+ if (moscowTag === '@must' && tierTag && tierTag !== '@mvp') {
170
+ issues.push(`Scenario "${scenario.name}": @must scenarios should be tagged @mvp (found ${tierTag})`);
171
+ }
172
+ if (moscowTag === '@could' && tierTag === '@mvp') {
173
+ issues.push(`Scenario "${scenario.name}": @could scenarios should not be tagged @mvp`);
174
+ }
175
+
176
+ return {
177
+ valid: issues.length === 0,
178
+ moscow: moscowTag || null,
179
+ tier: tierTag || null,
180
+ issues
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Count scenarios by MoSCoW priority
186
+ * @param {object[]} scenarios
187
+ * @returns {{ must: number, should: number, could: number, wont: number, untagged: number }}
188
+ */
189
+ function countByMosCow(scenarios) {
190
+ const counts = { must: 0, should: 0, could: 0, wont: 0, untagged: 0 };
191
+ for (const s of scenarios) {
192
+ const tag = (s.tags || []).find(t => MOSCOW_TAGS.includes(t));
193
+ if (!tag) counts.untagged++;
194
+ else counts[tag.replace('@', '')]++;
195
+ }
196
+ return counts;
197
+ }
198
+
199
+ // ─────────────────────────────────────────────
200
+ // INVEST Validation
201
+ // ─────────────────────────────────────────────
202
+
203
+ /**
204
+ * Validate INVEST criteria for a Feature + its scenarios
205
+ * @param {object} parsed - Result from parseFeatureFile
206
+ * @returns {{ score: number, max: number, dimensions: object[], passed: boolean }}
207
+ */
208
+ function validateINVEST(parsed) {
209
+ const dimensions = [];
210
+
211
+ // Independent — check for explicit dependency language
212
+ const dependencyWords = ['requires', 'depends on', 'after', 'before completing'];
213
+ const featureName = parsed.feature ? parsed.feature.name.toLowerCase() : '';
214
+ const hasDependencyLanguage = dependencyWords.some(w => featureName.includes(w));
215
+ dimensions.push({
216
+ dimension: 'Independent',
217
+ letter: 'I',
218
+ passed: !hasDependencyLanguage,
219
+ note: hasDependencyLanguage
220
+ ? 'Feature name suggests hard dependency — split or remove dependency language'
221
+ : 'No hard dependency language detected in Feature name'
222
+ });
223
+
224
+ // Negotiable — check that Then clauses don't over-specify implementation
225
+ const implementationWords = ['using react', 'via postgres', 'with redis', 'using jwt', 'via sendgrid'];
226
+ let thenClauses = [];
227
+ for (const s of parsed.scenarios) {
228
+ thenClauses.push(...s.steps.filter(st => st.resolvedType === 'Then').map(st => st.text.toLowerCase()));
229
+ }
230
+ const overSpecified = thenClauses.some(t => implementationWords.some(w => t.includes(w)));
231
+ dimensions.push({
232
+ dimension: 'Negotiable',
233
+ letter: 'N',
234
+ passed: !overSpecified,
235
+ note: overSpecified
236
+ ? 'Then clauses reference specific implementation technology — keep outcomes technology-agnostic'
237
+ : 'Then clauses describe outcomes, not implementation'
238
+ });
239
+
240
+ // Valuable — check Feature has "As a... I want... So that..." structure
241
+ const hasAsA = parsed.feature && /as a/i.test(parsed.feature.name);
242
+ dimensions.push({
243
+ dimension: 'Valuable',
244
+ letter: 'V',
245
+ passed: !!parsed.feature, // Feature declaration exists
246
+ note: parsed.feature
247
+ ? (hasAsA ? 'Feature has user-value statement' : 'Feature exists but consider adding "As a... I want... So that..."')
248
+ : 'No Feature declaration found'
249
+ });
250
+
251
+ // Estimable — check sufficient detail in steps
252
+ const avgStepsPerScenario = parsed.scenarios.length > 0
253
+ ? parsed.scenarios.reduce((sum, s) => sum + s.steps.length, 0) / parsed.scenarios.length
254
+ : 0;
255
+ const estimable = avgStepsPerScenario >= 2 && avgStepsPerScenario <= 10;
256
+ dimensions.push({
257
+ dimension: 'Estimable',
258
+ letter: 'E',
259
+ passed: estimable,
260
+ note: avgStepsPerScenario < 2
261
+ ? 'Scenarios have too few steps — add more detail for estimability'
262
+ : avgStepsPerScenario > 10
263
+ ? 'Scenarios are overly complex — split into smaller scenarios'
264
+ : `Average ${avgStepsPerScenario.toFixed(1)} steps per scenario — good estimability`
265
+ });
266
+
267
+ // Small — count @must scenarios (should be <= 8 for one phase)
268
+ const mustCount = parsed.scenarios.filter(s => (s.tags || []).includes('@must')).length;
269
+ const small = mustCount <= 8;
270
+ dimensions.push({
271
+ dimension: 'Small',
272
+ letter: 'S',
273
+ passed: small,
274
+ note: small
275
+ ? `${mustCount} @must scenarios — fits in one phase`
276
+ : `${mustCount} @must scenarios — consider splitting Feature across phases (max 8 recommended)`
277
+ });
278
+
279
+ // Testable — check all Then clauses have specific assertions
280
+ const vagueWords = ['should work', 'is correct', 'looks good', 'is happy', 'functions properly'];
281
+ const vagueThens = thenClauses.filter(t => vagueWords.some(w => t.includes(w)));
282
+ dimensions.push({
283
+ dimension: 'Testable',
284
+ letter: 'T',
285
+ passed: vagueThens.length === 0,
286
+ note: vagueThens.length === 0
287
+ ? 'All Then clauses have specific, testable assertions'
288
+ : `${vagueThens.length} vague Then clause(s) found — replace with specific assertions`
289
+ });
290
+
291
+ const score = dimensions.filter(d => d.passed).length;
292
+ return {
293
+ score,
294
+ max: dimensions.length,
295
+ dimensions,
296
+ passed: score === dimensions.length
297
+ };
298
+ }
299
+
300
+ // ─────────────────────────────────────────────
301
+ // Structural Validation
302
+ // ─────────────────────────────────────────────
303
+
304
+ /**
305
+ * Validate Given/When/Then structure of scenarios
306
+ * @param {object[]} scenarios
307
+ * @returns {{ valid: boolean, issues: string[] }}
308
+ */
309
+ function validateStructure(scenarios) {
310
+ const issues = [];
311
+
312
+ for (const scenario of scenarios) {
313
+ if (scenario.type === 'background') continue;
314
+
315
+ const steps = scenario.steps;
316
+ if (steps.length === 0) {
317
+ issues.push(`Scenario "${scenario.name}" has no steps`);
318
+ continue;
319
+ }
320
+
321
+ const hasGiven = steps.some(s => s.resolvedType === 'Given');
322
+ const hasWhen = steps.some(s => s.resolvedType === 'When');
323
+ const hasThen = steps.some(s => s.resolvedType === 'Then');
324
+
325
+ if (!hasWhen) {
326
+ issues.push(`Scenario "${scenario.name}": missing When step (the action being tested)`);
327
+ }
328
+ if (!hasThen) {
329
+ issues.push(`Scenario "${scenario.name}": missing Then step (the expected outcome)`);
330
+ }
331
+
332
+ // Check order: Given before When, When before Then
333
+ let givenIndex = steps.findLastIndex(s => s.resolvedType === 'Given');
334
+ let whenIndex = steps.findIndex(s => s.resolvedType === 'When');
335
+ let thenIndex = steps.findIndex(s => s.resolvedType === 'Then');
336
+
337
+ if (hasWhen && hasThen && whenIndex > thenIndex) {
338
+ issues.push(`Scenario "${scenario.name}": When step appears after Then step`);
339
+ }
340
+ if (hasGiven && hasWhen && givenIndex > whenIndex) {
341
+ // Only warn if Given appears much later
342
+ }
343
+ }
344
+
345
+ return { valid: issues.length === 0, issues };
346
+ }
347
+
348
+ // ─────────────────────────────────────────────
349
+ // Main Validation Entry Point
350
+ // ─────────────────────────────────────────────
351
+
352
+ /**
353
+ * Validate a single .feature file
354
+ * @param {string} filePath - Path to .feature file
355
+ * @returns {{ file: string, valid: boolean, invest: object, moscow: object, structure: object, summary: string }}
356
+ */
357
+ function validateFeatureFile(filePath) {
358
+ if (!fs.existsSync(filePath)) {
359
+ return {
360
+ file: filePath,
361
+ valid: false,
362
+ error: `File not found: ${filePath}`,
363
+ invest: null,
364
+ moscow: null,
365
+ structure: null,
366
+ summary: 'FILE_NOT_FOUND'
367
+ };
368
+ }
369
+
370
+ const content = fs.readFileSync(filePath, 'utf8');
371
+ const parsed = parseFeatureFile(content);
372
+
373
+ // Structural validation
374
+ const structure = validateStructure(parsed.scenarios);
375
+
376
+ // INVEST validation
377
+ const invest = validateINVEST(parsed);
378
+
379
+ // MoSCoW validation (per scenario)
380
+ const moscowResults = parsed.scenarios.map(s => validateMosCowTags(s));
381
+ const moscowIssues = moscowResults.flatMap(r => r.issues);
382
+ const moscow = {
383
+ valid: moscowIssues.length === 0,
384
+ issues: moscowIssues,
385
+ counts: countByMosCow(parsed.scenarios),
386
+ scenarios: moscowResults
387
+ };
388
+
389
+ const allIssues = [
390
+ ...parsed.errors,
391
+ ...structure.issues,
392
+ ...moscowIssues
393
+ ];
394
+
395
+ const valid = allIssues.length === 0 && invest.score >= 5; // Must pass at least 5/6 INVEST
396
+
397
+ return {
398
+ file: filePath,
399
+ valid,
400
+ invest,
401
+ moscow,
402
+ structure,
403
+ parseErrors: parsed.errors,
404
+ scenarioCount: parsed.scenarios.length,
405
+ summary: valid ? 'PASS' : `FAIL (${allIssues.length} issues, INVEST ${invest.score}/${invest.max})`
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Validate all .feature files in a directory
411
+ * @param {string} dirPath - Directory to scan
412
+ * @param {object} options - { outputTraceability: boolean, cwd: string }
413
+ * @returns {{ valid: boolean, files: object[], totalScenarios: number, moscowCounts: object }}
414
+ */
415
+ function validateFeatureDirectory(dirPath, options = {}) {
416
+ const featureFiles = findFeatureFiles(dirPath);
417
+
418
+ if (featureFiles.length === 0) {
419
+ return {
420
+ valid: false,
421
+ files: [],
422
+ totalScenarios: 0,
423
+ moscowCounts: { must: 0, should: 0, could: 0, wont: 0, untagged: 0 },
424
+ error: `No .feature files found in ${dirPath}`
425
+ };
426
+ }
427
+
428
+ const results = featureFiles.map(f => validateFeatureFile(f));
429
+ const totalMoscow = { must: 0, should: 0, could: 0, wont: 0, untagged: 0 };
430
+
431
+ let totalScenarios = 0;
432
+ const allScenarios = []; // for traceability matrix
433
+
434
+ for (const r of results) {
435
+ totalScenarios += r.scenarioCount || 0;
436
+ if (r.moscow && r.moscow.counts) {
437
+ for (const [key, val] of Object.entries(r.moscow.counts)) {
438
+ totalMoscow[key] = (totalMoscow[key] || 0) + val;
439
+ }
440
+ }
441
+ // Collect scenario data for traceability
442
+ if (r.moscow && r.moscow.scenarios) {
443
+ const featureBase = path.basename(r.file);
444
+ r.moscow.scenarios.forEach((s, idx) => {
445
+ allScenarios.push({
446
+ id: s.id || `SC-${idx}`,
447
+ name: s.scenario ? s.scenario.name : `Scenario ${idx + 1}`,
448
+ moscow: s.moscow || 'untagged',
449
+ file: featureBase,
450
+ status: 'not-run'
451
+ });
452
+ });
453
+ }
454
+ }
455
+
456
+ // Write traceability matrix if requested or by default when .planning/ exists
457
+ const cwd = options.cwd || process.cwd();
458
+ const planningDir = path.join(cwd, '.planning');
459
+ if (allScenarios.length > 0 && fs.existsSync(planningDir)) {
460
+ try {
461
+ const matrixLines = [
462
+ '# BDD Traceability Matrix',
463
+ '',
464
+ '| ID | Scenario | MoSCoW | Feature File | Status |',
465
+ '|-----|----------|--------|-------------|--------|'
466
+ ];
467
+ for (const s of allScenarios) {
468
+ matrixLines.push(`| ${s.id} | ${s.name} | ${s.moscow} | ${s.file} | ⬜ ${s.status} |`);
469
+ }
470
+ matrixLines.push('');
471
+ matrixLines.push(`*Generated: ${new Date().toISOString()}*`);
472
+ fs.writeFileSync(
473
+ path.join(planningDir, 'bdd-traceability.md'),
474
+ matrixLines.join('\n'),
475
+ 'utf8'
476
+ );
477
+ } catch {
478
+ // Non-fatal — traceability matrix is best-effort
479
+ }
480
+ }
481
+
482
+ return {
483
+ valid: results.every(r => r.valid),
484
+ files: results,
485
+ totalScenarios,
486
+ moscowCounts: totalMoscow
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Recursively find all .feature files
492
+ * @param {string} dirPath
493
+ * @returns {string[]}
494
+ */
495
+ function findFeatureFiles(dirPath) {
496
+ if (!fs.existsSync(dirPath)) return [];
497
+
498
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
499
+ const files = [];
500
+
501
+ for (const entry of entries) {
502
+ const fullPath = path.join(dirPath, entry.name);
503
+ if (entry.isDirectory()) {
504
+ files.push(...findFeatureFiles(fullPath));
505
+ } else if (entry.name.endsWith('.feature')) {
506
+ files.push(fullPath);
507
+ }
508
+ }
509
+
510
+ return files;
511
+ }
512
+
513
+ /**
514
+ * Format validation report as human-readable markdown
515
+ * @param {object} result - From validateFeatureDirectory or validateFeatureFile
516
+ * @returns {string}
517
+ */
518
+ function formatReport(result) {
519
+ const lines = [];
520
+
521
+ if (result.files) {
522
+ // Directory report
523
+ lines.push(`## BDD Validation Report`);
524
+ lines.push(`**Status:** ${result.valid ? '✓ PASS' : '✗ FAIL'}`);
525
+ lines.push(`**Files:** ${result.files.length} | **Scenarios:** ${result.totalScenarios}`);
526
+ lines.push('');
527
+ lines.push('### MoSCoW Distribution');
528
+ lines.push(`| Priority | Count |`);
529
+ lines.push(`|----------|-------|`);
530
+ for (const [key, val] of Object.entries(result.moscowCounts)) {
531
+ lines.push(`| @${key} | ${val} |`);
532
+ }
533
+ lines.push('');
534
+ lines.push('### File Results');
535
+ for (const f of result.files) {
536
+ const icon = f.valid ? '✓' : '✗';
537
+ lines.push(`- ${icon} \`${path.basename(f.file)}\` — ${f.summary}`);
538
+ if (!f.valid && f.parseErrors) {
539
+ for (const e of f.parseErrors) lines.push(` - **Parse Error:** ${e}`);
540
+ }
541
+ if (f.structure && f.structure.issues.length > 0) {
542
+ for (const e of f.structure.issues) lines.push(` - **Structure:** ${e}`);
543
+ }
544
+ if (f.moscow && f.moscow.issues.length > 0) {
545
+ for (const e of f.moscow.issues.slice(0, 3)) lines.push(` - **MoSCoW:** ${e}`);
546
+ }
547
+ }
548
+ } else {
549
+ // Single file report
550
+ lines.push(`## BDD Validation: ${path.basename(result.file)}`);
551
+ lines.push(`**Status:** ${result.valid ? '✓ PASS' : '✗ FAIL'}`);
552
+ lines.push(`**Scenarios:** ${result.scenarioCount}`);
553
+
554
+ if (result.invest) {
555
+ lines.push('');
556
+ lines.push('### INVEST Score');
557
+ lines.push(`**${result.invest.score}/${result.invest.max}** dimensions pass`);
558
+ for (const d of result.invest.dimensions) {
559
+ lines.push(`- ${d.passed ? '✓' : '✗'} **${d.letter}** — ${d.dimension}: ${d.note}`);
560
+ }
561
+ }
562
+ }
563
+
564
+ return lines.join('\n');
565
+ }
566
+
567
+ // ─────────────────────────────────────────────
568
+ // CLI Interface
569
+ // ─────────────────────────────────────────────
570
+
571
+ if (require.main === module) {
572
+ const args = process.argv.slice(2);
573
+ const cmd = args[0];
574
+ const target = args[1];
575
+
576
+ if (!cmd || !target) {
577
+ console.error('Usage: bdd-validator.cjs <validate-file|validate-dir|count-moscow> <path>');
578
+ process.exit(1);
579
+ }
580
+
581
+ try {
582
+ if (cmd === 'validate-file') {
583
+ const result = validateFeatureFile(target);
584
+ if (args.includes('--json')) {
585
+ console.log(JSON.stringify(result, null, 2));
586
+ } else {
587
+ console.log(formatReport(result));
588
+ process.exit(result.valid ? 0 : 1);
589
+ }
590
+ } else if (cmd === 'validate-dir') {
591
+ const result = validateFeatureDirectory(target);
592
+ if (args.includes('--json')) {
593
+ console.log(JSON.stringify(result, null, 2));
594
+ } else {
595
+ console.log(formatReport(result));
596
+ process.exit(result.valid ? 0 : 1);
597
+ }
598
+ } else if (cmd === 'count-moscow') {
599
+ const result = validateFeatureDirectory(target);
600
+ console.log(JSON.stringify(result.moscowCounts, null, 2));
601
+ } else {
602
+ console.error(`Unknown command: ${cmd}`);
603
+ process.exit(1);
604
+ }
605
+ } catch (err) {
606
+ console.error(`Error: ${err.message}`);
607
+ process.exit(1);
608
+ }
609
+ }
610
+
611
+ module.exports = {
612
+ parseFeatureFile,
613
+ validateFeatureFile,
614
+ validateFeatureDirectory,
615
+ validateMosCowTags,
616
+ validateINVEST,
617
+ validateStructure,
618
+ countByMosCow,
619
+ findFeatureFiles,
620
+ formatReport,
621
+ generateScenarioId
622
+ };