jumpstart-mode 1.1.11 → 1.1.13

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 (188) hide show
  1. package/.github/agents/jumpstart-adversary.agent.md +2 -1
  2. package/.github/agents/jumpstart-architect.agent.md +6 -7
  3. package/.github/agents/jumpstart-challenger.agent.md +2 -1
  4. package/.github/agents/jumpstart-developer.agent.md +1 -1
  5. package/.github/agents/jumpstart-devops.agent.md +2 -2
  6. package/.github/agents/jumpstart-diagram-verifier.agent.md +2 -1
  7. package/.github/agents/jumpstart-maintenance.agent.md +1 -0
  8. package/.github/agents/jumpstart-performance.agent.md +1 -0
  9. package/.github/agents/jumpstart-pm.agent.md +1 -1
  10. package/.github/agents/jumpstart-refactor.agent.md +1 -0
  11. package/.github/agents/jumpstart-requirements-extractor.agent.md +1 -0
  12. package/.github/agents/jumpstart-researcher.agent.md +1 -0
  13. package/.github/agents/jumpstart-retrospective.agent.md +1 -0
  14. package/.github/agents/jumpstart-reviewer.agent.md +2 -0
  15. package/.github/agents/jumpstart-scout.agent.md +1 -1
  16. package/.github/agents/jumpstart-scrum-master.agent.md +1 -0
  17. package/.github/agents/jumpstart-security.agent.md +2 -1
  18. package/.github/agents/jumpstart-tech-writer.agent.md +1 -0
  19. package/.github/agents/jumpstart-uiux-designer.agent.md +66 -0
  20. package/.github/workflows/quality.yml +19 -2
  21. package/.jumpstart/agents/analyst.md +38 -0
  22. package/.jumpstart/agents/architect.md +39 -1
  23. package/.jumpstart/agents/challenger.md +38 -0
  24. package/.jumpstart/agents/developer.md +41 -0
  25. package/.jumpstart/agents/pm.md +38 -0
  26. package/.jumpstart/agents/scout.md +33 -0
  27. package/.jumpstart/agents/ux-designer.md +29 -9
  28. package/.jumpstart/commands/commands.md +6 -5
  29. package/.jumpstart/config.yaml +25 -1
  30. package/.jumpstart/roadmap.md +1 -1
  31. package/.jumpstart/schemas/timeline.schema.json +1 -0
  32. package/.jumpstart/skills/README.md +1 -0
  33. package/.jumpstart/skills/quality-gates/SKILL.md +126 -0
  34. package/.jumpstart/skills/skill-creator/SKILL.md +485 -357
  35. package/.jumpstart/skills/skill-creator/agents/analyzer.md +274 -0
  36. package/.jumpstart/skills/skill-creator/agents/comparator.md +202 -0
  37. package/.jumpstart/skills/skill-creator/agents/grader.md +223 -0
  38. package/.jumpstart/skills/skill-creator/assets/eval_review.html +146 -0
  39. package/.jumpstart/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  40. package/.jumpstart/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  41. package/.jumpstart/skills/skill-creator/references/schemas.md +430 -0
  42. package/.jumpstart/skills/skill-creator/scripts/__init__.py +0 -0
  43. package/.jumpstart/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  44. package/.jumpstart/skills/skill-creator/scripts/generate_report.py +326 -0
  45. package/.jumpstart/skills/skill-creator/scripts/improve_description.py +247 -0
  46. package/.jumpstart/skills/skill-creator/scripts/package_skill.py +136 -110
  47. package/.jumpstart/skills/skill-creator/scripts/run_eval.py +310 -0
  48. package/.jumpstart/skills/skill-creator/scripts/run_loop.py +328 -0
  49. package/.jumpstart/skills/skill-creator/scripts/utils.py +47 -0
  50. package/.jumpstart/skills/ui-ux-pro-max/SKILL.md +266 -0
  51. package/.jumpstart/skills/ui-ux-pro-max/data/charts.csv +26 -0
  52. package/.jumpstart/skills/ui-ux-pro-max/data/colors.csv +97 -0
  53. package/.jumpstart/skills/ui-ux-pro-max/data/icons.csv +101 -0
  54. package/.jumpstart/skills/ui-ux-pro-max/data/landing.csv +31 -0
  55. package/.jumpstart/skills/ui-ux-pro-max/data/products.csv +97 -0
  56. package/.jumpstart/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  57. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  58. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  59. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  60. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  61. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  62. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  63. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  64. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  65. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  66. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  67. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  68. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  69. package/.jumpstart/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  70. package/.jumpstart/skills/ui-ux-pro-max/data/styles.csv +68 -0
  71. package/.jumpstart/skills/ui-ux-pro-max/data/typography.csv +58 -0
  72. package/.jumpstart/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  73. package/.jumpstart/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  74. package/.jumpstart/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  75. package/.jumpstart/skills/ui-ux-pro-max/scripts/core.py +253 -0
  76. package/.jumpstart/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  77. package/.jumpstart/skills/ui-ux-pro-max/scripts/search.py +114 -0
  78. package/.jumpstart/state/timeline.json +659 -0
  79. package/.jumpstart/templates/model-map.md +1 -1
  80. package/.jumpstart/templates/ux-design.md +3 -3
  81. package/.jumpstart/usage-log.json +74 -3
  82. package/AGENTS.md +1 -1
  83. package/README.md +64 -3
  84. package/bin/cli.js +3217 -1
  85. package/bin/headless-runner.js +62 -2
  86. package/bin/lib/agent-checkpoint.js +168 -0
  87. package/bin/lib/ai-evaluation.js +104 -0
  88. package/bin/lib/ai-intake.js +152 -0
  89. package/bin/lib/ambiguity-heatmap.js +152 -0
  90. package/bin/lib/artifact-comparison.js +104 -0
  91. package/bin/lib/ast-edit-engine.js +157 -0
  92. package/bin/lib/backlog-sync.js +338 -0
  93. package/bin/lib/bcdr-planning.js +158 -0
  94. package/bin/lib/bidirectional-trace.js +199 -0
  95. package/bin/lib/branch-workflow.js +266 -0
  96. package/bin/lib/cab-output.js +119 -0
  97. package/bin/lib/chat-integration.js +122 -0
  98. package/bin/lib/ci-cd-integration.js +208 -0
  99. package/bin/lib/codebase-retrieval.js +125 -0
  100. package/bin/lib/collaboration.js +168 -0
  101. package/bin/lib/compliance-packs.js +213 -0
  102. package/bin/lib/context-chunker.js +128 -0
  103. package/bin/lib/context-onboarding.js +122 -0
  104. package/bin/lib/contract-first.js +124 -0
  105. package/bin/lib/cost-router.js +148 -0
  106. package/bin/lib/credential-boundary.js +155 -0
  107. package/bin/lib/data-classification.js +180 -0
  108. package/bin/lib/data-contracts.js +129 -0
  109. package/bin/lib/db-evolution.js +158 -0
  110. package/bin/lib/decision-conflicts.js +299 -0
  111. package/bin/lib/delivery-confidence.js +361 -0
  112. package/bin/lib/dependency-upgrade.js +153 -0
  113. package/bin/lib/design-system.js +133 -0
  114. package/bin/lib/deterministic-artifacts.js +151 -0
  115. package/bin/lib/diagram-studio.js +115 -0
  116. package/bin/lib/domain-ontology.js +140 -0
  117. package/bin/lib/ea-review-packet.js +151 -0
  118. package/bin/lib/enterprise-search.js +123 -0
  119. package/bin/lib/enterprise-templates.js +140 -0
  120. package/bin/lib/environment-promotion.js +220 -0
  121. package/bin/lib/estimation-studio.js +130 -0
  122. package/bin/lib/event-modeling.js +133 -0
  123. package/bin/lib/evidence-collector.js +179 -0
  124. package/bin/lib/finops-planner.js +182 -0
  125. package/bin/lib/fitness-functions.js +279 -0
  126. package/bin/lib/focus.js +448 -0
  127. package/bin/lib/governance-dashboard.js +165 -0
  128. package/bin/lib/guided-handoff.js +120 -0
  129. package/bin/lib/impact-analysis.js +190 -0
  130. package/bin/lib/incident-feedback.js +157 -0
  131. package/bin/lib/integrate.js +1 -1
  132. package/bin/lib/knowledge-graph.js +122 -0
  133. package/bin/lib/legacy-modernizer.js +160 -0
  134. package/bin/lib/migration-planner.js +144 -0
  135. package/bin/lib/model-governance.js +185 -0
  136. package/bin/lib/model-router.js +144 -0
  137. package/bin/lib/multi-repo.js +272 -0
  138. package/bin/lib/next-phase.js +53 -8
  139. package/bin/lib/ops-ownership.js +152 -0
  140. package/bin/lib/parallel-agents.js +257 -0
  141. package/bin/lib/pattern-library.js +115 -0
  142. package/bin/lib/persona-packs.js +99 -0
  143. package/bin/lib/plan-executor.js +366 -0
  144. package/bin/lib/platform-engineering.js +119 -0
  145. package/bin/lib/playback-summaries.js +126 -0
  146. package/bin/lib/policy-engine.js +240 -0
  147. package/bin/lib/portfolio-reporting.js +357 -0
  148. package/bin/lib/pr-package.js +197 -0
  149. package/bin/lib/project-memory.js +235 -0
  150. package/bin/lib/prompt-governance.js +130 -0
  151. package/bin/lib/promptless-mode.js +128 -0
  152. package/bin/lib/quality-graph.js +193 -0
  153. package/bin/lib/raci-matrix.js +188 -0
  154. package/bin/lib/refactor-planner.js +167 -0
  155. package/bin/lib/reference-architectures.js +304 -0
  156. package/bin/lib/release-readiness.js +171 -0
  157. package/bin/lib/repo-graph.js +262 -0
  158. package/bin/lib/requirements-baseline.js +358 -0
  159. package/bin/lib/risk-register.js +211 -0
  160. package/bin/lib/role-approval.js +249 -0
  161. package/bin/lib/role-views.js +142 -0
  162. package/bin/lib/root-cause-analysis.js +132 -0
  163. package/bin/lib/runtime-debugger.js +154 -0
  164. package/bin/lib/safe-rename.js +135 -0
  165. package/bin/lib/secret-scanner.js +313 -0
  166. package/bin/lib/semantic-diff.js +335 -0
  167. package/bin/lib/sla-slo.js +210 -0
  168. package/bin/lib/smoke-tester.js +344 -0
  169. package/bin/lib/spec-comments.js +147 -0
  170. package/bin/lib/spec-maturity.js +287 -0
  171. package/bin/lib/sre-integration.js +154 -0
  172. package/bin/lib/structured-elicitation.js +174 -0
  173. package/bin/lib/telemetry-feedback.js +118 -0
  174. package/bin/lib/test-generator.js +146 -0
  175. package/bin/lib/timeline.js +2 -1
  176. package/bin/lib/tool-bridge.js +159 -0
  177. package/bin/lib/tool-guardrails.js +139 -0
  178. package/bin/lib/tool-schemas.js +281 -3
  179. package/bin/lib/transcript-ingestion.js +150 -0
  180. package/bin/lib/type-checker.js +261 -0
  181. package/bin/lib/uat-coverage.js +411 -0
  182. package/bin/lib/vendor-risk.js +173 -0
  183. package/bin/lib/waiver-workflow.js +174 -0
  184. package/bin/lib/web-dashboard.js +126 -0
  185. package/bin/lib/workshop-mode.js +165 -0
  186. package/bin/lib/workstream-ownership.js +104 -0
  187. package/package.json +1 -1
  188. package/.github/agents/jumpstart-ux-designer.agent.md +0 -45
@@ -0,0 +1,261 @@
1
+ /**
2
+ * type-checker.js — Automated Type Checking Gate
3
+ *
4
+ * Detects and runs type checkers (TypeScript tsc, Python mypy/pyright)
5
+ * after agent writes to src/. Provides structured output for the
6
+ * Developer agent during Phase 4 implementation.
7
+ *
8
+ * Usage:
9
+ * echo '{"files":["src/index.ts"],"root":"."}' | node bin/lib/type-checker.js
10
+ *
11
+ * Input (stdin JSON):
12
+ * {
13
+ * "files": ["src/index.ts", "src/utils.ts"],
14
+ * "root": ".",
15
+ * "config": {
16
+ * "type_command": "npx tsc --noEmit",
17
+ * "strict": true
18
+ * }
19
+ * }
20
+ *
21
+ * Output (stdout JSON):
22
+ * {
23
+ * "files_checked": 2,
24
+ * "errors": 3,
25
+ * "warnings": 0,
26
+ * "findings": [...],
27
+ * "pass": false,
28
+ * "checker": "TypeScript"
29
+ * }
30
+ */
31
+
32
+ import { createRequire } from 'module';
33
+ const require = createRequire(import.meta.url);
34
+ const fs = require('fs');
35
+ const path = require('path');
36
+ const { execSync } = require('child_process');
37
+
38
+ /**
39
+ * Detect the project's type checker from configuration files.
40
+ *
41
+ * @param {string} root - Project root path.
42
+ * @returns {{ command: string, name: string } | null}
43
+ */
44
+ function detectTypeChecker(root) {
45
+ const checks = [
46
+ { file: 'tsconfig.json', command: 'npx tsc --noEmit', name: 'TypeScript' },
47
+ { file: 'jsconfig.json', command: 'npx tsc --noEmit --allowJs', name: 'TypeScript (JS)' },
48
+ { file: 'pyrightconfig.json', command: 'npx pyright', name: 'Pyright' },
49
+ { file: 'mypy.ini', command: 'python -m mypy', name: 'mypy' },
50
+ { file: '.mypy.ini', command: 'python -m mypy', name: 'mypy' }
51
+ ];
52
+
53
+ // Check pyproject.toml for mypy or pyright config
54
+ const pyprojectPath = path.join(root, 'pyproject.toml');
55
+ if (fs.existsSync(pyprojectPath)) {
56
+ try {
57
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
58
+ if (content.includes('[tool.mypy]')) {
59
+ return { command: 'python -m mypy', name: 'mypy' };
60
+ }
61
+ if (content.includes('[tool.pyright]')) {
62
+ return { command: 'npx pyright', name: 'Pyright' };
63
+ }
64
+ } catch {
65
+ // ignore
66
+ }
67
+ }
68
+
69
+ for (const check of checks) {
70
+ if (fs.existsSync(path.join(root, check.file))) {
71
+ return { command: check.command, name: check.name };
72
+ }
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Parse type checker output into structured findings.
80
+ * Handles TypeScript, Pyright, and mypy output formats.
81
+ *
82
+ * @param {string} output - Raw type checker output.
83
+ * @param {string} checkerName - Name of the type checker.
84
+ * @returns {Array<{ file: string, line: number|null, severity: string, message: string, code: string|null }>}
85
+ */
86
+ function parseTypeErrors(output, checkerName) {
87
+ const findings = [];
88
+ const lines = output.split('\n');
89
+
90
+ for (const line of lines) {
91
+ // TypeScript format: src/index.ts(10,5): error TS2322: Type 'string' is not assignable...
92
+ const tsMatch = line.match(/^(.+?)\((\d+),\d+\):\s+(error|warning)\s+(TS\d+):\s+(.+)/);
93
+ if (tsMatch) {
94
+ findings.push({
95
+ file: tsMatch[1],
96
+ line: parseInt(tsMatch[2], 10),
97
+ severity: tsMatch[3],
98
+ code: tsMatch[4],
99
+ message: tsMatch[5].trim()
100
+ });
101
+ continue;
102
+ }
103
+
104
+ // TypeScript alternative format: src/index.ts:10:5 - error TS2322: Type 'string'...
105
+ const tsAltMatch = line.match(/^(.+?):(\d+):\d+\s+-\s+(error|warning)\s+(TS\d+):\s+(.+)/);
106
+ if (tsAltMatch) {
107
+ findings.push({
108
+ file: tsAltMatch[1],
109
+ line: parseInt(tsAltMatch[2], 10),
110
+ severity: tsAltMatch[3],
111
+ code: tsAltMatch[4],
112
+ message: tsAltMatch[5].trim()
113
+ });
114
+ continue;
115
+ }
116
+
117
+ // mypy format: src/main.py:10: error: Incompatible types... [assignment]
118
+ const mypyMatch = line.match(/^(.+?):(\d+):\s+(error|warning|note):\s+(.+?)(?:\s+\[(.+?)\])?$/);
119
+ if (mypyMatch && !line.startsWith(' ')) {
120
+ findings.push({
121
+ file: mypyMatch[1],
122
+ line: parseInt(mypyMatch[2], 10),
123
+ severity: mypyMatch[3] === 'note' ? 'warning' : mypyMatch[3],
124
+ code: mypyMatch[5] || null,
125
+ message: mypyMatch[4].trim()
126
+ });
127
+ continue;
128
+ }
129
+
130
+ // Pyright format: src/main.py:10:5 - error: Cannot assign... (reportGeneralClassIssues)
131
+ const pyrightMatch = line.match(/^(.+?):(\d+):\d+\s+-\s+(error|warning|information):\s+(.+?)(?:\s+\((.+?)\))?$/);
132
+ if (pyrightMatch) {
133
+ findings.push({
134
+ file: pyrightMatch[1],
135
+ line: parseInt(pyrightMatch[2], 10),
136
+ severity: pyrightMatch[3] === 'information' ? 'warning' : pyrightMatch[3],
137
+ code: pyrightMatch[5] || null,
138
+ message: pyrightMatch[4].trim()
139
+ });
140
+ }
141
+ }
142
+
143
+ return findings;
144
+ }
145
+
146
+ /**
147
+ * Run type checking on the project.
148
+ *
149
+ * @param {object} input - Type check options.
150
+ * @param {string[]} [input.files] - Specific files (used for filtering results).
151
+ * @param {string} [input.root] - Project root.
152
+ * @param {object} [input.config] - Override config.
153
+ * @param {string} [input.config.type_command] - Custom type check command.
154
+ * @param {boolean} [input.config.strict] - Enable strict mode.
155
+ * @returns {object} Type check results.
156
+ */
157
+ function runTypeCheck(input) {
158
+ const { files = [], root = '.', config = {} } = input;
159
+ const resolvedRoot = path.resolve(root);
160
+
161
+ // Determine type checker
162
+ let checkerInfo;
163
+ if (config.type_command) {
164
+ checkerInfo = { command: config.type_command, name: 'custom' };
165
+ } else {
166
+ checkerInfo = detectTypeChecker(resolvedRoot);
167
+ }
168
+
169
+ if (!checkerInfo) {
170
+ return {
171
+ files_checked: files.length,
172
+ errors: 0,
173
+ warnings: 0,
174
+ findings: [],
175
+ pass: true,
176
+ checker: null,
177
+ message: 'No type checker detected. Consider adding tsconfig.json (TypeScript) or mypy.ini (Python).'
178
+ };
179
+ }
180
+
181
+ // Build command
182
+ let cmd = checkerInfo.command;
183
+ if (config.strict && checkerInfo.name === 'TypeScript') {
184
+ cmd += ' --strict';
185
+ }
186
+
187
+ let output = '';
188
+ let exitCode = 0;
189
+
190
+ try {
191
+ output = execSync(cmd, {
192
+ cwd: resolvedRoot,
193
+ encoding: 'utf8',
194
+ stdio: ['pipe', 'pipe', 'pipe'],
195
+ timeout: 120000
196
+ });
197
+ } catch (err) {
198
+ output = (err.stdout || '') + (err.stderr || '');
199
+ exitCode = err.status || 1;
200
+ }
201
+
202
+ let findings = parseTypeErrors(output, checkerInfo.name);
203
+
204
+ // If specific files provided, filter findings to those files
205
+ if (files.length > 0) {
206
+ const normalizedFiles = new Set(files.map(f => {
207
+ if (path.isAbsolute(f)) return path.relative(resolvedRoot, f);
208
+ return f;
209
+ }));
210
+
211
+ findings = findings.filter(f => {
212
+ const normalized = path.isAbsolute(f.file)
213
+ ? path.relative(resolvedRoot, f.file)
214
+ : f.file;
215
+ return normalizedFiles.has(normalized);
216
+ });
217
+ }
218
+
219
+ const errors = findings.filter(f => f.severity === 'error').length;
220
+ const warnings = findings.filter(f => f.severity === 'warning').length;
221
+
222
+ return {
223
+ files_checked: files.length || '(project-wide)',
224
+ errors,
225
+ warnings,
226
+ findings,
227
+ pass: errors === 0,
228
+ checker: checkerInfo.name,
229
+ exit_code: exitCode
230
+ };
231
+ }
232
+
233
+ // ─── CLI Entry Point ──────────────────────────────────────────────────────────
234
+
235
+ if (process.argv[1] && (
236
+ process.argv[1].endsWith('type-checker.js') ||
237
+ process.argv[1].endsWith('type-checker')
238
+ )) {
239
+ let input = '';
240
+ process.stdin.setEncoding('utf8');
241
+ process.stdin.on('data', chunk => { input += chunk; });
242
+ process.stdin.on('end', () => {
243
+ try {
244
+ const parsed = input.trim() ? JSON.parse(input) : {};
245
+ const result = runTypeCheck(parsed);
246
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
247
+ process.exit(result.pass ? 0 : 1);
248
+ } catch (err) {
249
+ process.stdout.write(JSON.stringify({ error: err.message }) + '\n');
250
+ process.exit(2);
251
+ }
252
+ });
253
+
254
+ if (process.stdin.isTTY) {
255
+ const result = runTypeCheck({});
256
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
257
+ process.exit(result.pass ? 0 : 1);
258
+ }
259
+ }
260
+
261
+ export { runTypeCheck, detectTypeChecker, parseTypeErrors };
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * uat-coverage.js — Automated User Acceptance Testing (UAT) Alignment
5
+ *
6
+ * Part of Jump Start Framework.
7
+ *
8
+ * Extends the coverage.js concept to verify that PRD acceptance criteria
9
+ * (Gherkin-style Given/When/Then or plain-text AC) are actually covered
10
+ * by the generated test suite. Bridges Phase 2 (Planning) and Phase 4
11
+ * (Implementing) by ensuring tests fulfil business needs.
12
+ *
13
+ * Usage:
14
+ * node bin/lib/uat-coverage.js specs/prd.md tests/
15
+ *
16
+ * Output: JSON or Markdown report showing AC → test mapping.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ /**
25
+ * Extract acceptance criteria from a PRD document.
26
+ * Supports both Gherkin (Given/When/Then) and bullet-list AC formats.
27
+ *
28
+ * @param {string} prdContent - PRD markdown content.
29
+ * @returns {Array<{ story_id: string, criteria: string[], gherkin: string[] }>}
30
+ */
31
+ function extractAcceptanceCriteria(prdContent) {
32
+ const stories = [];
33
+ const storyPattern = /\b(E\d+-S\d+)\b/g;
34
+ const storyIds = [...new Set((prdContent.match(storyPattern) || []))];
35
+
36
+ for (const storyId of storyIds) {
37
+ // Find the section for this story
38
+ const escapedId = storyId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ const sectionRegex = new RegExp(
40
+ `${escapedId}[\\s\\S]*?(?=E\\d+-S\\d+|## |$)`,
41
+ 'g'
42
+ );
43
+ const section = sectionRegex.exec(prdContent);
44
+
45
+ if (!section) {
46
+ stories.push({ story_id: storyId, criteria: [], gherkin: [] });
47
+ continue;
48
+ }
49
+
50
+ const sectionText = section[0];
51
+
52
+ // Extract Gherkin blocks (Given/When/Then)
53
+ const gherkinLines = [];
54
+ const gherkinPattern = /^\s*(Given|When|Then|And|But)\s+(.+)/gm;
55
+ let gherkinMatch;
56
+ while ((gherkinMatch = gherkinPattern.exec(sectionText)) !== null) {
57
+ gherkinLines.push(`${gherkinMatch[1]} ${gherkinMatch[2].trim()}`);
58
+ }
59
+
60
+ // Extract bullet-point acceptance criteria
61
+ const criteria = [];
62
+ // Look for AC section markers
63
+ const acSection = sectionText.match(
64
+ /(?:acceptance\s+criteria|AC|criteria)[:\s]*\n([\s\S]*?)(?=\n(?:#{1,3}\s|\n\n)|$)/i
65
+ );
66
+
67
+ if (acSection) {
68
+ const bulletPattern = /^\s*[-*]\s+(.+)/gm;
69
+ let bulletMatch;
70
+ while ((bulletMatch = bulletPattern.exec(acSection[1])) !== null) {
71
+ criteria.push(bulletMatch[1].trim());
72
+ }
73
+ }
74
+
75
+ // Also extract standalone bullet criteria near the story ID
76
+ if (criteria.length === 0) {
77
+ const bulletPattern = /^\s*[-*]\s+(.+)/gm;
78
+ let bulletMatch;
79
+ while ((bulletMatch = bulletPattern.exec(sectionText)) !== null) {
80
+ const text = bulletMatch[1].trim();
81
+ // Filter out non-AC items (headers, metadata, etc.)
82
+ if (text.length > 10 && !text.startsWith('#') && !text.startsWith('|')) {
83
+ criteria.push(text);
84
+ }
85
+ }
86
+ }
87
+
88
+ stories.push({
89
+ story_id: storyId,
90
+ criteria,
91
+ gherkin: gherkinLines
92
+ });
93
+ }
94
+
95
+ return stories;
96
+ }
97
+
98
+ /**
99
+ * Scan test files for references to story IDs and acceptance criteria.
100
+ *
101
+ * @param {string} testDir - Path to the test directory.
102
+ * @param {string[]} storyIds - Story IDs to look for.
103
+ * @returns {Map<string, { files: string[], keywords: string[] }>}
104
+ */
105
+ function scanTestCoverage(testDir, storyIds) {
106
+ const coverage = new Map();
107
+ for (const id of storyIds) {
108
+ coverage.set(id, { files: [], keywords: [] });
109
+ }
110
+
111
+ if (!fs.existsSync(testDir)) {
112
+ return coverage;
113
+ }
114
+
115
+ const testFiles = walkTestFiles(testDir);
116
+
117
+ for (const testFile of testFiles) {
118
+ let content;
119
+ try {
120
+ content = fs.readFileSync(testFile, 'utf8');
121
+ } catch {
122
+ continue;
123
+ }
124
+
125
+ for (const storyId of storyIds) {
126
+ // Check for direct story ID reference
127
+ if (content.includes(storyId)) {
128
+ coverage.get(storyId).files.push(path.relative(testDir, testFile));
129
+ }
130
+
131
+ // Check for describe/it blocks that reference the story semantically
132
+ const escapedId = storyId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
133
+ const testBlockPattern = new RegExp(
134
+ `(?:describe|it|test)\\s*\\(\\s*['"\`].*${escapedId}.*['"\`]`,
135
+ 'i'
136
+ );
137
+ if (testBlockPattern.test(content)) {
138
+ const entry = coverage.get(storyId);
139
+ const relFile = path.relative(testDir, testFile);
140
+ if (!entry.files.includes(relFile)) {
141
+ entry.files.push(relFile);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ return coverage;
148
+ }
149
+
150
+ /**
151
+ * Walk a directory tree and collect test file paths.
152
+ *
153
+ * @param {string} dir - Directory to walk.
154
+ * @returns {string[]}
155
+ */
156
+ function walkTestFiles(dir) {
157
+ const results = [];
158
+ const testPatterns = ['.test.', '.spec.', '_test.', '_spec.', '.feature'];
159
+
160
+ function walk(currentDir) {
161
+ let entries;
162
+ try {
163
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
164
+ } catch {
165
+ return;
166
+ }
167
+ for (const entry of entries) {
168
+ const fullPath = path.join(currentDir, entry.name);
169
+ if (entry.isDirectory()) {
170
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
171
+ walk(fullPath);
172
+ }
173
+ } else {
174
+ const isTestFile = testPatterns.some(p => entry.name.includes(p));
175
+ if (isTestFile) {
176
+ results.push(fullPath);
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ walk(dir);
183
+ return results;
184
+ }
185
+
186
+ /**
187
+ * Match acceptance criteria text against test file content for semantic coverage.
188
+ *
189
+ * @param {Array<{ story_id: string, criteria: string[], gherkin: string[] }>} storyCriteria
190
+ * @param {string} testDir - Path to test directory.
191
+ * @returns {Array<{ story_id: string, criterion: string, covered: boolean, test_files: string[] }>}
192
+ */
193
+ function matchCriteriaToTests(storyCriteria, testDir) {
194
+ const results = [];
195
+
196
+ if (!fs.existsSync(testDir)) {
197
+ for (const story of storyCriteria) {
198
+ const allCriteria = [...story.criteria, ...story.gherkin];
199
+ for (const criterion of allCriteria) {
200
+ results.push({
201
+ story_id: story.story_id,
202
+ criterion,
203
+ covered: false,
204
+ test_files: []
205
+ });
206
+ }
207
+ }
208
+ return results;
209
+ }
210
+
211
+ const testFiles = walkTestFiles(testDir);
212
+ const testContents = new Map();
213
+ for (const file of testFiles) {
214
+ try {
215
+ testContents.set(file, fs.readFileSync(file, 'utf8').toLowerCase());
216
+ } catch {
217
+ // skip
218
+ }
219
+ }
220
+
221
+ for (const story of storyCriteria) {
222
+ const allCriteria = [...story.criteria, ...story.gherkin];
223
+
224
+ for (const criterion of allCriteria) {
225
+ // Extract key terms from the criterion (words > 3 chars, not stopwords)
226
+ const keywords = extractKeywords(criterion);
227
+ const matchingFiles = [];
228
+
229
+ for (const [file, content] of testContents) {
230
+ // Check if test file mentions the story ID
231
+ const hasStoryRef = content.includes(story.story_id.toLowerCase());
232
+
233
+ // Check keyword overlap (at least 50% of keywords present)
234
+ const keywordHits = keywords.filter(k => content.includes(k.toLowerCase()));
235
+ const keywordCoverage = keywords.length > 0
236
+ ? keywordHits.length / keywords.length
237
+ : 0;
238
+
239
+ if (hasStoryRef || keywordCoverage >= 0.5) {
240
+ matchingFiles.push(path.relative(testDir, file));
241
+ }
242
+ }
243
+
244
+ results.push({
245
+ story_id: story.story_id,
246
+ criterion,
247
+ covered: matchingFiles.length > 0,
248
+ test_files: [...new Set(matchingFiles)]
249
+ });
250
+ }
251
+ }
252
+
253
+ return results;
254
+ }
255
+
256
+ /**
257
+ * Extract meaningful keywords from a text string.
258
+ *
259
+ * @param {string} text - Input text.
260
+ * @returns {string[]}
261
+ */
262
+ function extractKeywords(text) {
263
+ const stopwords = new Set([
264
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
265
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'shall',
266
+ 'should', 'may', 'might', 'must', 'can', 'could', 'and', 'but', 'or',
267
+ 'nor', 'not', 'so', 'yet', 'both', 'either', 'neither', 'each',
268
+ 'every', 'all', 'any', 'few', 'more', 'most', 'other', 'some',
269
+ 'such', 'than', 'too', 'very', 'just', 'that', 'this', 'these',
270
+ 'those', 'with', 'from', 'into', 'for', 'about', 'given', 'when',
271
+ 'then', 'user', 'system'
272
+ ]);
273
+
274
+ return text
275
+ .toLowerCase()
276
+ .replace(/[^a-z0-9\s]/g, ' ')
277
+ .split(/\s+/)
278
+ .filter(w => w.length > 3 && !stopwords.has(w));
279
+ }
280
+
281
+ /**
282
+ * Compute UAT coverage statistics.
283
+ *
284
+ * @param {string} prdPath - Path to the PRD file.
285
+ * @param {string} testDir - Path to the test directory.
286
+ * @returns {object} Coverage results.
287
+ */
288
+ function computeUATCoverage(prdPath, testDir) {
289
+ if (!fs.existsSync(prdPath)) {
290
+ throw new Error(`PRD not found: ${prdPath}`);
291
+ }
292
+
293
+ const prdContent = fs.readFileSync(prdPath, 'utf8');
294
+ const storyCriteria = extractAcceptanceCriteria(prdContent);
295
+ const storyIds = storyCriteria.map(s => s.story_id);
296
+
297
+ // Story-level coverage (does the test suite reference this story?)
298
+ const storyCoverage = scanTestCoverage(testDir, storyIds);
299
+
300
+ // Criteria-level coverage (are individual AC items addressed?)
301
+ const criteriaResults = matchCriteriaToTests(storyCriteria, testDir);
302
+
303
+ const totalCriteria = criteriaResults.length;
304
+ const coveredCriteria = criteriaResults.filter(r => r.covered).length;
305
+ const criteriaCoveragePct = totalCriteria > 0
306
+ ? Math.round((coveredCriteria / totalCriteria) * 100)
307
+ : 100;
308
+
309
+ const totalStories = storyIds.length;
310
+ const coveredStories = storyIds.filter(id => {
311
+ const entry = storyCoverage.get(id);
312
+ return entry && entry.files.length > 0;
313
+ });
314
+ const storyCoveragePct = totalStories > 0
315
+ ? Math.round((coveredStories.length / totalStories) * 100)
316
+ : 100;
317
+
318
+ return {
319
+ total_stories: totalStories,
320
+ covered_stories: coveredStories.length,
321
+ story_coverage_pct: storyCoveragePct,
322
+ total_criteria: totalCriteria,
323
+ covered_criteria: coveredCriteria,
324
+ criteria_coverage_pct: criteriaCoveragePct,
325
+ story_details: storyCriteria.map(s => ({
326
+ story_id: s.story_id,
327
+ criteria_count: s.criteria.length + s.gherkin.length,
328
+ test_files: (storyCoverage.get(s.story_id) || { files: [] }).files
329
+ })),
330
+ criteria_details: criteriaResults,
331
+ pass: criteriaCoveragePct >= 80
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Generate a UAT coverage report in markdown format.
337
+ *
338
+ * @param {string} prdPath - Path to the PRD file.
339
+ * @param {string} testDir - Path to the test directory.
340
+ * @returns {string} Markdown report.
341
+ */
342
+ function generateUATReport(prdPath, testDir) {
343
+ const result = computeUATCoverage(prdPath, testDir);
344
+
345
+ let report = `# UAT Coverage Report: Acceptance Criteria → Tests\n\n`;
346
+ report += `**Story Coverage:** ${result.story_coverage_pct}% (${result.covered_stories}/${result.total_stories} stories)\n`;
347
+ report += `**Criteria Coverage:** ${result.criteria_coverage_pct}% (${result.covered_criteria}/${result.total_criteria} criteria)\n`;
348
+ report += `**Status:** ${result.pass ? '✅ PASS' : '❌ FAIL'} (threshold: 80%)\n\n`;
349
+
350
+ report += `## Story Summary\n\n`;
351
+ report += `| Story | Criteria | Test Files | Status |\n`;
352
+ report += `|-------|----------|------------|--------|\n`;
353
+
354
+ for (const story of result.story_details) {
355
+ const status = story.test_files.length > 0 ? '✅' : '❌';
356
+ const files = story.test_files.length > 0
357
+ ? story.test_files.join(', ')
358
+ : '_none_';
359
+ report += `| ${story.story_id} | ${story.criteria_count} | ${files} | ${status} |\n`;
360
+ }
361
+
362
+ // Show uncovered criteria
363
+ const uncovered = result.criteria_details.filter(c => !c.covered);
364
+ if (uncovered.length > 0) {
365
+ report += `\n## Uncovered Acceptance Criteria\n\n`;
366
+ for (const item of uncovered) {
367
+ report += `- ❌ **${item.story_id}**: ${item.criterion}\n`;
368
+ }
369
+ }
370
+
371
+ // Show covered criteria
372
+ const covered = result.criteria_details.filter(c => c.covered);
373
+ if (covered.length > 0) {
374
+ report += `\n## Covered Acceptance Criteria\n\n`;
375
+ for (const item of covered) {
376
+ report += `- ✅ **${item.story_id}**: ${item.criterion} → ${item.test_files.join(', ')}\n`;
377
+ }
378
+ }
379
+
380
+ return report;
381
+ }
382
+
383
+ // ─── CLI Entry Point ──────────────────────────────────────────────────────────
384
+
385
+ if (require.main === module) {
386
+ const args = process.argv.slice(2);
387
+ if (args.length < 2) {
388
+ console.error('Usage: node bin/lib/uat-coverage.js <prd-path> <test-dir>');
389
+ process.exit(2);
390
+ }
391
+
392
+ try {
393
+ const report = generateUATReport(args[0], args[1]);
394
+ process.stdout.write(report + '\n');
395
+ const result = computeUATCoverage(args[0], args[1]);
396
+ process.exit(result.pass ? 0 : 1);
397
+ } catch (err) {
398
+ console.error(`Error: ${err.message}`);
399
+ process.exit(2);
400
+ }
401
+ }
402
+
403
+ module.exports = {
404
+ extractAcceptanceCriteria,
405
+ scanTestCoverage,
406
+ matchCriteriaToTests,
407
+ extractKeywords,
408
+ computeUATCoverage,
409
+ generateUATReport,
410
+ walkTestFiles
411
+ };