wogiflow 1.0.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 (221) hide show
  1. package/.workflow/agents/reviewer.md +81 -0
  2. package/.workflow/agents/security.md +94 -0
  3. package/.workflow/agents/story-writer.md +58 -0
  4. package/.workflow/bridges/base-bridge.js +395 -0
  5. package/.workflow/bridges/claude-bridge.js +434 -0
  6. package/.workflow/bridges/index.js +130 -0
  7. package/.workflow/lib/assumption-detector.js +481 -0
  8. package/.workflow/lib/config-substitution.js +371 -0
  9. package/.workflow/lib/failure-categories.js +478 -0
  10. package/.workflow/state/app-map.md.template +15 -0
  11. package/.workflow/state/architecture.md.template +24 -0
  12. package/.workflow/state/component-index.json.template +5 -0
  13. package/.workflow/state/decisions.md.template +15 -0
  14. package/.workflow/state/feedback-patterns.md.template +9 -0
  15. package/.workflow/state/knowledge-sync.json.template +6 -0
  16. package/.workflow/state/progress.md.template +14 -0
  17. package/.workflow/state/ready.json.template +7 -0
  18. package/.workflow/state/request-log.md.template +14 -0
  19. package/.workflow/state/session-state.json.template +11 -0
  20. package/.workflow/state/stack.md.template +33 -0
  21. package/.workflow/state/testing.md.template +36 -0
  22. package/.workflow/templates/claude-md.hbs +257 -0
  23. package/.workflow/templates/correction-report.md +67 -0
  24. package/.workflow/templates/gemini-md.hbs +52 -0
  25. package/README.md +1802 -0
  26. package/bin/flow +205 -0
  27. package/lib/index.js +33 -0
  28. package/lib/installer.js +467 -0
  29. package/lib/release-channel.js +269 -0
  30. package/lib/skill-registry.js +526 -0
  31. package/lib/upgrader.js +401 -0
  32. package/lib/utils.js +305 -0
  33. package/package.json +64 -0
  34. package/scripts/flow +985 -0
  35. package/scripts/flow-adaptive-learning.js +1259 -0
  36. package/scripts/flow-aggregate.js +488 -0
  37. package/scripts/flow-archive +133 -0
  38. package/scripts/flow-auto-context.js +1015 -0
  39. package/scripts/flow-auto-learn.js +615 -0
  40. package/scripts/flow-bridge.js +223 -0
  41. package/scripts/flow-browser-suggest.js +316 -0
  42. package/scripts/flow-bug.js +247 -0
  43. package/scripts/flow-cascade.js +711 -0
  44. package/scripts/flow-changelog +85 -0
  45. package/scripts/flow-checkpoint.js +483 -0
  46. package/scripts/flow-cli.js +403 -0
  47. package/scripts/flow-code-intelligence.js +760 -0
  48. package/scripts/flow-complexity.js +502 -0
  49. package/scripts/flow-config-set.js +152 -0
  50. package/scripts/flow-constants.js +157 -0
  51. package/scripts/flow-context +152 -0
  52. package/scripts/flow-context-init.js +482 -0
  53. package/scripts/flow-context-monitor.js +384 -0
  54. package/scripts/flow-context-scoring.js +886 -0
  55. package/scripts/flow-correct.js +458 -0
  56. package/scripts/flow-damage-control.js +985 -0
  57. package/scripts/flow-deps +101 -0
  58. package/scripts/flow-diff.js +700 -0
  59. package/scripts/flow-done +151 -0
  60. package/scripts/flow-done.js +489 -0
  61. package/scripts/flow-durable-session.js +1541 -0
  62. package/scripts/flow-entropy-monitor.js +345 -0
  63. package/scripts/flow-export-profile +349 -0
  64. package/scripts/flow-export-scanner.js +1046 -0
  65. package/scripts/flow-figma-confirm.js +400 -0
  66. package/scripts/flow-figma-extract.js +496 -0
  67. package/scripts/flow-figma-generate.js +683 -0
  68. package/scripts/flow-figma-index.js +909 -0
  69. package/scripts/flow-figma-match.js +617 -0
  70. package/scripts/flow-figma-mcp-server.js +518 -0
  71. package/scripts/flow-figma-pipeline.js +414 -0
  72. package/scripts/flow-file-ops.js +301 -0
  73. package/scripts/flow-gate-confidence.js +825 -0
  74. package/scripts/flow-guided-edit.js +659 -0
  75. package/scripts/flow-health +185 -0
  76. package/scripts/flow-health.js +413 -0
  77. package/scripts/flow-hooks.js +556 -0
  78. package/scripts/flow-http-client.js +249 -0
  79. package/scripts/flow-hybrid-detect.js +167 -0
  80. package/scripts/flow-hybrid-interactive.js +591 -0
  81. package/scripts/flow-hybrid-test.js +152 -0
  82. package/scripts/flow-import-profile +439 -0
  83. package/scripts/flow-init +253 -0
  84. package/scripts/flow-instruction-richness.js +827 -0
  85. package/scripts/flow-jira-integration.js +579 -0
  86. package/scripts/flow-knowledge-router.js +522 -0
  87. package/scripts/flow-knowledge-sync.js +589 -0
  88. package/scripts/flow-linear-integration.js +631 -0
  89. package/scripts/flow-links.js +774 -0
  90. package/scripts/flow-log-manager.js +559 -0
  91. package/scripts/flow-loop-enforcer.js +1246 -0
  92. package/scripts/flow-loop-retry-learning.js +630 -0
  93. package/scripts/flow-lsp.js +923 -0
  94. package/scripts/flow-map-index +348 -0
  95. package/scripts/flow-map-sync +201 -0
  96. package/scripts/flow-memory-blocks.js +668 -0
  97. package/scripts/flow-memory-compactor.js +350 -0
  98. package/scripts/flow-memory-db.js +1110 -0
  99. package/scripts/flow-memory-sync.js +484 -0
  100. package/scripts/flow-metrics.js +353 -0
  101. package/scripts/flow-migrate-ids.js +370 -0
  102. package/scripts/flow-model-adapter.js +802 -0
  103. package/scripts/flow-model-router.js +884 -0
  104. package/scripts/flow-models.js +1231 -0
  105. package/scripts/flow-morning.js +517 -0
  106. package/scripts/flow-multi-approach.js +660 -0
  107. package/scripts/flow-new-feature +86 -0
  108. package/scripts/flow-onboard +1042 -0
  109. package/scripts/flow-orchestrate-llm.js +459 -0
  110. package/scripts/flow-orchestrate.js +3592 -0
  111. package/scripts/flow-output.js +123 -0
  112. package/scripts/flow-parallel-detector.js +399 -0
  113. package/scripts/flow-parallel-dispatch.js +987 -0
  114. package/scripts/flow-parallel.js +428 -0
  115. package/scripts/flow-pattern-enforcer.js +600 -0
  116. package/scripts/flow-prd-manager.js +282 -0
  117. package/scripts/flow-progress.js +323 -0
  118. package/scripts/flow-project-analyzer.js +975 -0
  119. package/scripts/flow-prompt-composer.js +487 -0
  120. package/scripts/flow-providers.js +1381 -0
  121. package/scripts/flow-queue.js +308 -0
  122. package/scripts/flow-ready +82 -0
  123. package/scripts/flow-ready.js +189 -0
  124. package/scripts/flow-regression.js +396 -0
  125. package/scripts/flow-response-parser.js +450 -0
  126. package/scripts/flow-resume.js +284 -0
  127. package/scripts/flow-rules-sync.js +439 -0
  128. package/scripts/flow-run-trace.js +718 -0
  129. package/scripts/flow-safety.js +587 -0
  130. package/scripts/flow-search +104 -0
  131. package/scripts/flow-security.js +481 -0
  132. package/scripts/flow-session-end +106 -0
  133. package/scripts/flow-session-end.js +437 -0
  134. package/scripts/flow-session-state.js +671 -0
  135. package/scripts/flow-setup-hooks +216 -0
  136. package/scripts/flow-setup-hooks.js +377 -0
  137. package/scripts/flow-skill-create.js +329 -0
  138. package/scripts/flow-skill-creator.js +572 -0
  139. package/scripts/flow-skill-generator.js +1046 -0
  140. package/scripts/flow-skill-learn.js +880 -0
  141. package/scripts/flow-skill-matcher.js +578 -0
  142. package/scripts/flow-spec-generator.js +820 -0
  143. package/scripts/flow-stack-wizard.js +895 -0
  144. package/scripts/flow-standup +162 -0
  145. package/scripts/flow-start +74 -0
  146. package/scripts/flow-start.js +235 -0
  147. package/scripts/flow-status +110 -0
  148. package/scripts/flow-status.js +301 -0
  149. package/scripts/flow-step-browser.js +83 -0
  150. package/scripts/flow-step-changelog.js +217 -0
  151. package/scripts/flow-step-comments.js +306 -0
  152. package/scripts/flow-step-complexity.js +234 -0
  153. package/scripts/flow-step-coverage.js +218 -0
  154. package/scripts/flow-step-knowledge.js +193 -0
  155. package/scripts/flow-step-pr-tests.js +364 -0
  156. package/scripts/flow-step-regression.js +89 -0
  157. package/scripts/flow-step-review.js +516 -0
  158. package/scripts/flow-step-security.js +162 -0
  159. package/scripts/flow-step-silent-failures.js +290 -0
  160. package/scripts/flow-step-simplifier.js +346 -0
  161. package/scripts/flow-story +105 -0
  162. package/scripts/flow-story.js +500 -0
  163. package/scripts/flow-suspend.js +252 -0
  164. package/scripts/flow-sync-daemon.js +654 -0
  165. package/scripts/flow-task-analyzer.js +606 -0
  166. package/scripts/flow-team-dashboard.js +748 -0
  167. package/scripts/flow-team-sync.js +752 -0
  168. package/scripts/flow-team.js +977 -0
  169. package/scripts/flow-tech-options.js +528 -0
  170. package/scripts/flow-templates.js +812 -0
  171. package/scripts/flow-tiered-learning.js +728 -0
  172. package/scripts/flow-trace +204 -0
  173. package/scripts/flow-transcript-chunking.js +1106 -0
  174. package/scripts/flow-transcript-digest.js +7918 -0
  175. package/scripts/flow-transcript-language.js +465 -0
  176. package/scripts/flow-transcript-parsing.js +1085 -0
  177. package/scripts/flow-transcript-stories.js +2194 -0
  178. package/scripts/flow-update-map +224 -0
  179. package/scripts/flow-utils.js +2242 -0
  180. package/scripts/flow-verification.js +644 -0
  181. package/scripts/flow-verify.js +1177 -0
  182. package/scripts/flow-voice-input.js +638 -0
  183. package/scripts/flow-watch +168 -0
  184. package/scripts/flow-workflow-steps.js +521 -0
  185. package/scripts/flow-workflow.js +1029 -0
  186. package/scripts/flow-worktree.js +489 -0
  187. package/scripts/hooks/adapters/base-adapter.js +102 -0
  188. package/scripts/hooks/adapters/claude-code.js +359 -0
  189. package/scripts/hooks/adapters/index.js +79 -0
  190. package/scripts/hooks/core/component-check.js +341 -0
  191. package/scripts/hooks/core/index.js +35 -0
  192. package/scripts/hooks/core/loop-check.js +241 -0
  193. package/scripts/hooks/core/session-context.js +294 -0
  194. package/scripts/hooks/core/task-gate.js +177 -0
  195. package/scripts/hooks/core/validation.js +230 -0
  196. package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
  197. package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
  198. package/scripts/hooks/entry/claude-code/session-end.js +87 -0
  199. package/scripts/hooks/entry/claude-code/session-start.js +46 -0
  200. package/scripts/hooks/entry/claude-code/stop.js +43 -0
  201. package/scripts/postinstall.js +139 -0
  202. package/templates/browser-test-flow.json +56 -0
  203. package/templates/bug-report.md +43 -0
  204. package/templates/component-detail.md +42 -0
  205. package/templates/component.stories.tsx +49 -0
  206. package/templates/context/constraints.md +83 -0
  207. package/templates/context/conventions.md +177 -0
  208. package/templates/context/stack.md +60 -0
  209. package/templates/correction-report.md +90 -0
  210. package/templates/feature-proposal.md +35 -0
  211. package/templates/hybrid/_base.md +254 -0
  212. package/templates/hybrid/_patterns.md +45 -0
  213. package/templates/hybrid/create-component.md +127 -0
  214. package/templates/hybrid/create-file.md +56 -0
  215. package/templates/hybrid/create-hook.md +145 -0
  216. package/templates/hybrid/create-service.md +70 -0
  217. package/templates/hybrid/fix-bug.md +33 -0
  218. package/templates/hybrid/modify-file.md +55 -0
  219. package/templates/story.md +68 -0
  220. package/templates/task.json +56 -0
  221. package/templates/trace.md +69 -0
@@ -0,0 +1,2194 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Transcript Stories Module
5
+ *
6
+ * Extracted from flow-transcript-digest.js for maintainability.
7
+ * Handles story generation, presentation queue, editing, and workflow export.
8
+ *
9
+ * Part of E3-S2: Story Generation with Source Tracing
10
+ *
11
+ * Dependencies: Requires core functions from flow-transcript-digest.js
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+
18
+ // Core functions are injected via init() to avoid circular dependencies
19
+ let digestCore = null;
20
+
21
+ /**
22
+ * Initialize with core digest functions
23
+ * @param {object} core - Core functions from flow-transcript-digest.js
24
+ */
25
+ function init(core) {
26
+ digestCore = core;
27
+ }
28
+
29
+ // Helper to ensure init was called
30
+ function requireInit() {
31
+ if (!digestCore) {
32
+ throw new Error('flow-transcript-stories not initialized. Call init() first.');
33
+ }
34
+ }
35
+
36
+ // Proxy functions to core module
37
+ function loadActiveDigest() { requireInit(); return digestCore.loadActiveDigest(); }
38
+ function saveActiveDigest(d) { requireInit(); return digestCore.saveActiveDigest(d); }
39
+ function loadTopics() { requireInit(); return digestCore.loadTopics(); }
40
+ function saveTopics(t) { requireInit(); return digestCore.saveTopics(t); }
41
+ function loadStatementMap() { requireInit(); return digestCore.loadStatementMap(); }
42
+ function loadClarifications() { requireInit(); return digestCore.loadClarifications(); }
43
+ function isRequirement(s) { requireInit(); return digestCore.isRequirement(s); }
44
+ function isVagueStatement(s) { requireInit(); return digestCore.isVagueStatement(s); }
45
+ function analyzeComplexity() { requireInit(); return digestCore.analyzeComplexity(); }
46
+
47
+ // State directory
48
+ const STATE_DIR = path.join(process.cwd(), '.workflow', 'state', 'digests');
49
+
50
+ // ==========================================================================
51
+ // E3-S2: Story Generation with Source Tracing
52
+ // ==========================================================================
53
+
54
+ /**
55
+ * User type patterns for story generation
56
+ */
57
+ const USER_TYPE_PATTERNS = [
58
+ { pattern: /\b(admin|administrator)\b/i, type: 'admin' },
59
+ { pattern: /\b(user|customer|client)\b/i, type: 'user' },
60
+ { pattern: /\b(manager|supervisor)\b/i, type: 'manager' },
61
+ { pattern: /\b(developer|dev)\b/i, type: 'developer' },
62
+ { pattern: /\b(guest|visitor|anonymous)\b/i, type: 'guest' },
63
+ { pattern: /\b(owner|creator)\b/i, type: 'owner' }
64
+ ];
65
+
66
+ /**
67
+ * Scenario name patterns
68
+ */
69
+ const SCENARIO_PATTERNS = [
70
+ { pattern: /\b(create|add|new)\b/i, prefix: 'Create' },
71
+ { pattern: /\b(edit|update|modify|change)\b/i, prefix: 'Update' },
72
+ { pattern: /\b(delete|remove|archive)\b/i, prefix: 'Delete' },
73
+ { pattern: /\b(view|show|display|see|list)\b/i, prefix: 'View' },
74
+ { pattern: /\b(search|find|filter)\b/i, prefix: 'Search' },
75
+ { pattern: /\b(login|authenticate|sign in)\b/i, prefix: 'Login' },
76
+ { pattern: /\b(logout|sign out)\b/i, prefix: 'Logout' },
77
+ { pattern: /\b(validate|check|verify)\b/i, prefix: 'Validate' },
78
+ { pattern: /\b(submit|save|confirm)\b/i, prefix: 'Submit' },
79
+ { pattern: /\b(cancel|dismiss|close)\b/i, prefix: 'Cancel' },
80
+ { pattern: /\b(select|choose|pick)\b/i, prefix: 'Select' },
81
+ { pattern: /\b(upload|import)\b/i, prefix: 'Upload' },
82
+ { pattern: /\b(download|export)\b/i, prefix: 'Download' }
83
+ ];
84
+
85
+ /**
86
+ * Generate unique story ID
87
+ */
88
+ function generateStoryId() {
89
+ return 'story-' + crypto.randomBytes(4).toString('hex');
90
+ }
91
+
92
+ /**
93
+ * Detect user type from statements
94
+ */
95
+ function detectUserType(statements) {
96
+ for (const statement of statements) {
97
+ if (!statement.text) continue;
98
+ for (const { pattern, type } of USER_TYPE_PATTERNS) {
99
+ if (pattern.test(statement.text)) {
100
+ return { value: type, source: statement.id };
101
+ }
102
+ }
103
+ }
104
+ return { value: 'user', source: 'default' };
105
+ }
106
+
107
+ /**
108
+ * Extract main object/entity from text
109
+ */
110
+ function extractObject(text) {
111
+ // Look for nouns after action verbs
112
+ const patterns = [
113
+ /(?:create|add|new|edit|update|delete|remove|view|show)\s+(?:a\s+|the\s+)?(\w+)/i,
114
+ /(\w+)\s+(?:table|form|list|page|modal|button)/i,
115
+ /\b(user|product|order|item|account|profile|setting|message)\b/i
116
+ ];
117
+
118
+ for (const pattern of patterns) {
119
+ const match = text.match(pattern);
120
+ if (match) {
121
+ return match[1].toLowerCase();
122
+ }
123
+ }
124
+
125
+ // Default to first noun-like word
126
+ const words = text.split(/\s+/);
127
+ for (const word of words) {
128
+ if (word.length > 3 && /^[a-z]+$/i.test(word)) {
129
+ return word.toLowerCase();
130
+ }
131
+ }
132
+
133
+ return 'item';
134
+ }
135
+
136
+ /**
137
+ * Generate scenario name from requirement
138
+ */
139
+ function generateScenarioName(requirement) {
140
+ const text = requirement.text || '';
141
+
142
+ for (const { pattern, prefix } of SCENARIO_PATTERNS) {
143
+ if (pattern.test(text)) {
144
+ const object = extractObject(text);
145
+ return `${prefix} ${object}`;
146
+ }
147
+ }
148
+
149
+ return `Handle ${extractObject(text)}`;
150
+ }
151
+
152
+ /**
153
+ * Extract action from requirement text
154
+ */
155
+ function extractActionFromText(text) {
156
+ // Look for verb phrases
157
+ const patterns = [
158
+ /(?:should|can|will|must)\s+(be able to\s+)?(\w+(?:\s+\w+)?)/i,
159
+ /(?:want to|need to)\s+(\w+(?:\s+\w+)?)/i,
160
+ /(?:add|create|edit|delete|view|manage)\s+(\w+(?:\s+\w+)?)/i
161
+ ];
162
+
163
+ for (const pattern of patterns) {
164
+ const match = text.match(pattern);
165
+ if (match) {
166
+ return match[match.length - 1];
167
+ }
168
+ }
169
+
170
+ return 'perform the action';
171
+ }
172
+
173
+ /**
174
+ * Extract outcome from requirement text
175
+ */
176
+ function extractOutcomeFromText(text) {
177
+ // Look for outcome indicators
178
+ if (/\b(table|list|grid)\b/i.test(text)) {
179
+ return 'I should see the data displayed';
180
+ }
181
+ if (/\b(form|input)\b/i.test(text)) {
182
+ return 'I should see the form';
183
+ }
184
+ if (/\b(button)\b/i.test(text)) {
185
+ return 'the action should be performed';
186
+ }
187
+ if (/\b(modal|dialog|popup)\b/i.test(text)) {
188
+ return 'I should see the modal';
189
+ }
190
+ if (/\b(create|add|new)\b/i.test(text)) {
191
+ return 'a new item should be created';
192
+ }
193
+ if (/\b(delete|remove)\b/i.test(text)) {
194
+ return 'the item should be removed';
195
+ }
196
+ if (/\b(update|edit|modify)\b/i.test(text)) {
197
+ return 'the changes should be saved';
198
+ }
199
+
200
+ return 'the expected result should occur';
201
+ }
202
+
203
+ /**
204
+ * Convert statement to Given clause
205
+ */
206
+ function convertToGiven(text) {
207
+ // Remove leading "when", "if", etc.
208
+ let given = text.replace(/^(when|if|after|once|assuming)\s+/i, '');
209
+
210
+ // Convert to first person if needed
211
+ given = given.replace(/\b(the user|users)\b/i, 'I');
212
+
213
+ return given;
214
+ }
215
+
216
+ /**
217
+ * Extract Given clause from context
218
+ */
219
+ function extractGiven(requirement, contextStatements, topic) {
220
+ // Look for precondition statements
221
+ const preconditions = contextStatements.filter(s =>
222
+ /\b(when|if|after|once|assuming|logged in|on the)\b/i.test(s.text || '') &&
223
+ s.id !== requirement.id
224
+ );
225
+
226
+ if (preconditions.length > 0) {
227
+ return {
228
+ text: convertToGiven(preconditions[0].text),
229
+ source: preconditions[0].id
230
+ };
231
+ }
232
+
233
+ // Default context based on topic
234
+ const topicLower = (topic.title || '').toLowerCase();
235
+ if (topicLower.includes('dashboard') || topicLower.includes('management')) {
236
+ return { text: `I am on the ${topicLower} page`, source: 'context' };
237
+ }
238
+ if (topicLower.includes('form')) {
239
+ return { text: 'I am filling out the form', source: 'context' };
240
+ }
241
+ if (topicLower.includes('settings') || topicLower.includes('profile')) {
242
+ return { text: 'I am in the settings section', source: 'context' };
243
+ }
244
+
245
+ return { text: 'I am logged into the system', source: 'context' };
246
+ }
247
+
248
+ /**
249
+ * Extract When clause from requirement
250
+ */
251
+ function extractWhen(requirement) {
252
+ const text = requirement.text || '';
253
+ const action = extractActionFromText(text);
254
+
255
+ return {
256
+ text: `I ${action}`,
257
+ source: requirement.id
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Extract Then clause from requirement
263
+ */
264
+ function extractThen(requirement) {
265
+ const text = requirement.text || '';
266
+ const outcome = extractOutcomeFromText(text);
267
+
268
+ return {
269
+ text: outcome,
270
+ source: requirement.id
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Generate criteria from clarification answers
276
+ */
277
+ function generateCriteriaFromClarification(clarification, topic) {
278
+ const criteria = [];
279
+ const question = (clarification.question || '').toLowerCase();
280
+ const answer = clarification.answer || '';
281
+
282
+ // Column-related questions
283
+ if (question.includes('column') || question.includes('display') || question.includes('show')) {
284
+ criteria.push({
285
+ scenario: 'Display correct columns',
286
+ given: { text: 'I am viewing the table', source: 'context' },
287
+ when: { text: 'the data loads', source: 'context' },
288
+ then: { text: `I should see columns: ${answer}`, source: clarification.id },
289
+ sources: [clarification.id],
290
+ originalText: `Q: ${clarification.question}\nA: ${answer}`,
291
+ type: 'clarification'
292
+ });
293
+ }
294
+
295
+ // Validation-related questions
296
+ if (question.includes('validation') || question.includes('required') || question.includes('rules')) {
297
+ criteria.push({
298
+ scenario: 'Validate form input',
299
+ given: { text: 'I am filling out the form', source: 'context' },
300
+ when: { text: 'I submit with invalid data', source: 'context' },
301
+ then: { text: `validation should enforce: ${answer}`, source: clarification.id },
302
+ sources: [clarification.id],
303
+ originalText: `Q: ${clarification.question}\nA: ${answer}`,
304
+ type: 'clarification'
305
+ });
306
+ }
307
+
308
+ // Action-related questions
309
+ if (question.includes('action') || question.includes('button') || question.includes('click')) {
310
+ criteria.push({
311
+ scenario: 'Handle user actions',
312
+ given: { text: 'I am on the page', source: 'context' },
313
+ when: { text: 'I perform an action', source: 'context' },
314
+ then: { text: `the available actions are: ${answer}`, source: clarification.id },
315
+ sources: [clarification.id],
316
+ originalText: `Q: ${clarification.question}\nA: ${answer}`,
317
+ type: 'clarification'
318
+ });
319
+ }
320
+
321
+ // Sort/filter questions
322
+ if (question.includes('sort') || question.includes('filter') || question.includes('order')) {
323
+ criteria.push({
324
+ scenario: 'Sort and filter data',
325
+ given: { text: 'I am viewing the data', source: 'context' },
326
+ when: { text: 'I apply sorting or filtering', source: 'context' },
327
+ then: { text: `sorting/filtering should support: ${answer}`, source: clarification.id },
328
+ sources: [clarification.id],
329
+ originalText: `Q: ${clarification.question}\nA: ${answer}`,
330
+ type: 'clarification'
331
+ });
332
+ }
333
+
334
+ // Generic fallback
335
+ if (criteria.length === 0) {
336
+ const keyword = question.split(' ').find(w => w.length > 4) || 'detail';
337
+ criteria.push({
338
+ scenario: `Handle ${keyword}`,
339
+ given: { text: 'the feature is active', source: 'context' },
340
+ when: { text: 'the user interacts', source: 'context' },
341
+ then: { text: answer, source: clarification.id },
342
+ sources: [clarification.id],
343
+ originalText: `Q: ${clarification.question}\nA: ${answer}`,
344
+ type: 'clarification'
345
+ });
346
+ }
347
+
348
+ return criteria;
349
+ }
350
+
351
+ /**
352
+ * Build traceability matrix for a story
353
+ */
354
+ function buildTraceabilityMatrix(criteria) {
355
+ const matrix = [];
356
+
357
+ for (let i = 0; i < criteria.length; i++) {
358
+ const criterion = criteria[i];
359
+ const criterionId = `AC-${i + 1}`;
360
+
361
+ for (const sourceId of criterion.sources || []) {
362
+ matrix.push({
363
+ criterion_id: criterionId,
364
+ criterion_name: criterion.scenario,
365
+ source_id: sourceId,
366
+ source_text: (criterion.originalText || '').slice(0, 60) + '...',
367
+ source_type: sourceId.startsWith('s-') ? 'statement' :
368
+ sourceId.startsWith('q-') ? 'clarification' : 'context'
369
+ });
370
+ }
371
+ }
372
+
373
+ return matrix;
374
+ }
375
+
376
+ /**
377
+ * Validate story coverage
378
+ */
379
+ function validateStoryCoverage(story, topicStatements) {
380
+ const warnings = [];
381
+ const coveredSources = new Set(
382
+ story.acceptance_criteria.flatMap(c => c.sources || [])
383
+ );
384
+
385
+ // Check all requirements are covered
386
+ const requirements = topicStatements.filter(s =>
387
+ isRequirement({ text: s.text })
388
+ );
389
+
390
+ for (const req of requirements) {
391
+ if (!coveredSources.has(req.id)) {
392
+ warnings.push({
393
+ type: 'uncovered_requirement',
394
+ statement_id: req.id,
395
+ text: req.text,
396
+ message: 'Requirement not covered by any acceptance criterion'
397
+ });
398
+ }
399
+ }
400
+
401
+ // Check for assumptions
402
+ for (const criterion of story.acceptance_criteria) {
403
+ if (!criterion.sources || criterion.sources.length === 0 ||
404
+ criterion.sources.every(s => s === 'context' || s === 'default')) {
405
+ warnings.push({
406
+ type: 'assumption',
407
+ criterion: criterion.scenario,
408
+ message: 'Criterion has no direct source - may be an assumption'
409
+ });
410
+ }
411
+ }
412
+
413
+ return {
414
+ valid: warnings.filter(w => w.type === 'uncovered_requirement').length === 0,
415
+ coverage_percent: requirements.length > 0 ?
416
+ Math.round((coveredSources.size / requirements.length) * 100) : 100,
417
+ warnings
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Generate a story from a topic
423
+ */
424
+ function generateStoryFromTopic(topicId) {
425
+ const topics = loadTopics();
426
+ const statementMap = loadStatementMap();
427
+ const clarifications = loadClarifications();
428
+ const complexityResult = analyzeComplexity();
429
+
430
+ if (!topics || !topics.topics) {
431
+ return { error: 'No topics found' };
432
+ }
433
+
434
+ const topic = topics.topics.find(t => t.id === topicId);
435
+ if (!topic) {
436
+ return { error: `Topic ${topicId} not found` };
437
+ }
438
+
439
+ const statements = statementMap?.statements || [];
440
+ const topicStatements = statements.filter(s => s.topic_id === topicId);
441
+ const requirements = topicStatements.filter(s =>
442
+ isRequirement({ text: s.text })
443
+ );
444
+
445
+ // Get answered clarifications for this topic
446
+ const topicClarifications = (clarifications?.questions || [])
447
+ .filter(q => q.topic_id === topicId && q.status === 'answered');
448
+
449
+ // Detect user type
450
+ const userType = detectUserType(topicStatements);
451
+
452
+ // Generate acceptance criteria from requirements
453
+ const criteria = [];
454
+
455
+ for (const req of requirements) {
456
+ criteria.push({
457
+ scenario: generateScenarioName(req),
458
+ given: extractGiven(req, topicStatements, topic),
459
+ when: extractWhen(req),
460
+ then: extractThen(req),
461
+ sources: [req.id],
462
+ originalText: req.text,
463
+ type: 'requirement'
464
+ });
465
+ }
466
+
467
+ // Add criteria from clarification answers
468
+ for (const clarification of topicClarifications) {
469
+ const derived = generateCriteriaFromClarification(clarification, topic);
470
+ criteria.push(...derived);
471
+ }
472
+
473
+ // Build traceability matrix
474
+ const traceability = buildTraceabilityMatrix(criteria);
475
+
476
+ // Get topic complexity
477
+ const topicComplexity = complexityResult.topic_analysis?.find(t => t.topic_id === topicId);
478
+
479
+ // Build story object
480
+ const story = {
481
+ id: generateStoryId(),
482
+ topic_id: topicId,
483
+ title: topic.title,
484
+ generated_at: now(),
485
+ user_story: {
486
+ user_type: userType.value,
487
+ user_type_source: userType.source,
488
+ action: requirements.length > 0 ? extractActionFromText(requirements[0].text) : 'use this feature',
489
+ action_source: requirements.length > 0 ? requirements[0].id : 'inferred',
490
+ benefit: 'accomplish their goals efficiently',
491
+ benefit_source: 'inferred'
492
+ },
493
+ description: {
494
+ text: `Feature for ${topic.title.toLowerCase()}. ` +
495
+ (topicStatements.length > 0 ? topicStatements[0].text : ''),
496
+ source_statements: topicStatements.slice(0, 3).map(s => s.id)
497
+ },
498
+ acceptance_criteria: criteria.map((c, i) => ({
499
+ id: `AC-${i + 1}`,
500
+ ...c
501
+ })),
502
+ traceability,
503
+ coverage: {
504
+ statements_total: topicStatements.length,
505
+ statements_covered: new Set(criteria.flatMap(c => c.sources || [])).size,
506
+ requirements_total: requirements.length,
507
+ clarifications_used: topicClarifications.length,
508
+ coverage_percent: requirements.length > 0 ?
509
+ Math.round((new Set(criteria.filter(c => c.type === 'requirement').flatMap(c => c.sources)).size / requirements.length) * 100) : 100
510
+ },
511
+ complexity: topicComplexity || { score: 0, level: 'unknown' },
512
+ validation: validateStoryCoverage({ acceptance_criteria: criteria }, topicStatements)
513
+ };
514
+
515
+ return story;
516
+ }
517
+
518
+ /**
519
+ * Generate stories for all active topics
520
+ */
521
+ function generateAllStories() {
522
+ const topics = loadTopics();
523
+ if (!topics || !topics.topics) {
524
+ return { error: 'No topics found' };
525
+ }
526
+
527
+ const activeTopics = topics.topics.filter(t => t.status === 'active');
528
+ const stories = [];
529
+ const errors = [];
530
+
531
+ for (const topic of activeTopics) {
532
+ const story = generateStoryFromTopic(topic.id);
533
+ if (story.error) {
534
+ errors.push({ topic_id: topic.id, error: story.error });
535
+ } else {
536
+ stories.push(story);
537
+ }
538
+ }
539
+
540
+ return {
541
+ stories,
542
+ summary: {
543
+ total_topics: activeTopics.length,
544
+ stories_generated: stories.length,
545
+ errors: errors.length,
546
+ total_criteria: stories.reduce((sum, s) => sum + s.acceptance_criteria.length, 0),
547
+ average_coverage: stories.length > 0 ?
548
+ Math.round(stories.reduce((sum, s) => sum + s.coverage.coverage_percent, 0) / stories.length) : 0
549
+ },
550
+ errors
551
+ };
552
+ }
553
+
554
+ /**
555
+ * Save story to digest
556
+ */
557
+ function saveStory(story) {
558
+ const activeDigest = loadActiveDigest();
559
+ if (!activeDigest.session.digest_path) {
560
+ return { error: 'No active digest session' };
561
+ }
562
+
563
+ const storiesPath = path.join(activeDigest.session.digest_path, 'stories');
564
+ fs.mkdirSync(storiesPath, { recursive: true });
565
+
566
+ const storyPath = path.join(storiesPath, `${story.id}.json`);
567
+ fs.writeFileSync(storyPath, JSON.stringify(story, null, 2));
568
+
569
+ return { saved: true, path: storyPath };
570
+ }
571
+
572
+ /**
573
+ * Load story from digest
574
+ */
575
+ function loadStory(storyId) {
576
+ const activeDigest = loadActiveDigest();
577
+ if (!activeDigest.session.digest_path) {
578
+ return null;
579
+ }
580
+
581
+ const storyPath = path.join(activeDigest.session.digest_path, 'stories', `${storyId}.json`);
582
+ try {
583
+ return JSON.parse(fs.readFileSync(storyPath, 'utf8'));
584
+ } catch (err) {
585
+ return null;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Load all stories from digest
591
+ */
592
+ function loadAllStories() {
593
+ const activeDigest = loadActiveDigest();
594
+ if (!activeDigest.session.digest_path) {
595
+ return [];
596
+ }
597
+
598
+ const storiesPath = path.join(activeDigest.session.digest_path, 'stories');
599
+ try {
600
+ const files = fs.readdirSync(storiesPath).filter(f => f.endsWith('.json'));
601
+ return files.map(f => {
602
+ try {
603
+ return JSON.parse(fs.readFileSync(path.join(storiesPath, f), 'utf8'));
604
+ } catch (err) {
605
+ return null;
606
+ }
607
+ }).filter(Boolean);
608
+ } catch (err) {
609
+ return [];
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Format story as markdown with source tracing
615
+ */
616
+ function formatStoryAsMarkdown(story) {
617
+ let md = '';
618
+
619
+ md += `# [${story.id}] ${story.title}\n\n`;
620
+
621
+ // Source topic
622
+ md += `## Source Topic\n`;
623
+ md += `**Topic ID**: ${story.topic_id}\n`;
624
+ md += `**Statements**: ${story.coverage.statements_total} statements, ${story.coverage.requirements_total} requirements\n\n`;
625
+
626
+ // User story
627
+ md += `## User Story\n`;
628
+ md += `**As a** ${story.user_story.user_type} \`[${story.user_story.user_type_source}]\`\n`;
629
+ md += `**I want** to ${story.user_story.action} \`[${story.user_story.action_source}]\`\n`;
630
+ md += `**So that** I can ${story.user_story.benefit} \`[${story.user_story.benefit_source}]\`\n\n`;
631
+
632
+ // Description
633
+ md += `## Description\n`;
634
+ md += `${story.description.text}\n\n`;
635
+ if (story.description.source_statements.length > 0) {
636
+ md += `**Source statements:** ${story.description.source_statements.join(', ')}\n\n`;
637
+ }
638
+
639
+ // Acceptance criteria
640
+ md += `## Acceptance Criteria\n\n`;
641
+ for (const ac of story.acceptance_criteria) {
642
+ md += `### Scenario ${ac.id.replace('AC-', '')}: ${ac.scenario}\n`;
643
+ md += `**Given** ${ac.given.text} \`[${ac.given.source}]\`\n`;
644
+ md += `**When** ${ac.when.text} \`[${ac.when.source}]\`\n`;
645
+ md += `**Then** ${ac.then.text} \`[${ac.then.source}]\`\n\n`;
646
+
647
+ if (ac.originalText) {
648
+ md += `**Derived from:**\n`;
649
+ md += `> "${ac.originalText.slice(0, 100)}${ac.originalText.length > 100 ? '...' : ''}" — ${ac.sources.join(', ')}\n\n`;
650
+ }
651
+ }
652
+
653
+ // Coverage
654
+ md += `## Coverage\n`;
655
+ md += `- **Statements covered**: ${story.coverage.statements_covered}/${story.coverage.statements_total}\n`;
656
+ md += `- **Requirements coverage**: ${story.coverage.coverage_percent}%\n`;
657
+ md += `- **Clarifications used**: ${story.coverage.clarifications_used}\n`;
658
+ if (story.validation.warnings.length === 0) {
659
+ md += `- **Assumptions**: NONE\n`;
660
+ } else {
661
+ const assumptions = story.validation.warnings.filter(w => w.type === 'assumption');
662
+ if (assumptions.length > 0) {
663
+ md += `- **Potential assumptions**: ${assumptions.length}\n`;
664
+ }
665
+ }
666
+ md += '\n';
667
+
668
+ // Traceability matrix
669
+ md += `## Traceability Matrix\n`;
670
+ md += `| Criterion | Source | Type |\n`;
671
+ md += `|-----------|--------|------|\n`;
672
+ for (const row of story.traceability) {
673
+ md += `| ${row.criterion_id} | ${row.source_id} | ${row.source_type} |\n`;
674
+ }
675
+ md += '\n';
676
+
677
+ // Complexity
678
+ md += `## Complexity\n`;
679
+ md += `**Score**: ${story.complexity.score || 'N/A'} (${story.complexity.level || 'unknown'})\n`;
680
+ if (story.complexity.estimated_stories) {
681
+ md += `**Estimated stories**: ${story.complexity.estimated_stories}\n`;
682
+ }
683
+
684
+ return md;
685
+ }
686
+
687
+ // ==========================================================================
688
+ // E3-S3: One-by-One Presentation Flow
689
+ // ==========================================================================
690
+
691
+ /**
692
+ * Load presentation queue
693
+ */
694
+ function loadQueue() {
695
+ const activeDigest = loadActiveDigest();
696
+ if (!activeDigest.session.digest_path) {
697
+ return null;
698
+ }
699
+
700
+ const queuePath = path.join(activeDigest.session.digest_path, 'presentation-queue.json');
701
+ try {
702
+ return JSON.parse(fs.readFileSync(queuePath, 'utf8'));
703
+ } catch (err) {
704
+ return null;
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Save presentation queue
710
+ */
711
+ function saveQueue(queue) {
712
+ const activeDigest = loadActiveDigest();
713
+ if (!activeDigest.session.digest_path) {
714
+ return { error: 'No active digest session' };
715
+ }
716
+
717
+ const queuePath = path.join(activeDigest.session.digest_path, 'presentation-queue.json');
718
+ fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2));
719
+ return { saved: true };
720
+ }
721
+
722
+ /**
723
+ * Initialize presentation queue from generated stories
724
+ */
725
+ function initializePresentation() {
726
+ const stories = loadAllStories();
727
+ if (stories.length === 0) {
728
+ return { error: 'No stories to present. Run generate-stories first.' };
729
+ }
730
+
731
+ const activeDigest = loadActiveDigest();
732
+
733
+ const queue = {
734
+ session_id: activeDigest.session.id,
735
+ presentation: {
736
+ status: 'in_progress',
737
+ started_at: now(),
738
+ current_index: 0,
739
+ current_story_id: null
740
+ },
741
+ stories: stories.map(s => ({
742
+ id: s.id,
743
+ topic_id: s.topic_id,
744
+ title: s.title,
745
+ criteria_count: s.acceptance_criteria.length,
746
+ coverage: s.coverage.coverage_percent,
747
+ status: 'pending'
748
+ })),
749
+ summary: {
750
+ total: stories.length,
751
+ approved: 0,
752
+ rejected: 0,
753
+ skipped: 0,
754
+ pending: stories.length,
755
+ presenting: 0
756
+ }
757
+ };
758
+
759
+ saveQueue(queue);
760
+ return queue;
761
+ }
762
+
763
+ /**
764
+ * Get presentation status
765
+ */
766
+ function getPresentationStatus() {
767
+ const queue = loadQueue();
768
+ if (!queue) {
769
+ return { active: false };
770
+ }
771
+
772
+ return {
773
+ active: true,
774
+ status: queue.presentation.status,
775
+ progress: {
776
+ reviewed: queue.summary.approved + queue.summary.rejected,
777
+ remaining: queue.summary.pending + queue.summary.skipped,
778
+ total: queue.summary.total
779
+ },
780
+ current: queue.presentation.current_story_id,
781
+ summary: queue.summary
782
+ };
783
+ }
784
+
785
+ /**
786
+ * Get the next story to present
787
+ */
788
+ function getNextStory() {
789
+ let queue = loadQueue();
790
+
791
+ // Initialize if no queue exists
792
+ if (!queue) {
793
+ queue = initializePresentation();
794
+ if (queue.error) return queue;
795
+ }
796
+
797
+ // Mark any currently presenting story as skipped (interrupted)
798
+ const currentlyPresenting = queue.stories.find(s => s.status === 'presenting');
799
+ if (currentlyPresenting) {
800
+ currentlyPresenting.status = 'skipped';
801
+ currentlyPresenting.skipped_at = now();
802
+ queue.summary.presenting--;
803
+ queue.summary.skipped++;
804
+ }
805
+
806
+ // Find first pending story (prefer pending over skipped)
807
+ let nextIndex = queue.stories.findIndex(s => s.status === 'pending');
808
+
809
+ // If no pending, try skipped
810
+ if (nextIndex === -1) {
811
+ nextIndex = queue.stories.findIndex(s => s.status === 'skipped');
812
+ }
813
+
814
+ // All done
815
+ if (nextIndex === -1) {
816
+ queue.presentation.status = 'completed';
817
+ queue.presentation.completed_at = now();
818
+ saveQueue(queue);
819
+ return { complete: true, summary: queue.summary };
820
+ }
821
+
822
+ // Mark as presenting
823
+ const entry = queue.stories[nextIndex];
824
+ const wasSkipped = entry.status === 'skipped';
825
+ entry.status = 'presenting';
826
+ entry.presented_at = now();
827
+ if (wasSkipped) {
828
+ queue.summary.skipped--;
829
+ } else {
830
+ queue.summary.pending--;
831
+ }
832
+ queue.summary.presenting++;
833
+
834
+ queue.presentation.current_index = nextIndex;
835
+ queue.presentation.current_story_id = entry.id;
836
+
837
+ saveQueue(queue);
838
+
839
+ // Load full story
840
+ const story = loadStory(entry.id);
841
+
842
+ return {
843
+ index: nextIndex + 1,
844
+ total: queue.stories.length,
845
+ story,
846
+ queue_entry: entry,
847
+ summary: queue.summary
848
+ };
849
+ }
850
+
851
+ /**
852
+ * Get current story being presented
853
+ */
854
+ function getCurrentStory() {
855
+ const queue = loadQueue();
856
+ if (!queue) {
857
+ return { error: 'No presentation in progress' };
858
+ }
859
+
860
+ const currentEntry = queue.stories.find(s => s.status === 'presenting');
861
+ if (!currentEntry) {
862
+ return { error: 'No story currently being presented' };
863
+ }
864
+
865
+ const story = loadStory(currentEntry.id);
866
+ const index = queue.stories.indexOf(currentEntry);
867
+
868
+ return {
869
+ index: index + 1,
870
+ total: queue.stories.length,
871
+ story,
872
+ queue_entry: currentEntry,
873
+ summary: queue.summary
874
+ };
875
+ }
876
+
877
+ /**
878
+ * Approve current story
879
+ */
880
+ function approveCurrentStory() {
881
+ const queue = loadQueue();
882
+ if (!queue) {
883
+ return { error: 'No presentation in progress' };
884
+ }
885
+
886
+ const entry = queue.stories.find(s => s.status === 'presenting');
887
+ if (!entry) {
888
+ return { error: 'No story currently being presented' };
889
+ }
890
+
891
+ entry.status = 'approved';
892
+ entry.decided_at = now();
893
+
894
+ queue.summary.approved++;
895
+ queue.summary.presenting--;
896
+
897
+ saveQueue(queue);
898
+
899
+ return { success: true, story_id: entry.id, title: entry.title };
900
+ }
901
+
902
+ /**
903
+ * Reject current story with reason
904
+ */
905
+ function rejectCurrentStory(reason) {
906
+ const queue = loadQueue();
907
+ if (!queue) {
908
+ return { error: 'No presentation in progress' };
909
+ }
910
+
911
+ const entry = queue.stories.find(s => s.status === 'presenting');
912
+ if (!entry) {
913
+ return { error: 'No story currently being presented' };
914
+ }
915
+
916
+ entry.status = 'rejected';
917
+ entry.decided_at = now();
918
+ entry.rejection_reason = reason || 'No reason provided';
919
+
920
+ queue.summary.rejected++;
921
+ queue.summary.presenting--;
922
+
923
+ saveQueue(queue);
924
+
925
+ return { success: true, story_id: entry.id, title: entry.title, reason: entry.rejection_reason };
926
+ }
927
+
928
+ /**
929
+ * Skip current story for later
930
+ */
931
+ function skipCurrentStory() {
932
+ const queue = loadQueue();
933
+ if (!queue) {
934
+ return { error: 'No presentation in progress' };
935
+ }
936
+
937
+ const entry = queue.stories.find(s => s.status === 'presenting');
938
+ if (!entry) {
939
+ return { error: 'No story currently being presented' };
940
+ }
941
+
942
+ entry.status = 'skipped';
943
+ entry.skipped_at = now();
944
+
945
+ queue.summary.skipped++;
946
+ queue.summary.presenting--;
947
+
948
+ saveQueue(queue);
949
+
950
+ return { success: true, story_id: entry.id, title: entry.title };
951
+ }
952
+
953
+ /**
954
+ * Format story summary for presentation (compact view)
955
+ */
956
+ function formatStorySummary(storyData) {
957
+ const { index, total, story, summary } = storyData;
958
+
959
+ let output = '';
960
+
961
+ // Header box
962
+ output += `${'═'.repeat(64)}\n`;
963
+ output += ` Story ${index} of ${total}: ${story.title}\n`;
964
+ output += `${'═'.repeat(64)}\n\n`;
965
+
966
+ // User story
967
+ output += ` As a ${story.user_story.user_type.toUpperCase()},\n`;
968
+ output += ` I want to ${story.user_story.action.toUpperCase()}\n`;
969
+ output += ` So that I can ${story.user_story.benefit}\n\n`;
970
+
971
+ // Stats
972
+ output += ` Acceptance Criteria: ${story.acceptance_criteria.length}\n`;
973
+ output += ` Coverage: ${story.coverage.coverage_percent}%`;
974
+ if (story.validation.warnings.length === 0) {
975
+ output += ' (no assumptions)';
976
+ }
977
+ output += '\n';
978
+
979
+ if (story.complexity && story.complexity.score) {
980
+ output += ` Complexity: ${story.complexity.level} (${story.complexity.score})\n`;
981
+ }
982
+
983
+ output += `\n${'─'.repeat(64)}\n`;
984
+
985
+ // Progress
986
+ output += ` Progress: ${summary.approved} approved, ${summary.rejected} rejected, ${summary.pending + summary.skipped} remaining\n`;
987
+
988
+ output += `${'─'.repeat(64)}\n`;
989
+
990
+ return output;
991
+ }
992
+
993
+ /**
994
+ * Format presentation actions prompt
995
+ */
996
+ function formatActionsPrompt() {
997
+ return `\nActions: [a]pprove [r]eject [s]kip [v]iew full [n]ext [q]uit\n`;
998
+ }
999
+
1000
+ /**
1001
+ * Get completion summary
1002
+ */
1003
+ function getCompletionSummary() {
1004
+ const queue = loadQueue();
1005
+ if (!queue) {
1006
+ return { error: 'No presentation data' };
1007
+ }
1008
+
1009
+ const approved = queue.stories.filter(s => s.status === 'approved');
1010
+ const rejected = queue.stories.filter(s => s.status === 'rejected');
1011
+ const skipped = queue.stories.filter(s => s.status === 'skipped');
1012
+ const pending = queue.stories.filter(s => s.status === 'pending');
1013
+
1014
+ return {
1015
+ complete: queue.presentation.status === 'completed',
1016
+ summary: queue.summary,
1017
+ approved: approved.map(s => ({ id: s.id, title: s.title })),
1018
+ rejected: rejected.map(s => ({ id: s.id, title: s.title, reason: s.rejection_reason })),
1019
+ skipped: skipped.map(s => ({ id: s.id, title: s.title })),
1020
+ pending: pending.map(s => ({ id: s.id, title: s.title }))
1021
+ };
1022
+ }
1023
+
1024
+ /**
1025
+ * Reset presentation (start over)
1026
+ */
1027
+ function resetPresentation() {
1028
+ const queue = loadQueue();
1029
+ if (!queue) {
1030
+ return { error: 'No presentation to reset' };
1031
+ }
1032
+
1033
+ // Reset all stories to pending
1034
+ for (const entry of queue.stories) {
1035
+ entry.status = 'pending';
1036
+ delete entry.presented_at;
1037
+ delete entry.decided_at;
1038
+ delete entry.skipped_at;
1039
+ delete entry.rejection_reason;
1040
+ }
1041
+
1042
+ queue.presentation = {
1043
+ status: 'in_progress',
1044
+ started_at: now(),
1045
+ current_index: 0,
1046
+ current_story_id: null
1047
+ };
1048
+
1049
+ queue.summary = {
1050
+ total: queue.stories.length,
1051
+ approved: 0,
1052
+ rejected: 0,
1053
+ skipped: 0,
1054
+ pending: queue.stories.length,
1055
+ presenting: 0
1056
+ };
1057
+
1058
+ saveQueue(queue);
1059
+
1060
+ return { success: true, total: queue.stories.length };
1061
+ }
1062
+
1063
+ // ============================================================================
1064
+ // E3-S4: Edit and Change Handling
1065
+ // ============================================================================
1066
+
1067
+ /**
1068
+ * Generate unique edit session ID
1069
+ */
1070
+ function generateEditSessionId() {
1071
+ return 'edit-' + crypto.randomBytes(4).toString('hex');
1072
+ }
1073
+
1074
+ /**
1075
+ * Generate unique change ID
1076
+ */
1077
+ function generateChangeId() {
1078
+ return 'change-' + crypto.randomBytes(3).toString('hex');
1079
+ }
1080
+
1081
+ /**
1082
+ * Load edit sessions data
1083
+ */
1084
+ function loadEditSessions() {
1085
+ const activeDigest = loadActiveDigest();
1086
+ if (!activeDigest.session.digest_path) {
1087
+ return null;
1088
+ }
1089
+
1090
+ const sessionsPath = path.join(activeDigest.session.digest_path, 'edit-sessions.json');
1091
+ try {
1092
+ return JSON.parse(fs.readFileSync(sessionsPath, 'utf8'));
1093
+ } catch (err) {
1094
+ return { active_session: null, sessions: [] };
1095
+ }
1096
+ }
1097
+
1098
+ /**
1099
+ * Save edit sessions data
1100
+ */
1101
+ function saveEditSessions(data) {
1102
+ const activeDigest = loadActiveDigest();
1103
+ if (!activeDigest.session.digest_path) {
1104
+ return { error: 'No active digest session' };
1105
+ }
1106
+
1107
+ const sessionsPath = path.join(activeDigest.session.digest_path, 'edit-sessions.json');
1108
+ writeJson(sessionsPath, data);
1109
+ return { saved: true };
1110
+ }
1111
+
1112
+ /**
1113
+ * Start an edit session for a story
1114
+ */
1115
+ function startEditSession(storyId, reason) {
1116
+ const story = loadStory(storyId);
1117
+ if (!story) {
1118
+ return { error: `Story ${storyId} not found` };
1119
+ }
1120
+
1121
+ const sessionsData = loadEditSessions() || { active_session: null, sessions: [] };
1122
+
1123
+ // Check if there's already an active session
1124
+ if (sessionsData.active_session && sessionsData.active_session.active) {
1125
+ return {
1126
+ error: 'An edit session is already active',
1127
+ active_session: sessionsData.active_session
1128
+ };
1129
+ }
1130
+
1131
+ // Get rejection reason if story was rejected
1132
+ const queue = loadQueue();
1133
+ const queueEntry = queue?.stories.find(s => s.id === storyId);
1134
+ const rejectionReason = queueEntry?.rejection_reason;
1135
+
1136
+ const session = {
1137
+ id: generateEditSessionId(),
1138
+ story_id: storyId,
1139
+ started_at: now(),
1140
+ trigger: reason || (queueEntry?.status === 'rejected' ? 'rejection' : 'manual'),
1141
+ rejection_reason: rejectionReason,
1142
+ original_status: queueEntry?.status || 'unknown',
1143
+ changes: [],
1144
+ active: true
1145
+ };
1146
+
1147
+ sessionsData.active_session = session;
1148
+ saveEditSessions(sessionsData);
1149
+
1150
+ return {
1151
+ session,
1152
+ story,
1153
+ rejection_reason: rejectionReason,
1154
+ editable_sections: [
1155
+ 'user_story',
1156
+ 'acceptance_criteria',
1157
+ 'technical_notes',
1158
+ 'description'
1159
+ ]
1160
+ };
1161
+ }
1162
+
1163
+ /**
1164
+ * Get active edit session
1165
+ */
1166
+ function getActiveEditSession() {
1167
+ const sessionsData = loadEditSessions();
1168
+ if (!sessionsData || !sessionsData.active_session || !sessionsData.active_session.active) {
1169
+ return null;
1170
+ }
1171
+ return sessionsData.active_session;
1172
+ }
1173
+
1174
+ /**
1175
+ * Record a change in the active edit session
1176
+ */
1177
+ function recordChange(change) {
1178
+ const sessionsData = loadEditSessions();
1179
+ if (!sessionsData || !sessionsData.active_session || !sessionsData.active_session.active) {
1180
+ return { error: 'No active edit session' };
1181
+ }
1182
+
1183
+ change.id = generateChangeId();
1184
+ change.timestamp = now();
1185
+
1186
+ sessionsData.active_session.changes.push(change);
1187
+ saveEditSessions(sessionsData);
1188
+
1189
+ return { recorded: true, change };
1190
+ }
1191
+
1192
+ /**
1193
+ * Edit user story fields
1194
+ */
1195
+ function editUserStory(storyId, updates) {
1196
+ const session = getActiveEditSession();
1197
+ if (!session || session.story_id !== storyId) {
1198
+ return { error: 'No active edit session for this story. Run edit-story first.' };
1199
+ }
1200
+
1201
+ const story = loadStory(storyId);
1202
+ if (!story) {
1203
+ return { error: `Story ${storyId} not found` };
1204
+ }
1205
+
1206
+ const changes = [];
1207
+
1208
+ if (updates.user_type && updates.user_type !== story.user_story.user_type) {
1209
+ changes.push({
1210
+ type: 'user_story_modified',
1211
+ section: 'user_story',
1212
+ field: 'user_type',
1213
+ before: story.user_story.user_type,
1214
+ after: updates.user_type
1215
+ });
1216
+ story.user_story.user_type = updates.user_type;
1217
+ story.user_story.user_type_source = 'manual';
1218
+ }
1219
+
1220
+ if (updates.action && updates.action !== story.user_story.action) {
1221
+ changes.push({
1222
+ type: 'user_story_modified',
1223
+ section: 'user_story',
1224
+ field: 'action',
1225
+ before: story.user_story.action,
1226
+ after: updates.action
1227
+ });
1228
+ story.user_story.action = updates.action;
1229
+ story.user_story.action_source = 'manual';
1230
+ }
1231
+
1232
+ if (updates.benefit && updates.benefit !== story.user_story.benefit) {
1233
+ changes.push({
1234
+ type: 'user_story_modified',
1235
+ section: 'user_story',
1236
+ field: 'benefit',
1237
+ before: story.user_story.benefit,
1238
+ after: updates.benefit
1239
+ });
1240
+ story.user_story.benefit = updates.benefit;
1241
+ story.user_story.benefit_source = 'manual';
1242
+ }
1243
+
1244
+ // Record all changes
1245
+ for (const change of changes) {
1246
+ recordChange(change);
1247
+ }
1248
+
1249
+ // Save the story (uncommitted until commit-edit)
1250
+ saveStory(story);
1251
+
1252
+ return { success: true, story, changes };
1253
+ }
1254
+
1255
+ /**
1256
+ * Edit a specific acceptance criterion
1257
+ */
1258
+ function editCriterion(storyId, criterionId, updates) {
1259
+ const session = getActiveEditSession();
1260
+ if (!session || session.story_id !== storyId) {
1261
+ return { error: 'No active edit session for this story. Run edit-story first.' };
1262
+ }
1263
+
1264
+ const story = loadStory(storyId);
1265
+ if (!story) {
1266
+ return { error: `Story ${storyId} not found` };
1267
+ }
1268
+
1269
+ const criterion = story.acceptance_criteria.find(ac => ac.id === criterionId);
1270
+ if (!criterion) {
1271
+ return { error: `Criterion ${criterionId} not found in story ${storyId}` };
1272
+ }
1273
+
1274
+ const changes = [];
1275
+
1276
+ if (updates.scenario && updates.scenario !== criterion.scenario) {
1277
+ changes.push({
1278
+ type: 'criteria_modified',
1279
+ target: criterionId,
1280
+ field: 'scenario',
1281
+ before: criterion.scenario,
1282
+ after: updates.scenario
1283
+ });
1284
+ criterion.scenario = updates.scenario;
1285
+ }
1286
+
1287
+ if (updates.given) {
1288
+ const beforeText = criterion.given?.text || '';
1289
+ if (updates.given !== beforeText) {
1290
+ changes.push({
1291
+ type: 'criteria_modified',
1292
+ target: criterionId,
1293
+ field: 'given',
1294
+ before: beforeText,
1295
+ after: updates.given
1296
+ });
1297
+ criterion.given = { text: updates.given, source: 'manual' };
1298
+ }
1299
+ }
1300
+
1301
+ if (updates.when) {
1302
+ const beforeText = criterion.when?.text || '';
1303
+ if (updates.when !== beforeText) {
1304
+ changes.push({
1305
+ type: 'criteria_modified',
1306
+ target: criterionId,
1307
+ field: 'when',
1308
+ before: beforeText,
1309
+ after: updates.when
1310
+ });
1311
+ criterion.when = { text: updates.when, source: 'manual' };
1312
+ }
1313
+ }
1314
+
1315
+ if (updates.then) {
1316
+ const beforeText = criterion.then?.text || '';
1317
+ if (updates.then !== beforeText) {
1318
+ changes.push({
1319
+ type: 'criteria_modified',
1320
+ target: criterionId,
1321
+ field: 'then',
1322
+ before: beforeText,
1323
+ after: updates.then
1324
+ });
1325
+ criterion.then = { text: updates.then, source: 'manual' };
1326
+ }
1327
+ }
1328
+
1329
+ // Record all changes
1330
+ for (const change of changes) {
1331
+ recordChange(change);
1332
+ }
1333
+
1334
+ // Save the story
1335
+ saveStory(story);
1336
+
1337
+ return { success: true, story, criterion, changes };
1338
+ }
1339
+
1340
+ /**
1341
+ * Add a new acceptance criterion
1342
+ */
1343
+ function addCriterion(storyId, criterion) {
1344
+ const session = getActiveEditSession();
1345
+ if (!session || session.story_id !== storyId) {
1346
+ return { error: 'No active edit session for this story. Run edit-story first.' };
1347
+ }
1348
+
1349
+ const story = loadStory(storyId);
1350
+ if (!story) {
1351
+ return { error: `Story ${storyId} not found` };
1352
+ }
1353
+
1354
+ // Generate new AC ID
1355
+ const existingIds = story.acceptance_criteria.map(ac =>
1356
+ parseInt(ac.id.replace('AC-', ''), 10) || 0
1357
+ );
1358
+ const maxId = existingIds.length > 0 ? Math.max(...existingIds) : 0;
1359
+ const newId = `AC-${maxId + 1}`;
1360
+
1361
+ const newCriterion = {
1362
+ id: newId,
1363
+ scenario: criterion.scenario || `Scenario ${maxId + 1}`,
1364
+ given: { text: criterion.given || 'the system is ready', source: 'manual' },
1365
+ when: { text: criterion.when || 'the user performs the action', source: 'manual' },
1366
+ then: { text: criterion.then || 'the expected result occurs', source: 'manual' },
1367
+ and: criterion.and?.map(a => ({ text: a, source: 'manual' })) || [],
1368
+ derived_from: [{ id: 'manual', text: 'Manually added criterion' }]
1369
+ };
1370
+
1371
+ story.acceptance_criteria.push(newCriterion);
1372
+
1373
+ // Record the change
1374
+ recordChange({
1375
+ type: 'criteria_added',
1376
+ target: newId,
1377
+ before: null,
1378
+ after: newCriterion
1379
+ });
1380
+
1381
+ // Save the story
1382
+ saveStory(story);
1383
+
1384
+ return { success: true, story, criterion: newCriterion };
1385
+ }
1386
+
1387
+ /**
1388
+ * Remove an acceptance criterion
1389
+ */
1390
+ function removeCriterion(storyId, criterionId, reason) {
1391
+ const session = getActiveEditSession();
1392
+ if (!session || session.story_id !== storyId) {
1393
+ return { error: 'No active edit session for this story. Run edit-story first.' };
1394
+ }
1395
+
1396
+ const story = loadStory(storyId);
1397
+ if (!story) {
1398
+ return { error: `Story ${storyId} not found` };
1399
+ }
1400
+
1401
+ const index = story.acceptance_criteria.findIndex(ac => ac.id === criterionId);
1402
+ if (index === -1) {
1403
+ return { error: `Criterion ${criterionId} not found in story ${storyId}` };
1404
+ }
1405
+
1406
+ // Don't allow removing last criterion
1407
+ if (story.acceptance_criteria.length === 1) {
1408
+ return { error: 'Cannot remove the last acceptance criterion' };
1409
+ }
1410
+
1411
+ const removed = story.acceptance_criteria.splice(index, 1)[0];
1412
+
1413
+ // Record the change
1414
+ recordChange({
1415
+ type: 'criteria_removed',
1416
+ target: criterionId,
1417
+ before: removed,
1418
+ after: null,
1419
+ reason: reason || 'Removed by user'
1420
+ });
1421
+
1422
+ // Save the story
1423
+ saveStory(story);
1424
+
1425
+ return { success: true, story, removed };
1426
+ }
1427
+
1428
+ /**
1429
+ * Validate an edited story
1430
+ */
1431
+ function validateEditedStory(story) {
1432
+ const warnings = [];
1433
+ const errors = [];
1434
+
1435
+ // Check user story completeness
1436
+ if (!story.user_story.user_type || story.user_story.user_type === '') {
1437
+ errors.push({ field: 'user_story.user_type', message: 'User type is required' });
1438
+ }
1439
+ if (!story.user_story.action || story.user_story.action === '') {
1440
+ errors.push({ field: 'user_story.action', message: 'Action is required' });
1441
+ }
1442
+
1443
+ // Check acceptance criteria
1444
+ if (!story.acceptance_criteria || story.acceptance_criteria.length === 0) {
1445
+ errors.push({ field: 'acceptance_criteria', message: 'At least one acceptance criterion required' });
1446
+ }
1447
+
1448
+ for (const ac of story.acceptance_criteria || []) {
1449
+ if (!ac.given?.text) {
1450
+ warnings.push({ field: `${ac.id}.given`, message: 'Given clause is empty' });
1451
+ }
1452
+ if (!ac.when?.text) {
1453
+ warnings.push({ field: `${ac.id}.when`, message: 'When clause is empty' });
1454
+ }
1455
+ if (!ac.then?.text) {
1456
+ warnings.push({ field: `${ac.id}.then`, message: 'Then clause is empty' });
1457
+ }
1458
+ }
1459
+
1460
+ // Check for manual-only coverage (all criteria manually added)
1461
+ const manualOnlyCriteria = (story.acceptance_criteria || []).filter(ac =>
1462
+ ac.given?.source === 'manual' &&
1463
+ ac.when?.source === 'manual' &&
1464
+ ac.then?.source === 'manual'
1465
+ );
1466
+
1467
+ if (manualOnlyCriteria.length === story.acceptance_criteria?.length) {
1468
+ warnings.push({
1469
+ field: 'coverage',
1470
+ message: 'All criteria are manually added - no traceability to original transcript'
1471
+ });
1472
+ }
1473
+
1474
+ return {
1475
+ valid: errors.length === 0,
1476
+ errors,
1477
+ warnings
1478
+ };
1479
+ }
1480
+
1481
+ /**
1482
+ * Recalculate coverage after edits
1483
+ */
1484
+ function recalculateCoverage(story) {
1485
+ const totalCriteria = story.acceptance_criteria.length;
1486
+ let tracedCriteria = 0;
1487
+
1488
+ for (const ac of story.acceptance_criteria) {
1489
+ // Count criteria with at least one non-manual source
1490
+ const sources = [ac.given?.source, ac.when?.source, ac.then?.source];
1491
+ if (sources.some(s => s && s !== 'manual' && s !== 'context' && s !== 'inferred')) {
1492
+ tracedCriteria++;
1493
+ }
1494
+ }
1495
+
1496
+ return {
1497
+ statements_total: story.coverage?.statements_total || 0,
1498
+ statements_covered: story.coverage?.statements_covered || 0,
1499
+ coverage_percent: totalCriteria > 0 ? Math.round((tracedCriteria / totalCriteria) * 100) : 0,
1500
+ clarifications_used: story.coverage?.clarifications_used || 0,
1501
+ manual_criteria: totalCriteria - tracedCriteria,
1502
+ assumptions: story.coverage?.assumptions || []
1503
+ };
1504
+ }
1505
+
1506
+ /**
1507
+ * Update queue after edit is committed
1508
+ */
1509
+ function updateQueueAfterEdit(storyId) {
1510
+ const queue = loadQueue();
1511
+ if (!queue) return { error: 'No queue found' };
1512
+
1513
+ const entry = queue.stories.find(s => s.id === storyId);
1514
+ if (!entry) return { error: 'Story not found in queue' };
1515
+
1516
+ // Track previous status
1517
+ const previousStatus = entry.status;
1518
+
1519
+ // Update summary counts
1520
+ if (previousStatus === 'rejected') {
1521
+ queue.summary.rejected--;
1522
+ queue.summary.pending++;
1523
+ } else if (previousStatus === 'approved') {
1524
+ queue.summary.approved--;
1525
+ queue.summary.pending++;
1526
+ } else if (previousStatus === 'skipped') {
1527
+ queue.summary.skipped--;
1528
+ queue.summary.pending++;
1529
+ }
1530
+
1531
+ // Reset entry to pending
1532
+ entry.status = 'pending';
1533
+ entry.edited_at = now();
1534
+ delete entry.rejection_reason;
1535
+ delete entry.decided_at;
1536
+
1537
+ // Reset presentation status if complete
1538
+ if (queue.presentation.status === 'completed') {
1539
+ queue.presentation.status = 'in_progress';
1540
+ }
1541
+
1542
+ saveQueue(queue);
1543
+
1544
+ return { previous_status: previousStatus, new_status: 'pending' };
1545
+ }
1546
+
1547
+ /**
1548
+ * Commit edit session and return story to review queue
1549
+ */
1550
+ function commitEditSession() {
1551
+ const sessionsData = loadEditSessions();
1552
+ if (!sessionsData || !sessionsData.active_session || !sessionsData.active_session.active) {
1553
+ return { error: 'No active edit session to commit' };
1554
+ }
1555
+
1556
+ const session = sessionsData.active_session;
1557
+ const story = loadStory(session.story_id);
1558
+
1559
+ if (!story) {
1560
+ return { error: 'Story not found' };
1561
+ }
1562
+
1563
+ // Validate the edited story
1564
+ const validation = validateEditedStory(story);
1565
+ if (!validation.valid) {
1566
+ return {
1567
+ error: 'Story validation failed',
1568
+ errors: validation.errors,
1569
+ warnings: validation.warnings
1570
+ };
1571
+ }
1572
+
1573
+ // Mark session complete
1574
+ session.completed_at = now();
1575
+ session.active = false;
1576
+ session.changes_count = session.changes.length;
1577
+
1578
+ // Update story with edit history
1579
+ if (!story.edit_history) {
1580
+ story.edit_history = [];
1581
+ }
1582
+ story.edit_history.push({
1583
+ session_id: session.id,
1584
+ timestamp: session.completed_at,
1585
+ changes_count: session.changes_count,
1586
+ trigger: session.trigger
1587
+ });
1588
+
1589
+ // Recalculate coverage
1590
+ story.coverage = recalculateCoverage(story);
1591
+ story.last_edited = now();
1592
+
1593
+ // Save story
1594
+ saveStory(story);
1595
+
1596
+ // Move session to history
1597
+ sessionsData.sessions.push(session);
1598
+ sessionsData.active_session = null;
1599
+ saveEditSessions(sessionsData);
1600
+
1601
+ // Update presentation queue
1602
+ const queueUpdate = updateQueueAfterEdit(story.id);
1603
+
1604
+ return {
1605
+ success: true,
1606
+ story_id: story.id,
1607
+ changes_made: session.changes_count,
1608
+ previous_status: queueUpdate.previous_status,
1609
+ new_status: 'pending',
1610
+ validation_warnings: validation.warnings
1611
+ };
1612
+ }
1613
+
1614
+ /**
1615
+ * Cancel edit session and discard changes
1616
+ */
1617
+ function cancelEditSession() {
1618
+ const sessionsData = loadEditSessions();
1619
+ if (!sessionsData || !sessionsData.active_session || !sessionsData.active_session.active) {
1620
+ return { error: 'No active edit session to cancel' };
1621
+ }
1622
+
1623
+ const session = sessionsData.active_session;
1624
+ const changesCount = session.changes.length;
1625
+
1626
+ // Mark session as cancelled
1627
+ session.cancelled_at = now();
1628
+ session.active = false;
1629
+ session.cancelled = true;
1630
+
1631
+ // Move to history
1632
+ sessionsData.sessions.push(session);
1633
+ sessionsData.active_session = null;
1634
+ saveEditSessions(sessionsData);
1635
+
1636
+ // Note: We don't revert the story file here. The changes were saved as they were made.
1637
+ // A proper implementation would need to store the original story state and restore it.
1638
+ // For simplicity, we just mark the session as cancelled.
1639
+
1640
+ return { success: true, discarded_changes: changesCount };
1641
+ }
1642
+
1643
+ /**
1644
+ * Get changes in current edit session
1645
+ */
1646
+ function getEditChanges() {
1647
+ const session = getActiveEditSession();
1648
+ if (!session) {
1649
+ return { error: 'No active edit session' };
1650
+ }
1651
+
1652
+ return {
1653
+ session_id: session.id,
1654
+ story_id: session.story_id,
1655
+ started_at: session.started_at,
1656
+ trigger: session.trigger,
1657
+ rejection_reason: session.rejection_reason,
1658
+ changes: session.changes,
1659
+ changes_count: session.changes.length
1660
+ };
1661
+ }
1662
+
1663
+ /**
1664
+ * Get edit history for a story
1665
+ */
1666
+ function getEditHistory(storyId) {
1667
+ const story = loadStory(storyId);
1668
+ if (!story) {
1669
+ return { error: `Story ${storyId} not found` };
1670
+ }
1671
+
1672
+ const sessionsData = loadEditSessions() || { sessions: [] };
1673
+
1674
+ // Filter sessions for this story
1675
+ const storySessions = sessionsData.sessions.filter(s => s.story_id === storyId);
1676
+
1677
+ return {
1678
+ story_id: storyId,
1679
+ title: story.title,
1680
+ edit_count: storySessions.length,
1681
+ sessions: storySessions.map(s => ({
1682
+ session_id: s.id,
1683
+ timestamp: s.completed_at || s.cancelled_at,
1684
+ trigger: s.trigger,
1685
+ changes_count: s.changes_count || s.changes.length,
1686
+ cancelled: s.cancelled || false
1687
+ })),
1688
+ story_edit_history: story.edit_history || []
1689
+ };
1690
+ }
1691
+
1692
+ /**
1693
+ * List stories that can be edited (rejected or approved)
1694
+ */
1695
+ function listEditableStories() {
1696
+ const queue = loadQueue();
1697
+ if (!queue) {
1698
+ return { error: 'No presentation queue found' };
1699
+ }
1700
+
1701
+ const editable = queue.stories.filter(s =>
1702
+ s.status === 'rejected' || s.status === 'approved' || s.status === 'skipped'
1703
+ );
1704
+
1705
+ return {
1706
+ total: editable.length,
1707
+ rejected: editable.filter(s => s.status === 'rejected').map(s => ({
1708
+ id: s.id,
1709
+ title: s.title,
1710
+ status: s.status,
1711
+ rejection_reason: s.rejection_reason
1712
+ })),
1713
+ approved: editable.filter(s => s.status === 'approved').map(s => ({
1714
+ id: s.id,
1715
+ title: s.title,
1716
+ status: s.status
1717
+ })),
1718
+ skipped: editable.filter(s => s.status === 'skipped').map(s => ({
1719
+ id: s.id,
1720
+ title: s.title,
1721
+ status: s.status
1722
+ }))
1723
+ };
1724
+ }
1725
+
1726
+ // ============================================================================
1727
+ // E3-S5: ready.json Integration
1728
+ // ============================================================================
1729
+
1730
+ /**
1731
+ * Generate a workflow ID for task tracking
1732
+ */
1733
+ function generateWorkflowId() {
1734
+ return 'wf-' + crypto.randomBytes(4).toString('hex');
1735
+ }
1736
+
1737
+ /**
1738
+ * Generate sub-task ID from parent
1739
+ */
1740
+ function generateSubTaskId(parentId, index) {
1741
+ return `${parentId}-${String(index).padStart(2, '0')}`;
1742
+ }
1743
+
1744
+ /**
1745
+ * Map story complexity to task priority
1746
+ */
1747
+ const COMPLEXITY_TO_PRIORITY = {
1748
+ 'simple': 'P3',
1749
+ 'low': 'P3',
1750
+ 'medium': 'P2',
1751
+ 'high': 'P1',
1752
+ 'very_high': 'P0'
1753
+ };
1754
+
1755
+ function mapPriority(story) {
1756
+ const level = story.complexity?.level || 'medium';
1757
+ return COMPLEXITY_TO_PRIORITY[level] || 'P2';
1758
+ }
1759
+
1760
+ /**
1761
+ * Format user story description
1762
+ */
1763
+ function formatUserStoryDescription(userStory) {
1764
+ if (!userStory) return '';
1765
+ const who = userStory.user_type || 'user';
1766
+ const what = userStory.action || 'perform an action';
1767
+ const why = userStory.benefit || 'achieve my goal';
1768
+ return `As a ${who}, I want to ${what}, so that ${why}.`;
1769
+ }
1770
+
1771
+ /**
1772
+ * Convert a story to a workflow task
1773
+ */
1774
+ function convertStoryToTask(story, options = {}) {
1775
+ const taskId = options.taskId || generateWorkflowId();
1776
+ const activeDigest = loadActiveDigest();
1777
+
1778
+ return {
1779
+ id: taskId,
1780
+ title: story.title,
1781
+ type: options.type || 'story',
1782
+ parent: options.parent || null,
1783
+ epic: options.epic || null,
1784
+ status: 'ready',
1785
+ priority: mapPriority(story),
1786
+ dependencies: options.dependencies || [],
1787
+ createdAt: now(),
1788
+ source: {
1789
+ type: 'transcript-digestion',
1790
+ digest_id: activeDigest.session?.id || 'unknown',
1791
+ story_id: story.id,
1792
+ topic_id: story.topic_id
1793
+ },
1794
+ description: formatUserStoryDescription(story.user_story),
1795
+ acceptanceCriteria: (story.acceptance_criteria || []).map(ac => ({
1796
+ id: ac.id,
1797
+ scenario: ac.scenario,
1798
+ given: ac.given?.text || '',
1799
+ when: ac.when?.text || '',
1800
+ then: ac.then?.text || ''
1801
+ })),
1802
+ metadata: {
1803
+ coverage: story.coverage?.coverage_percent || 0,
1804
+ criteria_count: (story.acceptance_criteria || []).length,
1805
+ generated_at: story.generated_at
1806
+ }
1807
+ };
1808
+ }
1809
+
1810
+ /**
1811
+ * Validate stories before export
1812
+ */
1813
+ function validateForExport(stories) {
1814
+ const warnings = [];
1815
+ const errors = [];
1816
+
1817
+ for (const story of stories) {
1818
+ // Check coverage threshold
1819
+ if (story.coverage && story.coverage.coverage_percent < 50) {
1820
+ warnings.push({
1821
+ story_id: story.id,
1822
+ message: `Low coverage: ${story.coverage.coverage_percent}%`
1823
+ });
1824
+ }
1825
+
1826
+ // Check for empty criteria
1827
+ if (!story.acceptance_criteria || story.acceptance_criteria.length === 0) {
1828
+ errors.push({
1829
+ story_id: story.id,
1830
+ message: 'No acceptance criteria'
1831
+ });
1832
+ }
1833
+
1834
+ // Check for manual-only criteria
1835
+ if (story.acceptance_criteria && story.acceptance_criteria.length > 0) {
1836
+ const manualOnly = story.acceptance_criteria.every(ac =>
1837
+ ac.given?.source === 'manual' &&
1838
+ ac.when?.source === 'manual' &&
1839
+ ac.then?.source === 'manual'
1840
+ );
1841
+ if (manualOnly) {
1842
+ warnings.push({
1843
+ story_id: story.id,
1844
+ message: 'All criteria manually added - no transcript traceability'
1845
+ });
1846
+ }
1847
+ }
1848
+ }
1849
+
1850
+ return {
1851
+ valid: errors.length === 0,
1852
+ errors,
1853
+ warnings
1854
+ };
1855
+ }
1856
+
1857
+ /**
1858
+ * Export approved stories from the presentation queue
1859
+ */
1860
+ function exportApprovedStories(options = {}) {
1861
+ const queue = loadQueue();
1862
+ if (!queue) {
1863
+ return { error: 'No presentation queue found' };
1864
+ }
1865
+
1866
+ const approved = queue.stories.filter(s => s.status === 'approved');
1867
+ if (approved.length === 0) {
1868
+ return { error: 'No approved stories to export' };
1869
+ }
1870
+
1871
+ // Load and convert each story
1872
+ const tasks = [];
1873
+ const loadErrors = [];
1874
+
1875
+ for (const entry of approved) {
1876
+ const story = loadStory(entry.id);
1877
+ if (!story) {
1878
+ loadErrors.push({ id: entry.id, error: 'Story file not found' });
1879
+ continue;
1880
+ }
1881
+
1882
+ const task = convertStoryToTask(story, options);
1883
+ tasks.push(task);
1884
+ }
1885
+
1886
+ // Validate before export
1887
+ const stories = approved.map(e => loadStory(e.id)).filter(Boolean);
1888
+ const validation = validateForExport(stories);
1889
+
1890
+ return {
1891
+ tasks,
1892
+ loadErrors,
1893
+ validation,
1894
+ summary: {
1895
+ total_approved: approved.length,
1896
+ exported: tasks.length,
1897
+ failed: loadErrors.length
1898
+ }
1899
+ };
1900
+ }
1901
+
1902
+ /**
1903
+ * Create a feature task grouping multiple stories
1904
+ */
1905
+ function createFeatureTask(stories, featureName) {
1906
+ const featureId = generateWorkflowId();
1907
+
1908
+ return {
1909
+ id: featureId,
1910
+ title: featureName || `Feature: ${stories[0]?.title || 'Untitled'}`,
1911
+ type: 'parent',
1912
+ subTasks: stories.map((s, i) => generateSubTaskId(featureId, i + 1)),
1913
+ status: 'ready',
1914
+ priority: 'P2',
1915
+ dependencies: [],
1916
+ createdAt: now(),
1917
+ source: {
1918
+ type: 'transcript-digestion',
1919
+ digest_id: loadActiveDigest().session?.id,
1920
+ story_count: stories.length
1921
+ }
1922
+ };
1923
+ }
1924
+
1925
+ /**
1926
+ * Add tasks to ready.json
1927
+ */
1928
+ function addTasksToReadyJson(tasks, options = {}) {
1929
+ const readyPath = path.join(process.cwd(), '.workflow', 'state', 'ready.json');
1930
+
1931
+ let readyData;
1932
+ try {
1933
+ readyData = JSON.parse(fs.readFileSync(readyPath, 'utf8'));
1934
+ } catch (err) {
1935
+ readyData = {
1936
+ lastUpdated: now(),
1937
+ ready: [],
1938
+ inProgress: [],
1939
+ blocked: [],
1940
+ recentlyCompleted: []
1941
+ };
1942
+ }
1943
+
1944
+ // Check for duplicates by source story_id
1945
+ const existingStoryIds = new Set(
1946
+ readyData.ready
1947
+ .filter(t => t.source?.type === 'transcript-digestion')
1948
+ .map(t => t.source?.story_id)
1949
+ );
1950
+
1951
+ const newTasks = tasks.filter(t => !existingStoryIds.has(t.source?.story_id));
1952
+
1953
+ if (newTasks.length === 0) {
1954
+ return { error: 'All tasks already exist in ready.json', skipped: tasks.length };
1955
+ }
1956
+
1957
+ // Add new tasks
1958
+ readyData.ready.push(...newTasks);
1959
+ readyData.lastUpdated = now();
1960
+
1961
+ fs.writeFileSync(readyPath, JSON.stringify(readyData, null, 2));
1962
+
1963
+ return {
1964
+ success: true,
1965
+ added: newTasks.length,
1966
+ skipped: tasks.length - newTasks.length,
1967
+ total_ready: readyData.ready.length
1968
+ };
1969
+ }
1970
+
1971
+ /**
1972
+ * Format a task as markdown file
1973
+ */
1974
+ function formatTaskAsMarkdown(task) {
1975
+ let md = `# ${task.id} ${task.title}\n\n`;
1976
+
1977
+ md += `## User Story\n`;
1978
+ md += `${task.description}\n\n`;
1979
+
1980
+ md += `## Acceptance Criteria\n\n`;
1981
+ for (const ac of task.acceptanceCriteria || []) {
1982
+ md += `### ${ac.id}: ${ac.scenario}\n`;
1983
+ md += `**Given** ${ac.given}\n`;
1984
+ md += `**When** ${ac.when}\n`;
1985
+ md += `**Then** ${ac.then}\n\n`;
1986
+ }
1987
+
1988
+ md += `## Metadata\n`;
1989
+ md += `- **Priority**: ${task.priority}\n`;
1990
+ md += `- **Coverage**: ${task.metadata?.coverage || 0}%\n`;
1991
+ md += `- **Criteria Count**: ${task.metadata?.criteria_count || 0}\n`;
1992
+ md += `- **Source**: Transcript Digestion (${task.source?.digest_id || 'unknown'})\n`;
1993
+
1994
+ return md;
1995
+ }
1996
+
1997
+ /**
1998
+ * Export story files to .workflow/changes/
1999
+ */
2000
+ function exportStoryFiles(tasks, featureName = 'general') {
2001
+ const changesDir = path.join(process.cwd(), '.workflow', 'changes', featureName);
2002
+ fs.mkdirSync(changesDir, { recursive: true });
2003
+
2004
+ const exported = [];
2005
+
2006
+ for (const task of tasks) {
2007
+ const filename = `${task.id}.md`;
2008
+ const filepath = path.join(changesDir, filename);
2009
+
2010
+ const content = formatTaskAsMarkdown(task);
2011
+ fs.writeFileSync(filepath, content);
2012
+
2013
+ exported.push({ id: task.id, path: filepath });
2014
+ }
2015
+
2016
+ return { exported, directory: changesDir };
2017
+ }
2018
+
2019
+ /**
2020
+ * Preview what would be exported
2021
+ */
2022
+ function previewExport() {
2023
+ const queue = loadQueue();
2024
+ if (!queue) {
2025
+ return { error: 'No presentation queue found' };
2026
+ }
2027
+
2028
+ const approved = queue.stories.filter(s => s.status === 'approved');
2029
+ const pending = queue.stories.filter(s => s.status === 'pending' || s.status === 'skipped');
2030
+
2031
+ const stories = approved.map(e => loadStory(e.id)).filter(Boolean);
2032
+ const validation = validateForExport(stories);
2033
+
2034
+ return {
2035
+ approved_count: approved.length,
2036
+ pending_count: pending.length,
2037
+ stories: approved.map(e => {
2038
+ const story = loadStory(e.id);
2039
+ return {
2040
+ id: e.id,
2041
+ title: e.title,
2042
+ priority: story ? mapPriority(story) : 'P2',
2043
+ criteria_count: story?.acceptance_criteria?.length || 0,
2044
+ coverage: story?.coverage?.coverage_percent || 0
2045
+ };
2046
+ }),
2047
+ validation,
2048
+ ready_to_export: validation.valid && approved.length > 0
2049
+ };
2050
+ }
2051
+
2052
+ /**
2053
+ * Finalize the digestion process and export to ready.json
2054
+ */
2055
+ function finalizeDigestion(options = {}) {
2056
+ // 1. Check presentation status
2057
+ const queue = loadQueue();
2058
+ if (!queue) {
2059
+ return { error: 'No presentation queue found' };
2060
+ }
2061
+
2062
+ const pendingCount = queue.stories.filter(s =>
2063
+ s.status === 'pending' || s.status === 'skipped'
2064
+ ).length;
2065
+
2066
+ if (pendingCount > 0 && !options.force) {
2067
+ return {
2068
+ error: `${pendingCount} stories not yet reviewed. Use --force to proceed anyway.`,
2069
+ pending: pendingCount
2070
+ };
2071
+ }
2072
+
2073
+ // 2. Export approved stories
2074
+ const exportResult = exportApprovedStories(options);
2075
+ if (exportResult.error) {
2076
+ return exportResult;
2077
+ }
2078
+
2079
+ // 3. Add to ready.json
2080
+ const addResult = addTasksToReadyJson(exportResult.tasks, options);
2081
+ if (addResult.error && addResult.skipped !== exportResult.tasks.length) {
2082
+ return addResult;
2083
+ }
2084
+
2085
+ // 4. Optionally export story files
2086
+ let fileExport = null;
2087
+ if (options.exportFiles) {
2088
+ fileExport = exportStoryFiles(exportResult.tasks, options.featureName || 'digest-export');
2089
+ }
2090
+
2091
+ // 5. Mark digest as complete
2092
+ const activeDigest = loadActiveDigest();
2093
+ activeDigest.session.status = 'completed';
2094
+ activeDigest.session.completed_at = now();
2095
+ activeDigest.session.exported = {
2096
+ task_count: addResult.added || 0,
2097
+ skipped_count: addResult.skipped || 0,
2098
+ timestamp: now()
2099
+ };
2100
+ saveActiveDigest(activeDigest);
2101
+
2102
+ return {
2103
+ success: true,
2104
+ approved_count: exportResult.summary.total_approved,
2105
+ tasks_added: addResult.added || 0,
2106
+ tasks_skipped: addResult.skipped || 0,
2107
+ files_exported: fileExport?.exported.length || 0,
2108
+ validation: exportResult.validation,
2109
+ digest_status: 'completed'
2110
+ };
2111
+ }
2112
+
2113
+ // ============================================================================
2114
+ // Module Exports
2115
+ // ============================================================================
2116
+
2117
+ module.exports = {
2118
+ // Initialization
2119
+ init,
2120
+
2121
+ // Story Generation (E3-S2)
2122
+ USER_TYPE_PATTERNS,
2123
+ SCENARIO_PATTERNS,
2124
+ generateStoryId,
2125
+ detectUserType,
2126
+ extractObject,
2127
+ generateScenarioName,
2128
+ extractActionFromText,
2129
+ extractOutcomeFromText,
2130
+ convertToGiven,
2131
+ extractGiven,
2132
+ extractWhen,
2133
+ extractThen,
2134
+ generateCriteriaFromClarification,
2135
+ buildTraceabilityMatrix,
2136
+ validateStoryCoverage,
2137
+ generateStoryFromTopic,
2138
+ generateAllStories,
2139
+ saveStory,
2140
+ loadStory,
2141
+ loadAllStories,
2142
+ formatStoryAsMarkdown,
2143
+
2144
+ // Story Presentation Queue
2145
+ loadQueue,
2146
+ saveQueue,
2147
+ initializePresentation,
2148
+ getPresentationStatus,
2149
+ getNextStory,
2150
+ getCurrentStory,
2151
+ approveCurrentStory,
2152
+ rejectCurrentStory,
2153
+ skipCurrentStory,
2154
+ formatStorySummary,
2155
+ formatActionsPrompt,
2156
+ getCompletionSummary,
2157
+ resetPresentation,
2158
+
2159
+ // Story Editing
2160
+ generateEditSessionId,
2161
+ generateChangeId,
2162
+ loadEditSessions,
2163
+ saveEditSessions,
2164
+ startEditSession,
2165
+ getActiveEditSession,
2166
+ recordChange,
2167
+ editUserStory,
2168
+ editCriterion,
2169
+ addCriterion,
2170
+ removeCriterion,
2171
+ validateEditedStory,
2172
+ recalculateCoverage,
2173
+ updateQueueAfterEdit,
2174
+ commitEditSession,
2175
+ cancelEditSession,
2176
+ getEditChanges,
2177
+ getEditHistory,
2178
+ listEditableStories,
2179
+
2180
+ // Workflow Export
2181
+ generateWorkflowId,
2182
+ generateSubTaskId,
2183
+ mapPriority,
2184
+ formatUserStoryDescription,
2185
+ convertStoryToTask,
2186
+ validateForExport,
2187
+ exportApprovedStories,
2188
+ createFeatureTask,
2189
+ addTasksToReadyJson,
2190
+ formatTaskAsMarkdown,
2191
+ exportStoryFiles,
2192
+ previewExport,
2193
+ finalizeDigestion
2194
+ };