wogiflow 2.4.2 → 2.4.3

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 (196) hide show
  1. package/.claude/docs/claude-code-compatibility.md +27 -0
  2. package/.claude/settings.json +1 -1
  3. package/.workflow/models/registry.json +1 -1
  4. package/package.json +4 -4
  5. package/scripts/MEMORY-ARCHITECTURE.md +1 -1
  6. package/scripts/base-workflow-step.js +136 -0
  7. package/scripts/flow-adaptive-learning.js +8 -9
  8. package/scripts/flow-aggregate.js +11 -6
  9. package/scripts/flow-api-index.js +4 -6
  10. package/scripts/flow-assumption-detector.js +0 -2
  11. package/scripts/flow-audit.js +15 -2
  12. package/scripts/flow-auto-context.js +8 -12
  13. package/scripts/flow-auto-learn.js +49 -49
  14. package/scripts/flow-background.js +5 -6
  15. package/scripts/flow-bridge-state.js +8 -10
  16. package/scripts/flow-bulk-loop.js +1 -3
  17. package/scripts/flow-bulk-orchestrator.js +1 -3
  18. package/scripts/flow-cascade-completion.js +0 -2
  19. package/scripts/flow-cascade.js +4 -4
  20. package/scripts/flow-checkpoint.js +10 -13
  21. package/scripts/flow-code-intelligence.js +10 -12
  22. package/scripts/flow-community-sync.js +4 -4
  23. package/scripts/flow-community.js +12 -20
  24. package/scripts/flow-config-defaults.js +0 -2
  25. package/scripts/flow-config-interactive.js +9 -5
  26. package/scripts/flow-config-loader.js +49 -92
  27. package/scripts/flow-config-substitution.js +0 -2
  28. package/scripts/flow-context-estimator.js +4 -4
  29. package/scripts/flow-context-init.js +10 -12
  30. package/scripts/flow-context-manager.js +0 -2
  31. package/scripts/flow-context-scoring.js +2 -2
  32. package/scripts/flow-contract-scan.js +6 -9
  33. package/scripts/flow-correct.js +29 -27
  34. package/scripts/flow-correction-detector.js +5 -1
  35. package/scripts/flow-damage-control.js +47 -54
  36. package/scripts/flow-decisions-merge.js +4 -14
  37. package/scripts/flow-diff.js +5 -8
  38. package/scripts/flow-done-gates.js +786 -0
  39. package/scripts/flow-done-report.js +123 -0
  40. package/scripts/flow-done.js +71 -717
  41. package/scripts/flow-entropy-monitor.js +1 -3
  42. package/scripts/flow-eval.js +5 -5
  43. package/scripts/flow-extraction-review.js +1 -0
  44. package/scripts/flow-failure-categories.js +0 -2
  45. package/scripts/flow-figma-confirm.js +5 -9
  46. package/scripts/flow-figma-generate.js +8 -10
  47. package/scripts/flow-figma-index.js +8 -10
  48. package/scripts/flow-figma-match.js +3 -5
  49. package/scripts/flow-figma-mcp-server.js +2 -4
  50. package/scripts/flow-figma-orchestrator.js +2 -3
  51. package/scripts/flow-figma-registry.js +2 -3
  52. package/scripts/flow-framework-resolver.js +0 -2
  53. package/scripts/flow-function-index.js +4 -6
  54. package/scripts/flow-gate-confidence.js +2 -2
  55. package/scripts/flow-gitignore.js +0 -2
  56. package/scripts/flow-guided-edit.js +5 -6
  57. package/scripts/flow-health.js +5 -6
  58. package/scripts/flow-hook-errors.js +6 -0
  59. package/scripts/flow-hook-status.js +263 -0
  60. package/scripts/flow-hooks.js +17 -29
  61. package/scripts/flow-http-client.js +9 -8
  62. package/scripts/flow-hybrid-interactive.js +7 -12
  63. package/scripts/flow-hybrid-test.js +12 -13
  64. package/scripts/flow-instruction-richness.js +1 -1
  65. package/scripts/flow-io.js +21 -4
  66. package/scripts/flow-knowledge-router.js +9 -3
  67. package/scripts/flow-learning-orchestrator.js +318 -13
  68. package/scripts/flow-links.js +5 -7
  69. package/scripts/flow-long-input-association.js +275 -0
  70. package/scripts/flow-long-input-chunking.js +1 -0
  71. package/scripts/flow-long-input-cli.js +0 -2
  72. package/scripts/flow-long-input-complexity.js +0 -2
  73. package/scripts/flow-long-input-constants.js +0 -2
  74. package/scripts/flow-long-input-contradictions.js +351 -0
  75. package/scripts/flow-long-input-detection.js +0 -2
  76. package/scripts/flow-long-input-passes.js +885 -0
  77. package/scripts/flow-long-input-stories.js +1 -1
  78. package/scripts/flow-long-input-voice.js +0 -2
  79. package/scripts/flow-long-input.js +425 -3005
  80. package/scripts/flow-loop-retry-learning.js +2 -3
  81. package/scripts/flow-lsp.js +3 -3
  82. package/scripts/flow-mcp-docs.js +3 -4
  83. package/scripts/flow-memory-db.js +6 -8
  84. package/scripts/flow-memory-sync.js +18 -11
  85. package/scripts/flow-metrics.js +1 -2
  86. package/scripts/flow-model-adapter.js +2 -3
  87. package/scripts/flow-model-config.js +72 -104
  88. package/scripts/flow-model-router.js +2 -2
  89. package/scripts/flow-model-types.js +0 -2
  90. package/scripts/flow-multi-approach.js +5 -6
  91. package/scripts/flow-orchestrate-context.js +3 -7
  92. package/scripts/flow-orchestrate-rollback.js +3 -8
  93. package/scripts/flow-orchestrate-state.js +8 -14
  94. package/scripts/flow-orchestrate-templates.js +2 -6
  95. package/scripts/flow-orchestrate-validator.js +5 -9
  96. package/scripts/flow-orchestrate.js +126 -103
  97. package/scripts/flow-output.js +0 -2
  98. package/scripts/flow-parallel.js +1 -1
  99. package/scripts/flow-paths.js +23 -2
  100. package/scripts/flow-pattern-enforcer.js +30 -28
  101. package/scripts/flow-pattern-extractor.js +3 -4
  102. package/scripts/flow-pending.js +0 -2
  103. package/scripts/flow-permissions.js +2 -3
  104. package/scripts/flow-plugin-registry.js +10 -12
  105. package/scripts/flow-prd-manager.js +1 -1
  106. package/scripts/flow-progress.js +7 -9
  107. package/scripts/flow-prompt-composer.js +3 -3
  108. package/scripts/flow-prompt-template.js +2 -2
  109. package/scripts/flow-providers.js +7 -4
  110. package/scripts/flow-registry-manager.js +7 -12
  111. package/scripts/flow-regression.js +9 -11
  112. package/scripts/flow-roadmap.js +2 -2
  113. package/scripts/flow-run-trace.js +16 -15
  114. package/scripts/flow-safety.js +2 -5
  115. package/scripts/flow-scanner-base.js +5 -7
  116. package/scripts/flow-scenario-engine.js +1 -5
  117. package/scripts/flow-security.js +29 -0
  118. package/scripts/flow-session-end.js +32 -41
  119. package/scripts/flow-session-learning.js +53 -49
  120. package/scripts/flow-setup-hooks.js +2 -3
  121. package/scripts/flow-skill-create.js +7 -12
  122. package/scripts/flow-skill-generator.js +12 -16
  123. package/scripts/flow-skill-learn.js +17 -8
  124. package/scripts/flow-skill-matcher.js +1 -2
  125. package/scripts/flow-spec-generator.js +2 -4
  126. package/scripts/flow-stack-wizard.js +5 -7
  127. package/scripts/flow-standards-learner.js +35 -16
  128. package/scripts/flow-start.js +2 -0
  129. package/scripts/flow-stats-collector.js +2 -2
  130. package/scripts/flow-status.js +10 -10
  131. package/scripts/flow-statusline-setup.js +2 -2
  132. package/scripts/flow-step-changelog.js +2 -3
  133. package/scripts/flow-step-comments.js +66 -81
  134. package/scripts/flow-step-complexity.js +50 -70
  135. package/scripts/flow-step-coverage.js +3 -5
  136. package/scripts/flow-step-knowledge.js +2 -3
  137. package/scripts/flow-step-pr-tests.js +64 -74
  138. package/scripts/flow-step-regression.js +3 -5
  139. package/scripts/flow-step-review.js +86 -103
  140. package/scripts/flow-step-security.js +111 -121
  141. package/scripts/flow-step-silent-failures.js +56 -83
  142. package/scripts/flow-step-simplifier.js +52 -70
  143. package/scripts/flow-story.js +4 -7
  144. package/scripts/flow-strict-adherence.js +3 -4
  145. package/scripts/flow-task-checkpoint.js +36 -5
  146. package/scripts/flow-task-enforcer.js +2 -24
  147. package/scripts/flow-tech-debt.js +1 -1
  148. package/scripts/flow-template-extractor.js +1 -0
  149. package/scripts/flow-templates.js +11 -13
  150. package/scripts/flow-test-api.js +9 -13
  151. package/scripts/flow-test-discovery.js +1 -1
  152. package/scripts/flow-test-generate.js +5 -9
  153. package/scripts/flow-test-integrity.js +3 -7
  154. package/scripts/flow-test-ui.js +5 -9
  155. package/scripts/flow-testing-deps.js +1 -3
  156. package/scripts/flow-tiered-learning.js +4 -4
  157. package/scripts/flow-todowrite-sync.js +1 -1
  158. package/scripts/flow-tokens.js +0 -2
  159. package/scripts/flow-verification-profile.js +6 -10
  160. package/scripts/flow-verify.js +12 -16
  161. package/scripts/flow-version-check.js +4 -12
  162. package/scripts/flow-webmcp-generator.js +3 -5
  163. package/scripts/flow-workflow-steps.js +0 -2
  164. package/scripts/flow-workflow.js +9 -11
  165. package/scripts/hooks/adapters/claude-code.js +2 -0
  166. package/scripts/hooks/core/config-change.js +1 -0
  167. package/scripts/hooks/core/extension-registry.js +0 -2
  168. package/scripts/hooks/core/instructions-loaded.js +1 -1
  169. package/scripts/hooks/core/observation-capture.js +5 -5
  170. package/scripts/hooks/core/phase-gate.js +5 -0
  171. package/scripts/hooks/core/post-compact.js +1 -12
  172. package/scripts/hooks/core/research-gate.js +2 -12
  173. package/scripts/hooks/core/routing-gate.js +6 -0
  174. package/scripts/hooks/core/task-completed.js +12 -0
  175. package/scripts/hooks/core/worktree-lifecycle.js +1 -1
  176. package/scripts/hooks/entry/claude-code/config-change.js +6 -29
  177. package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
  178. package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
  179. package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
  180. package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
  181. package/scripts/hooks/entry/claude-code/session-end.js +4 -28
  182. package/scripts/hooks/entry/claude-code/session-start.js +205 -243
  183. package/scripts/hooks/entry/claude-code/setup.js +8 -49
  184. package/scripts/hooks/entry/claude-code/stop.js +40 -72
  185. package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
  186. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
  187. package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
  188. package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
  189. package/scripts/hooks/entry/shared/hook-runner.js +99 -0
  190. package/scripts/hooks/entry/shared/read-stdin.js +0 -2
  191. package/scripts/registries/api-registry.js +0 -2
  192. package/scripts/registries/component-registry.js +5 -9
  193. package/scripts/registries/contract-scanner.js +2 -9
  194. package/scripts/registries/function-registry.js +0 -2
  195. package/scripts/registries/schema-registry.js +14 -18
  196. package/scripts/registries/service-registry.js +23 -27
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Long Input Processing - Multi-pass extraction system
4
+ * Long Input Processing - Multi-pass extraction system (Orchestrator)
5
5
  *
6
6
  * Ensures nothing is missed from long/complex inputs (transcripts, prompts,
7
7
  * specs, documents). Uses a 4-pass extraction system:
@@ -10,105 +10,72 @@
10
10
  * Pass 3: Orphan check
11
11
  * Pass 4: Contradiction resolution
12
12
  *
13
+ * This file is the orchestrator — domain logic is in:
14
+ * flow-long-input-association.js — statement splitting, association, statement map I/O
15
+ * flow-long-input-contradictions.js — contradiction detection, orphan resolution, clarification I/O
16
+ * flow-long-input-passes.js — runPass2/3/4, runFullPipeline, quickProcess
17
+ * flow-long-input-parsing.js — VTT/SRT/meeting parsing
18
+ * flow-long-input-language.js — language detection
19
+ * flow-long-input-stories.js — story generation, presentation, editing, export
20
+ * flow-long-input-chunking.js — durable sessions, chunking
21
+ * flow-long-input-constants.js — shared patterns and constants
22
+ * flow-long-input-voice.js — voice input processing
23
+ * flow-long-input-detection.js — large input detection, content classification
24
+ * flow-long-input-complexity.js — complexity scoring
25
+ *
13
26
  * Renamed from flow-transcript-digest.js in v1.8.0
14
27
  */
15
28
 
16
- const fs = require('fs');
17
- const path = require('path');
18
- const { estimateTokens, generateHashId, getConfig } = require('./flow-utils');
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+ const { estimateTokens, generateHashId, getConfig, safeJsonParse, PATHS } = require('./flow-utils');
19
32
  const { success: printSuccess, warn: printWarn } = require('./flow-output');
20
33
 
21
- // Import extracted modules (renamed from transcript-* to long-input-*)
34
+ // Import extracted sub-modules
22
35
  const transcriptParsing = require('./flow-long-input-parsing');
23
36
  const transcriptLanguage = require('./flow-long-input-language');
24
37
  const transcriptStories = require('./flow-long-input-stories');
25
38
  const transcriptChunking = require('./flow-long-input-chunking');
26
-
27
- // Import extracted constant/function modules
28
39
  const longInputConstants = require('./flow-long-input-constants');
29
40
  const longInputVoice = require('./flow-long-input-voice');
30
41
  const longInputDetection = require('./flow-long-input-detection');
31
42
  const longInputComplexity = require('./flow-long-input-complexity');
43
+ const longInputAssociation = require('./flow-long-input-association');
44
+ const longInputContradictions = require('./flow-long-input-contradictions');
45
+ const longInputPasses = require('./flow-long-input-passes');
32
46
 
33
- // Destructure commonly used language functions
47
+ // Destructure language functions
34
48
  const {
35
- detectLanguage,
36
- detectMultipleLanguages,
37
- getLanguageInfo,
38
- LANGUAGE_INFO
49
+ detectLanguage, detectMultipleLanguages, getLanguageInfo, LANGUAGE_INFO
39
50
  } = transcriptLanguage;
40
51
 
41
- // Destructure commonly used parsing functions
52
+ // Destructure parsing functions
42
53
  const {
43
- parseVTT,
44
- parseSRT,
45
- parseSubtitle,
46
- mergeCues,
47
- formatCuesAsText,
48
- getSubtitleStats,
49
- parseZoom,
50
- parseTeams,
51
- parseMeeting,
52
- mergeMeetingEntries,
53
- formatMeetingAsText,
54
- getMeetingStats
54
+ parseVTT, parseSRT, parseSubtitle, mergeCues, formatCuesAsText, getSubtitleStats,
55
+ parseZoom, parseTeams, parseMeeting, mergeMeetingEntries, formatMeetingAsText, getMeetingStats
55
56
  } = transcriptParsing;
56
57
 
57
- // Destructure commonly used chunking functions
58
+ // Destructure chunking functions
58
59
  const {
59
- loadDurableSessions,
60
- listDurableSessions,
61
- getDurableSession,
62
- switchDurableSession,
63
- archiveDurableSession,
64
- deleteDurableSession,
65
- generateRecoverySummaryForSession,
66
- getTimeSince,
67
- needsChunking,
68
- planChunks,
69
- getChunkingStatus
60
+ loadDurableSessions, listDurableSessions, getDurableSession, switchDurableSession,
61
+ archiveDurableSession, deleteDurableSession, generateRecoverySummaryForSession,
62
+ getTimeSince, needsChunking, planChunks, getChunkingStatus
70
63
  } = transcriptChunking;
71
64
 
72
- // Destructure additional language utilities
73
65
  const { listSupportedLanguages } = transcriptLanguage;
74
66
 
75
- // Destructure commonly used story functions
67
+ // Destructure story functions
76
68
  const {
77
- generateStoryFromTopic,
78
- generateAllStories,
79
- saveStory,
80
- loadStory,
81
- loadAllStories,
82
- formatStoryAsMarkdown,
83
- // initializePresentation - available if needed
84
- getPresentationStatus,
85
- getNextStory,
86
- getCurrentStory,
87
- approveCurrentStory,
88
- rejectCurrentStory,
89
- skipCurrentStory,
90
- formatStorySummary,
91
- formatActionsPrompt,
92
- getCompletionSummary,
93
- resetPresentation,
94
- // Edit session functions
95
- startEditSession,
96
- editUserStory,
97
- editCriterion,
98
- addCriterion,
99
- removeCriterion,
100
- getEditChanges,
101
- commitEditSession,
102
- cancelEditSession,
103
- getEditHistory,
104
- listEditableStories,
105
- // Export functions
106
- previewExport,
107
- exportApprovedStories,
108
- finalizeDigestion
69
+ generateStoryFromTopic, generateAllStories, saveStory, loadStory, loadAllStories,
70
+ formatStoryAsMarkdown, getPresentationStatus, getNextStory, getCurrentStory,
71
+ approveCurrentStory, rejectCurrentStory, skipCurrentStory, formatStorySummary,
72
+ formatActionsPrompt, getCompletionSummary, resetPresentation,
73
+ startEditSession, editUserStory, editCriterion, addCriterion, removeCriterion,
74
+ getEditChanges, commitEditSession, cancelEditSession, getEditHistory, listEditableStories,
75
+ previewExport, exportApprovedStories, finalizeDigestion
109
76
  } = transcriptStories;
110
77
 
111
- // Destructure constants from extracted module
78
+ // Destructure constants
112
79
  const {
113
80
  FILLER_PATTERNS, REQUIREMENT_PATTERNS, SEMANTIC_EXPANSIONS,
114
81
  CORRECTION_PATTERNS, ADDITIVE_PATTERNS,
@@ -117,14 +84,14 @@ const {
117
84
  UI_PATTERNS, DATA_PATTERNS, INTERACTION_PATTERNS, COMPLEXITY_LEVELS
118
85
  } = longInputConstants;
119
86
 
120
- // Destructure voice processing functions from extracted module
87
+ // Destructure voice functions
121
88
  const {
122
89
  isVoiceInput, removeFillers, applySelfCorrections, normalizeNumbers,
123
90
  detectUncertainty, detectYesNo, addPunctuation, normalizeVoiceInput,
124
91
  calculateVoiceConfidence, processVoiceAnswer
125
92
  } = longInputVoice;
126
93
 
127
- // Destructure detection functions from extracted module
94
+ // Destructure detection functions
128
95
  const {
129
96
  measureInputMetrics, isVTTFormat, isSRTFormat, detectMeetingFormat,
130
97
  detectInputFormat, analyzeInput, evaluateTrigger,
@@ -133,7 +100,7 @@ const {
133
100
  getDetailedClassification, shouldExcludeContent
134
101
  } = longInputDetection;
135
102
 
136
- // Destructure complexity functions from extracted module
103
+ // Destructure complexity functions
137
104
  const {
138
105
  countEntityTypes, extractEntities, getComplexityLevel,
139
106
  calculateComplexityScore, isRequirement, isVagueStatement,
@@ -142,24 +109,35 @@ const {
142
109
  generateEpicStructure, recommendOutputStructure
143
110
  } = longInputComplexity;
144
111
 
112
+ // Destructure association functions
113
+ const {
114
+ isMeaningfulStatement, splitIntoStatements, calculateAssociationConfidence,
115
+ associateStatements, saveStatementMap, loadStatementMap
116
+ } = longInputAssociation;
117
+
118
+ // Destructure contradiction functions
119
+ const {
120
+ detectContradictions, resolveOrphan, detectCorrectionPhrase, isAdditive,
121
+ calculateResolutionConfidence, generateContradictionQuestion,
122
+ saveClarifications, loadClarifications
123
+ } = longInputContradictions;
124
+
125
+ // Destructure passes functions
126
+ const {
127
+ extractKeyPhrase, createTopicFromOrphans, ensureGeneralTopic,
128
+ saveOrphans, loadOrphans, runPass2, runPass3, runPass4,
129
+ runFullPipeline, quickProcess, generateQuickSummary
130
+ } = longInputPasses;
131
+
145
132
  // Paths - temp processing files go to .workflow/tmp/, cleaned up after completion
146
133
  const TMP_DIR = path.join(process.cwd(), '.workflow', 'tmp', 'long-input');
147
134
  const STATE_DIR = TMP_DIR; // Alias for backward compatibility during migration
148
135
  const ACTIVE_DIGEST_FILE = path.join(TMP_DIR, 'active-digest.json');
149
- // Colors for CLI output
150
- const c = {
151
- reset: '\x1b[0m',
152
- dim: '\x1b[2m',
153
- green: '\x1b[32m',
154
- yellow: '\x1b[33m',
155
- blue: '\x1b[34m',
156
- cyan: '\x1b[36m',
157
- red: '\x1b[31m'
158
- };
159
136
 
160
- /**
161
- * Load configuration
162
- */
137
+ // ============================================
138
+ // Core session/utility functions (stay here)
139
+ // ============================================
140
+
163
141
  function loadConfig() {
164
142
  try {
165
143
  const config = getConfig();
@@ -169,34 +147,22 @@ function loadConfig() {
169
147
  }
170
148
  }
171
149
 
172
- /**
173
- * Generate unique digest ID
174
- */
175
150
  function generateDigestId() {
176
151
  return generateHashId('digest', '', '');
177
152
  }
178
153
 
179
- /**
180
- * Get current timestamp in ISO format
181
- */
182
154
  function now() {
183
155
  return new Date().toISOString();
184
156
  }
185
157
 
186
- /**
187
- * Load active digest session
188
- */
189
158
  function loadActiveDigest() {
190
- try {
191
- return JSON.parse(fs.readFileSync(ACTIVE_DIGEST_FILE, 'utf8'));
192
- } catch (_err) {
159
+ const data = safeJsonParse(ACTIVE_DIGEST_FILE, null);
160
+ if (!data || !data.session) {
193
161
  return { session: { status: 'inactive' } };
194
162
  }
163
+ return data;
195
164
  }
196
165
 
197
- /**
198
- * Save active digest session
199
- */
200
166
  function saveActiveDigest(data) {
201
167
  const content = JSON.stringify(data, null, 2);
202
168
  const tmpPath = ACTIVE_DIGEST_FILE + '.tmp';
@@ -204,105 +170,63 @@ function saveActiveDigest(data) {
204
170
  fs.renameSync(tmpPath, ACTIVE_DIGEST_FILE);
205
171
  }
206
172
 
207
- /**
208
- * Create new digest session
209
- */
210
173
  function createSession(transcript, options = {}) {
211
174
  const digestId = generateDigestId();
212
- const digestPath = path.join(STATE_DIR, digestId);
175
+ const digestPath = path.join(PATHS.state, digestId);
213
176
 
214
- // Create digest directory
215
177
  fs.mkdirSync(digestPath, { recursive: true });
216
-
217
- // Save transcript
218
178
  fs.writeFileSync(path.join(digestPath, 'transcript.md'), transcript);
219
179
 
220
- // Initialize topics.json
221
180
  const topics = {
222
181
  topics: [],
223
182
  metadata: {
224
- total_topics: 0,
225
- active_topics: 0,
226
- clarified_topics: 0,
227
- generated_topics: 0,
228
- detected_at: null,
229
- last_updated: now(),
230
- transcript_word_count: countWords(transcript),
231
- detection_method: 'pass-1-extraction'
183
+ total_topics: 0, active_topics: 0, clarified_topics: 0, generated_topics: 0,
184
+ detected_at: null, last_updated: now(),
185
+ transcript_word_count: countWords(transcript), detection_method: 'pass-1-extraction'
232
186
  }
233
187
  };
234
188
  fs.writeFileSync(path.join(digestPath, 'topics.json'), JSON.stringify(topics, null, 2));
235
189
 
236
- // Initialize statement-map.json
237
190
  const statementMap = {
238
191
  statements: [],
239
192
  metadata: {
240
- total_statements: 0,
241
- meaningful_statements: 0,
242
- mapped_statements: 0,
243
- orphan_statements: 0,
244
- contradictions_detected: 0,
245
- contradictions_resolved: 0,
193
+ total_statements: 0, meaningful_statements: 0, mapped_statements: 0,
194
+ orphan_statements: 0, contradictions_detected: 0, contradictions_resolved: 0,
246
195
  coverage_percentage: 0
247
196
  }
248
197
  };
249
198
  fs.writeFileSync(path.join(digestPath, 'statement-map.json'), JSON.stringify(statementMap, null, 2));
250
199
 
251
- // Initialize clarifications.json
252
200
  const clarifications = {
253
- questions: [],
254
- contradictions: [],
201
+ questions: [], contradictions: [],
255
202
  metadata: {
256
- total_questions: 0,
257
- answered_questions: 0,
258
- pending_questions: 0,
259
- total_contradictions: 0,
260
- resolved_contradictions: 0,
261
- auto_resolved_count: 0,
262
- user_resolved_count: 0
203
+ total_questions: 0, answered_questions: 0, pending_questions: 0,
204
+ total_contradictions: 0, resolved_contradictions: 0,
205
+ auto_resolved_count: 0, user_resolved_count: 0
263
206
  }
264
207
  };
265
208
  fs.writeFileSync(path.join(digestPath, 'clarifications.json'), JSON.stringify(clarifications, null, 2));
266
209
 
267
- // Initialize conversation.json (E2-S4)
268
210
  const conversation = {
269
- session_id: digestId,
270
- started_at: now(),
271
- last_interaction: now(),
211
+ session_id: digestId, started_at: now(), last_interaction: now(),
272
212
  interactions: [{
273
- id: `i-${Date.now().toString(36)}`,
274
- type: 'session_started',
275
- timestamp: now(),
276
- data: {
277
- word_count: countWords(transcript),
278
- content_type: options.contentType || 'unknown'
279
- }
213
+ id: `i-${Date.now().toString(36)}`, type: 'session_started', timestamp: now(),
214
+ data: { word_count: countWords(transcript), content_type: options.contentType || 'unknown' }
280
215
  }],
281
216
  checkpoints: []
282
217
  };
283
218
  fs.writeFileSync(path.join(digestPath, 'conversation.json'), JSON.stringify(conversation, null, 2));
284
219
 
285
- // Initialize orphans.json
286
220
  const orphans = {
287
221
  orphans: [],
288
- coverage: {
289
- total_meaningful: 0,
290
- mapped: 0,
291
- orphans_remaining: 0,
292
- percentage: 0
293
- }
222
+ coverage: { total_meaningful: 0, mapped: 0, orphans_remaining: 0, percentage: 0 }
294
223
  };
295
224
  fs.writeFileSync(path.join(digestPath, 'orphans.json'), JSON.stringify(orphans, null, 2));
296
225
 
297
- // Update active digest
298
226
  const activeDigest = {
299
227
  session: {
300
- id: digestId,
301
- started_at: now(),
302
- last_activity: now(),
303
- status: 'active',
304
- phase: 'ingestion',
305
- digest_path: digestPath
228
+ id: digestId, started_at: now(), last_activity: now(),
229
+ status: 'active', phase: 'ingestion', digest_path: digestPath
306
230
  },
307
231
  phases: {
308
232
  ingestion: { status: 'completed', started_at: now(), completed_at: now() },
@@ -315,3179 +239,691 @@ function createSession(transcript, options = {}) {
315
239
  approval: { status: 'pending', started_at: null, completed_at: null, stories_approved: 0, stories_pending: 0, current_story_index: 0 }
316
240
  },
317
241
  input: {
318
- source: options.source || 'paste',
319
- format: options.format || 'plain',
320
- language: options.language || null,
321
- word_count: countWords(transcript),
322
- chunked: false,
323
- chunk_count: 0
242
+ source: options.source || 'paste', format: options.format || 'plain',
243
+ language: options.language || null, word_count: countWords(transcript),
244
+ chunked: false, chunk_count: 0
324
245
  },
325
- output: {
326
- stories_created: [],
327
- tasks_added_to_ready: []
328
- }
246
+ output: { stories_created: [], tasks_added_to_ready: [] }
329
247
  };
330
248
 
331
249
  saveActiveDigest(activeDigest);
332
-
333
250
  return { digestId, digestPath, activeDigest };
334
251
  }
335
252
 
336
- /**
337
- * Count words in text
338
- */
339
253
  function countWords(text) {
340
254
  return text.split(/\s+/).filter(w => w.length > 0).length;
341
255
  }
342
256
 
343
- /**
344
- * Update phase status
345
- */
346
257
  function updatePhase(phase, status, data = {}) {
347
258
  const activeDigest = loadActiveDigest();
348
-
349
- if (!activeDigest.phases) {
350
- console.error('No active digest session');
351
- return null;
352
- }
353
-
354
- activeDigest.phases[phase] = {
355
- ...activeDigest.phases[phase],
356
- status,
357
- ...data
358
- };
359
-
259
+ if (!activeDigest.phases) { console.error('No active digest session'); return null; }
260
+ activeDigest.phases[phase] = { ...activeDigest.phases[phase], status, ...data };
360
261
  if (status === 'in_progress' && !activeDigest.phases[phase].started_at) {
361
262
  activeDigest.phases[phase].started_at = now();
362
263
  }
363
-
364
- if (status === 'completed') {
365
- activeDigest.phases[phase].completed_at = now();
366
- }
367
-
264
+ if (status === 'completed') { activeDigest.phases[phase].completed_at = now(); }
368
265
  activeDigest.session.last_activity = now();
369
266
  activeDigest.session.phase = phase;
370
-
371
267
  saveActiveDigest(activeDigest);
372
268
  return activeDigest;
373
269
  }
374
270
 
375
- /**
376
- * Check if statement is meaningful (contains requirements/substance)
377
- */
378
- function isMeaningfulStatement(text) {
379
- const trimmed = text.trim();
380
-
381
- // Too short
382
- if (trimmed.length < 5) return { meaningful: false, reason: 'too_short' };
383
-
384
- // Check filler patterns
385
- for (const pattern of FILLER_PATTERNS) {
386
- if (pattern.test(trimmed)) {
387
- return { meaningful: false, reason: 'filler' };
388
- }
389
- }
390
-
391
- // Check for requirement signals
392
- for (const pattern of REQUIREMENT_PATTERNS) {
393
- if (pattern.test(trimmed)) {
394
- return { meaningful: true, reason: 'requirement_signal' };
271
+ function saveTopics(topics) {
272
+ const activeDigest = loadActiveDigest();
273
+ if (!activeDigest.session.digest_path) { throw new Error('No active digest session'); }
274
+ const topicsPath = path.join(activeDigest.session.digest_path, 'topics.json');
275
+ const topicsData = {
276
+ topics: topics.topics || topics,
277
+ metadata: {
278
+ total_topics: (topics.topics || topics).length,
279
+ active_topics: (topics.topics || topics).filter(t => t.status === 'active').length,
280
+ clarified_topics: (topics.topics || topics).filter(t => t.clarification_complete).length,
281
+ generated_topics: (topics.topics || topics).filter(t => t.stories_generated).length,
282
+ detected_at: now(), last_updated: now(),
283
+ transcript_word_count: activeDigest.input?.word_count ?? 0,
284
+ detection_method: 'pass-1-extraction'
395
285
  }
396
- }
397
-
398
- // Check word count - very short statements without requirement signals are likely filler
399
- const wordCount = trimmed.split(/\s+/).length;
400
- if (wordCount < 4) {
401
- return { meaningful: false, reason: 'too_brief' };
402
- }
403
-
404
- // Default to meaningful if substantial enough
405
- return { meaningful: true, reason: 'substantial' };
286
+ };
287
+ fs.writeFileSync(topicsPath, JSON.stringify(topicsData, null, 2));
288
+ updatePhase('topic_extraction', 'completed', { topics_found: topicsData.topics.length });
289
+ return topicsData;
406
290
  }
407
291
 
408
- /**
409
- * Split transcript into statements
410
- */
411
- function splitIntoStatements(text) {
412
- const statements = [];
413
- let position = 0;
414
-
415
- // Split by sentence boundaries and speaker changes
416
- const segments = text.split(/(?<=[.!?])\s+|(?=^[A-Z][a-z]+:|\[\d{2}:\d{2})/gm);
417
-
418
- for (const segment of segments) {
419
- const trimmed = segment.trim();
420
- if (!trimmed) continue;
421
-
422
- // Extract speaker if present
423
- const speakerMatch = trimmed.match(/^([A-Z][a-z]+):\s*/);
424
- const speaker = speakerMatch ? speakerMatch[1] : null;
425
- const content = speakerMatch ? trimmed.slice(speakerMatch[0].length) : trimmed;
426
-
427
- // Extract timestamp if present
428
- const timestampMatch = trimmed.match(/^\[?(\d{2}:\d{2}(?::\d{2})?)\]?\s*/);
429
- const timestamp = timestampMatch ? timestampMatch[1] : null;
430
-
431
- if (content.trim()) {
432
- statements.push({
433
- text: content.trim(),
434
- speaker,
435
- timestamp,
436
- position
437
- });
438
- }
439
-
440
- position += segment.length;
441
- }
442
-
443
- return statements;
292
+ function loadTopics() {
293
+ const activeDigest = loadActiveDigest();
294
+ if (!activeDigest.session.digest_path) { return null; }
295
+ const topicsPath = path.join(activeDigest.session.digest_path, 'topics.json');
296
+ const data = safeJsonParse(topicsPath, null);
297
+ if (!data) return null;
298
+ return data;
444
299
  }
445
300
 
446
- /**
447
- * Calculate association confidence between statement and topic
448
- */
449
- function calculateAssociationConfidence(statement, topic) {
450
- let confidence = 0.5; // Base confidence
451
- const reasons = [];
452
-
453
- const statementLower = statement.text.toLowerCase();
454
- const topicTitle = topic.title.toLowerCase();
455
-
456
- // Entity match - highest confidence
457
- if (topic.entities) {
458
- for (const entity of topic.entities) {
459
- if (statementLower.includes(entity.toLowerCase())) {
460
- confidence = Math.max(confidence, 0.9);
461
- reasons.push(`entity_match:${entity}`);
462
- }
463
- }
464
- }
465
-
466
- // Title word match
467
- const titleWords = topicTitle.split(/\s+/).filter(w => w.length > 3);
468
- for (const word of titleWords) {
469
- if (statementLower.includes(word)) {
470
- confidence = Math.max(confidence, 0.8);
471
- reasons.push(`title_match:${word}`);
472
- }
473
- }
474
-
475
- // Keyword match
476
- if (topic.keywords) {
477
- for (const keyword of topic.keywords) {
478
- if (statementLower.includes(keyword.toLowerCase())) {
479
- confidence = Math.max(confidence, 0.75);
480
- reasons.push(`keyword_match:${keyword}`);
481
- }
482
- }
483
- }
484
-
485
- return { confidence, reasons };
301
+ function getStatus() {
302
+ const activeDigest = loadActiveDigest();
303
+ if (activeDigest.session.status === 'inactive') { return { active: false }; }
304
+ return { active: true, id: activeDigest.session.id, phase: activeDigest.session.phase, phases: activeDigest.phases, input: activeDigest.input };
486
305
  }
487
306
 
488
- /**
489
- * Associate statements with topics
490
- */
491
- function associateStatements(statements, topics) {
492
- const mappedStatements = [];
493
- let currentTopicId = null;
494
- let statementId = 1;
495
-
496
- for (const stmt of statements) {
497
- const meaningfulCheck = isMeaningfulStatement(stmt.text);
498
-
499
- const mappedStatement = {
500
- id: `s-${String(statementId).padStart(3, '0')}`,
501
- text: stmt.text,
502
- position: stmt.position,
503
- timestamp: stmt.timestamp,
504
- speaker: stmt.speaker,
505
- meaningful: meaningfulCheck.meaningful
506
- };
507
-
508
- if (!meaningfulCheck.meaningful) {
509
- mappedStatement.topic_id = null;
510
- mappedStatement.skip_reason = meaningfulCheck.reason;
511
- } else {
512
- // Find best matching topic
513
- let bestMatch = { topicId: null, confidence: 0, reasons: [] };
514
-
515
- for (const topic of topics) {
516
- const { confidence, reasons } = calculateAssociationConfidence(stmt, topic);
517
- if (confidence > bestMatch.confidence) {
518
- bestMatch = { topicId: topic.id, confidence, reasons };
519
- }
520
- }
521
-
522
- // Use context continuity if no strong match
523
- if (bestMatch.confidence < 0.6 && currentTopicId) {
524
- bestMatch = {
525
- topicId: currentTopicId,
526
- confidence: 0.6,
527
- reasons: ['context_continuity']
528
- };
529
- }
530
-
531
- mappedStatement.topic_id = bestMatch.topicId;
532
- mappedStatement.confidence = bestMatch.confidence;
533
- mappedStatement.association_reason = bestMatch.reasons.join(',') || 'context_continuity';
534
- mappedStatement.clarification_needed = bestMatch.confidence < 0.7;
535
-
536
- if (mappedStatement.clarification_needed) {
537
- mappedStatement.clarification_question =
538
- `You mentioned "${stmt.text.slice(0, 50)}..." - which feature does this relate to?`;
539
- }
307
+ function shouldTriggerDigestion(text) {
308
+ const config = loadConfig();
309
+ const threshold = config.autoTriggerThreshold ?? 2000;
310
+ const wordCount = countWords(text);
311
+ if (wordCount < threshold) { return { trigger: false, reason: 'below_threshold', wordCount }; }
312
+ const contentType = classifyContent(text);
313
+ if (contentType.type === 'requirements' || contentType.type === 'transcript') { return { trigger: true, reason: 'auto', wordCount, contentType }; }
314
+ if (contentType.type === 'code') { return { trigger: false, reason: 'code_detected', wordCount, contentType }; }
315
+ return { trigger: 'ask', reason: 'ambiguous', wordCount, contentType };
316
+ }
540
317
 
541
- // Update current topic for context continuity
542
- if (bestMatch.confidence >= 0.7) {
543
- currentTopicId = bestMatch.topicId;
544
- }
545
- }
318
+ function classifyContent(text) {
319
+ const codePatterns = [/```[\s\S]*```/g, /function\s+\w+\s*\(/g, /const\s+\w+\s*=/g, /import\s+.*from/g, /class\s+\w+/g];
320
+ let codeMatches = 0;
321
+ for (const pattern of codePatterns) { const matches = text.match(pattern); if (matches) codeMatches += matches.length; }
322
+ if (codeMatches > 10) { return { type: 'code', confidence: 0.8 }; }
323
+ const reqPatterns = [/we need/gi, /should have/gi, /must support/gi, /add a feature/gi, /implement/gi, /the \w+ should/gi];
324
+ let reqMatches = 0;
325
+ for (const pattern of reqPatterns) { const matches = text.match(pattern); if (matches) reqMatches += matches.length; }
326
+ if (reqMatches > 5) { return { type: 'requirements', confidence: 0.85 }; }
327
+ const transcriptPatterns = [/^\d{2}:\d{2}/gm, /^speaker \d+:/gim, /^\[.*\]:/gm, /^[A-Z][a-z]+:/gm];
328
+ let transcriptMatches = 0;
329
+ for (const pattern of transcriptPatterns) { const matches = text.match(pattern); if (matches) transcriptMatches += matches.length; }
330
+ if (transcriptMatches > 10) { return { type: 'transcript', confidence: 0.9 }; }
331
+ return { type: 'unknown', confidence: 0.5 };
332
+ }
546
333
 
547
- mappedStatements.push(mappedStatement);
548
- statementId++;
549
- }
334
+ // ============================================
335
+ // Multi-language question support (E5-S2)
336
+ // ============================================
550
337
 
551
- return mappedStatements;
338
+ function getQuestionTemplates(languageCode) {
339
+ if (QUESTION_TEMPLATES_BY_LANGUAGE[languageCode]) { return QUESTION_TEMPLATES_BY_LANGUAGE[languageCode]; }
340
+ return QUESTION_TEMPLATES_BY_LANGUAGE.en;
552
341
  }
553
342
 
554
- /**
555
- * Detect contradictions between statements
556
- */
557
- function detectContradictions(statements) {
558
- const contradictions = [];
559
- const meaningfulStatements = statements.filter(s => s.meaningful);
560
-
561
- // Contradiction patterns
562
- const opposites = [
563
- ['left', 'right'],
564
- ['top', 'bottom'],
565
- ['show', 'hide'],
566
- ['enable', 'disable'],
567
- ['add', 'remove'],
568
- ['include', 'exclude'],
569
- ['before', 'after'],
570
- ['above', 'below']
571
- ];
572
-
573
- for (let i = 0; i < meaningfulStatements.length; i++) {
574
- for (let j = i + 1; j < meaningfulStatements.length; j++) {
575
- const stmt1 = meaningfulStatements[i];
576
- const stmt2 = meaningfulStatements[j];
577
-
578
- // Only check statements in same topic
579
- if (stmt1.topic_id !== stmt2.topic_id) continue;
580
-
581
- const text1 = stmt1.text.toLowerCase();
582
- const text2 = stmt2.text.toLowerCase();
583
-
584
- // Check for opposite words
585
- for (const [word1, word2] of opposites) {
586
- if ((text1.includes(word1) && text2.includes(word2)) ||
587
- (text1.includes(word2) && text2.includes(word1))) {
588
- contradictions.push({
589
- statement1_id: stmt1.id,
590
- statement2_id: stmt2.id,
591
- type: 'opposite_values',
592
- attribute: `${word1}/${word2}`,
593
- resolution: 'pending'
594
- });
595
- }
596
- }
597
-
598
- // Check for number conflicts (same attribute, different values)
599
- const numbers1 = text1.match(/\d+/g);
600
- const numbers2 = text2.match(/\d+/g);
601
- if (numbers1 && numbers2) {
602
- // Simple heuristic: if both mention numbers in similar context
603
- const commonWords = text1.split(/\s+/).filter(w => text2.includes(w) && w.length > 3);
604
- if (commonWords.length > 2 && numbers1[0] !== numbers2[0]) {
605
- contradictions.push({
606
- statement1_id: stmt1.id,
607
- statement2_id: stmt2.id,
608
- type: 'quantity_conflict',
609
- values: [numbers1[0], numbers2[0]],
610
- resolution: 'pending'
611
- });
612
- }
613
- }
343
+ function generateLocalizedQuestion(templateKey, detailKey, entity, language = 'en') {
344
+ const isLanguageSupported = Object.hasOwn(QUESTION_TEMPLATES_BY_LANGUAGE, language);
345
+ const effectiveLang = isLanguageSupported ? language : 'en';
346
+ const templates = getQuestionTemplates(language);
347
+ const template = templates[templateKey]?.[detailKey];
348
+ if (!template) {
349
+ const enTemplate = QUESTION_TEMPLATES[templateKey]?.[detailKey];
350
+ if (enTemplate) {
351
+ return { question: enTemplate.question.replace('{entity}', entity), examples: enTemplate.examples || null, priority: enTemplate.priority || 'P2', language: 'en', fallback: true };
614
352
  }
353
+ return null;
615
354
  }
616
-
617
- return contradictions;
355
+ return { question: template.question.replace('{entity}', entity), examples: template.examples || null, priority: template.priority || 'P2', language: effectiveLang, fallback: !isLanguageSupported };
618
356
  }
619
357
 
620
- /**
621
- * Save statement map to digest
622
- */
623
- function saveStatementMap(statementMap) {
358
+ function detectSessionLanguage() {
624
359
  const activeDigest = loadActiveDigest();
625
- if (!activeDigest.session.digest_path) {
626
- throw new Error('No active digest session');
627
- }
628
-
629
- const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
630
-
631
- // Calculate metadata
632
- const meaningful = statementMap.statements.filter(s => s.meaningful);
633
- const mapped = meaningful.filter(s => s.topic_id !== null);
634
- const orphans = meaningful.filter(s => s.topic_id === null);
635
-
636
- const data = {
637
- statements: statementMap.statements,
638
- contradictions: statementMap.contradictions || [],
639
- metadata: {
640
- total_statements: statementMap.statements.length,
641
- meaningful_statements: meaningful.length,
642
- mapped_statements: mapped.length,
643
- orphan_statements: orphans.length,
644
- contradictions_detected: (statementMap.contradictions || []).length,
645
- contradictions_resolved: 0,
646
- coverage_percentage: meaningful.length > 0
647
- ? Math.round((mapped.length / meaningful.length) * 100 * 10) / 10
648
- : 0
649
- }
650
- };
651
-
652
- fs.writeFileSync(mapPath, JSON.stringify(data, null, 2));
653
-
654
- // Update phase
655
- updatePhase('statement_mapping', 'completed', {
656
- statements_mapped: mapped.length,
657
- orphans_found: orphans.length,
658
- contradictions_found: (statementMap.contradictions || []).length
659
- });
660
-
661
- return data;
360
+ if (!activeDigest.session.digest_path) { throw new Error('No active digest session'); }
361
+ const digestDir = activeDigest.session.digest_path;
362
+ const transcriptPath = path.join(digestDir, 'transcript.txt');
363
+ if (!fs.existsSync(transcriptPath)) { return { detected: false, reason: 'No transcript file found' }; }
364
+ const transcript = fs.readFileSync(transcriptPath, 'utf8');
365
+ const langResult = detectLanguage(transcript);
366
+ const multiResult = detectMultipleLanguages(transcript, { segmentSize: 500 });
367
+ activeDigest.session.detected_language = langResult.language;
368
+ activeDigest.session.language_confidence = langResult.confidence;
369
+ activeDigest.session.is_multilingual = multiResult.isMultilingual;
370
+ activeDigest.session.language_distribution = multiResult.distribution || {};
371
+ saveActiveDigest(activeDigest);
372
+ return { detected: true, language: langResult.language, languageName: LANGUAGE_INFO[langResult.language]?.name || 'Unknown', confidence: langResult.confidence, isMultilingual: multiResult.isMultilingual, distribution: multiResult.distribution };
662
373
  }
663
374
 
664
- /**
665
- * Load statement map from digest
666
- */
667
- function loadStatementMap() {
375
+ function getTopicLanguage(topicId) {
376
+ const topics = loadTopics();
377
+ const stmtMap = loadStatementMap();
668
378
  const activeDigest = loadActiveDigest();
669
- if (!activeDigest.session.digest_path) {
670
- return null;
671
- }
672
-
673
- const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
674
- try {
675
- return JSON.parse(fs.readFileSync(mapPath, 'utf8'));
676
- } catch (_err) {
677
- return null;
678
- }
379
+ if (!topics || !stmtMap) { return activeDigest.session?.detected_language || 'en'; }
380
+ const topic = topics.topics.find(t => t.id === topicId);
381
+ if (!topic) { return activeDigest.session?.detected_language || 'en'; }
382
+ if (topic.language) { return topic.language; }
383
+ const topicStatements = stmtMap.statements.filter(s => s.topic_id === topicId && s.meaningful);
384
+ if (topicStatements.length === 0) { return activeDigest.session?.detected_language || 'en'; }
385
+ const combinedText = topicStatements.map(s => s.text).join('\n');
386
+ const result = detectLanguage(combinedText);
387
+ return result.language;
679
388
  }
680
389
 
681
- /**
682
- * Process Pass 2: Statement Association
683
- */
684
- function runPass2() {
390
+ function setLanguagePreference(languageCode) {
685
391
  const activeDigest = loadActiveDigest();
686
- if (!activeDigest.session.digest_path) {
687
- throw new Error('No active digest session');
688
- }
689
-
690
- // Load transcript
691
- const transcriptPath = path.join(activeDigest.session.digest_path, 'transcript.md');
692
- const transcript = fs.readFileSync(transcriptPath, 'utf8');
693
-
694
- // Load topics from Pass 1
695
- const topicsData = loadTopics();
696
- if (!topicsData || !topicsData.topics.length) {
697
- throw new Error('No topics found - run Pass 1 first');
698
- }
699
-
700
- // Update phase status
701
- updatePhase('statement_mapping', 'in_progress');
702
-
703
- // Split into statements
704
- const statements = splitIntoStatements(transcript);
705
-
706
- // Associate with topics
707
- const mappedStatements = associateStatements(statements, topicsData.topics);
708
-
709
- // Detect contradictions
710
- const contradictions = detectContradictions(mappedStatements);
711
-
712
- // Mark contradicting statements
713
- for (const contradiction of contradictions) {
714
- const stmt1 = mappedStatements.find(s => s.id === contradiction.statement1_id);
715
- const stmt2 = mappedStatements.find(s => s.id === contradiction.statement2_id);
716
- if (stmt1) stmt1.contradicts = contradiction.statement2_id;
717
- if (stmt2) stmt2.contradicts = contradiction.statement1_id;
718
- }
392
+ if (!activeDigest.session.digest_path) { throw new Error('No active digest session'); }
393
+ const info = getLanguageInfo(languageCode);
394
+ if (!info.supported) { throw new Error(`Unsupported language code: ${languageCode}`); }
395
+ activeDigest.session.preferred_language = languageCode;
396
+ saveActiveDigest(activeDigest);
397
+ return { set: true, language: languageCode, languageName: info.name };
398
+ }
719
399
 
720
- // Save statement map
721
- const result = saveStatementMap({
722
- statements: mappedStatements,
723
- contradictions
724
- });
400
+ function getEffectiveLanguage(topicId = null) {
401
+ const activeDigest = loadActiveDigest();
402
+ if (activeDigest.session?.preferred_language) { return activeDigest.session.preferred_language; }
403
+ if (topicId) { const topicLang = getTopicLanguage(topicId); if (topicLang && QUESTION_TEMPLATES_BY_LANGUAGE[topicLang]) { return topicLang; } }
404
+ if (activeDigest.session?.detected_language && QUESTION_TEMPLATES_BY_LANGUAGE[activeDigest.session.detected_language]) { return activeDigest.session.detected_language; }
405
+ return 'en';
406
+ }
725
407
 
726
- return result;
408
+ function getSessionLanguageInfo() {
409
+ const activeDigest = loadActiveDigest();
410
+ return {
411
+ detected: activeDigest.session?.detected_language || null, detectedName: LANGUAGE_INFO[activeDigest.session?.detected_language]?.name || null,
412
+ confidence: activeDigest.session?.language_confidence || null, preferred: activeDigest.session?.preferred_language || null,
413
+ preferredName: LANGUAGE_INFO[activeDigest.session?.preferred_language]?.name || null, isMultilingual: activeDigest.session?.is_multilingual || false,
414
+ distribution: activeDigest.session?.language_distribution || {}, effective: getEffectiveLanguage()
415
+ };
727
416
  }
728
417
 
729
418
  // ============================================
730
- // Pass 3: Orphan Check
419
+ // Question Generation (E2-S1)
731
420
  // ============================================
732
421
 
733
- /**
734
- * Extract key phrase from statement for topic naming
735
- */
736
- function extractKeyPhrase(text) {
737
- // Remove common words
738
- const stopWords = ['the', 'a', 'an', 'is', 'are', 'should', 'must', 'will', 'can', 'be', 'it', 'we', 'i', 'to', 'for', 'of', 'in', 'on', 'with'];
739
- const words = text.toLowerCase()
740
- .replace(/[^\w\s]/g, '')
741
- .split(/\s+/)
742
- .filter(w => w.length > 2 && !stopWords.includes(w));
743
-
744
- // Get first 2-3 meaningful words
745
- const keyWords = words.slice(0, 3);
746
- if (keyWords.length === 0) return 'Misc';
747
-
748
- // Capitalize
749
- return keyWords.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
422
+ function isDetailProvided(detail, topicId, statements) {
423
+ const topicStatements = statements.filter(s => s.topic_id === topicId && s.meaningful);
424
+ const pattern = DETAIL_PATTERNS[detail];
425
+ if (!pattern) return false;
426
+ return topicStatements.some(s => pattern.test(s.text));
750
427
  }
751
428
 
752
- /**
753
- * Enhanced confidence calculation with semantic expansion
754
- */
755
- function calculateExpandedConfidence(statement, topic) {
756
- let confidence = 0.5;
757
- const reasons = [];
758
- const statementLower = statement.text.toLowerCase();
759
-
760
- // Standard matching first
761
- const { confidence: baseConf, reasons: baseReasons } = calculateAssociationConfidence(statement, topic);
762
- if (baseConf > confidence) {
763
- confidence = baseConf;
764
- reasons.push(...baseReasons);
765
- }
429
+ function extractEntityFromStatement(statement, pattern) {
430
+ const match = statement.text.match(pattern.pattern);
431
+ if (match && pattern.entity !== null) { return match[pattern.entity]; }
432
+ return pattern.type;
433
+ }
766
434
 
767
- // Semantic expansion - check synonyms
768
- for (const [term, synonyms] of Object.entries(SEMANTIC_EXPANSIONS)) {
769
- const allTerms = [term, ...synonyms];
770
- const topicTitle = topic.title.toLowerCase();
771
- const topicKeywords = (topic.keywords || []).map(k => k.toLowerCase());
772
-
773
- for (const syn of allTerms) {
774
- // Statement contains synonym and topic contains related term
775
- if (statementLower.includes(syn)) {
776
- for (const related of allTerms) {
777
- if (topicTitle.includes(related) || topicKeywords.includes(related)) {
778
- confidence = Math.max(confidence, 0.7);
779
- reasons.push(`semantic_expansion:${syn}->${related}`);
780
- }
435
+ function analyzeCompleteness(statement, topicId, allStatements) {
436
+ const gaps = [];
437
+ const text = statement.text.toLowerCase();
438
+ for (const entityPattern of ENTITY_PATTERNS) {
439
+ if (entityPattern.pattern.test(text)) {
440
+ const entity = extractEntityFromStatement(statement, entityPattern);
441
+ for (const detail of entityPattern.missing) {
442
+ if (!isDetailProvided(detail, topicId, allStatements)) {
443
+ gaps.push({ type: entityPattern.type, entity, detail, statementId: statement.id });
781
444
  }
782
445
  }
783
446
  }
784
447
  }
785
-
786
- return { confidence, reasons };
448
+ return gaps;
787
449
  }
788
450
 
789
- /**
790
- * Try to resolve a single orphan statement
791
- */
792
- function resolveOrphan(orphan, topics) {
793
- const candidates = [];
794
-
795
- // Try enhanced matching against all topics
796
- for (const topic of topics) {
797
- const { confidence, reasons } = calculateExpandedConfidence(orphan, topic);
798
- if (confidence >= 0.5) {
799
- candidates.push({ topic, confidence, reasons });
800
- }
801
- }
802
-
803
- // Sort by confidence
804
- candidates.sort((a, b) => b.confidence - a.confidence);
805
-
806
- // Resolution decision
807
- if (candidates.length === 0) {
808
- return {
809
- resolved: false,
810
- method: 'no_match',
811
- confidence: 0
812
- };
813
- }
814
-
815
- const best = candidates[0];
816
-
817
- // Clear winner
818
- if (best.confidence >= 0.6 && (candidates.length === 1 || best.confidence - candidates[1].confidence > 0.15)) {
819
- return {
820
- resolved: true,
821
- method: 'semantic_expansion',
822
- topic_id: best.topic.id,
823
- confidence: best.confidence,
824
- reasons: best.reasons
825
- };
826
- }
827
-
828
- // Ambiguous - multiple close matches
829
- if (candidates.length > 1 && candidates[0].confidence - candidates[1].confidence < 0.1) {
830
- return {
831
- resolved: false,
832
- method: 'ambiguous',
833
- possible_topics: candidates.slice(0, 3).map(c => c.topic.id),
834
- confidence: best.confidence
835
- };
451
+ function detectVagueness(statement) {
452
+ for (const vague of VAGUE_PATTERNS) {
453
+ if (vague.pattern.test(statement.text)) { return { isVague: true, key: vague.key, question: vague.question }; }
836
454
  }
837
-
838
- // Low confidence winner
839
- return {
840
- resolved: best.confidence >= 0.5,
841
- method: best.confidence >= 0.5 ? 'context_reanalysis' : 'low_confidence',
842
- topic_id: best.confidence >= 0.5 ? best.topic.id : null,
843
- confidence: best.confidence,
844
- reasons: best.reasons
845
- };
455
+ return { isVague: false };
846
456
  }
847
457
 
848
- /**
849
- * Create a new topic from orphan statements
850
- */
851
- function createTopicFromOrphans(orphans, _existingTopics) {
852
- // Guard against empty orphans array
853
- if (!orphans || orphans.length === 0) {
854
- const topicId = generateHashId('t-auto', '', '');
855
- return {
856
- id: topicId,
857
- title: 'Miscellaneous',
858
- description: 'Auto-generated topic for uncategorized statements',
859
- source: 'orphan_resolution',
860
- entities: [],
861
- keywords: [],
862
- statements: [],
863
- needs_review: true,
864
- confidence: 0.5,
865
- created_at: now()
866
- };
867
- }
868
-
869
- const topicId = generateHashId('t-auto', '', '');
870
-
871
- // Extract common keywords from orphans
872
- const allWords = orphans.flatMap(o =>
873
- o.text.toLowerCase()
874
- .replace(/[^\w\s]/g, '')
875
- .split(/\s+/)
876
- .filter(w => w.length > 3)
877
- );
878
-
879
- // Count word frequencies
880
- const wordCounts = {};
881
- for (const word of allWords) {
882
- wordCounts[word] = (wordCounts[word] || 0) + 1;
883
- }
884
-
885
- // Get most common words as keywords
886
- const keywords = Object.entries(wordCounts)
887
- .sort((a, b) => b[1] - a[1])
888
- .slice(0, 5)
889
- .map(([word]) => word);
890
-
891
- // Generate title from first orphan
892
- const title = extractKeyPhrase(orphans[0].text);
893
-
894
- return {
895
- id: topicId,
896
- title: title,
897
- description: `Auto-generated from ${orphans.length} orphan statement(s)`,
898
- source: 'orphan_resolution',
899
- entities: [],
900
- keywords,
901
- statements: orphans.map(o => o.id),
902
- needs_review: true,
903
- confidence: 0.7,
904
- created_at: now()
905
- };
906
- }
907
-
908
- /**
909
- * Ensure General topic exists
910
- */
911
- function ensureGeneralTopic(topics) {
912
- let general = topics.find(t => t.id === 't-general');
913
- if (!general) {
914
- general = {
915
- id: 't-general',
916
- title: 'General Requirements',
917
- description: 'Miscellaneous requirements that apply broadly or do not fit specific features',
918
- source: 'catch_all',
919
- entities: [],
920
- keywords: ['general', 'overall', 'misc'],
921
- statements: [],
922
- needs_review: true,
923
- confidence: 1.0
924
- };
925
- topics.push(general);
926
- }
927
- return general;
928
- }
929
-
930
- /**
931
- * Save orphan resolution results
932
- */
933
- function saveOrphans(orphansData) {
934
- const activeDigest = loadActiveDigest();
935
- if (!activeDigest.session.digest_path) {
936
- throw new Error('No active digest session');
937
- }
938
-
939
- const orphansPath = path.join(activeDigest.session.digest_path, 'orphans.json');
940
- fs.writeFileSync(orphansPath, JSON.stringify(orphansData, null, 2));
941
- return orphansData;
942
- }
943
-
944
- /**
945
- * Load orphan data
946
- */
947
- function loadOrphans() {
948
- const activeDigest = loadActiveDigest();
949
- if (!activeDigest.session.digest_path) {
950
- return null;
951
- }
952
-
953
- const orphansPath = path.join(activeDigest.session.digest_path, 'orphans.json');
954
- try {
955
- return JSON.parse(fs.readFileSync(orphansPath, 'utf8'));
956
- } catch (_err) {
957
- return null;
958
- }
959
- }
960
-
961
- /**
962
- * Process Pass 3: Orphan Check
963
- */
964
- function runPass3() {
965
- const activeDigest = loadActiveDigest();
966
- if (!activeDigest.session.digest_path) {
967
- throw new Error('No active digest session');
968
- }
969
-
970
- // Load statement map
971
- const stmtMap = loadStatementMap();
972
- if (!stmtMap) {
973
- throw new Error('No statement map found - run Pass 2 first');
974
- }
975
-
976
- // Load topics
977
- const topicsData = loadTopics();
978
- if (!topicsData) {
979
- throw new Error('No topics found');
980
- }
981
-
982
- // Update phase
983
- updatePhase('orphan_check', 'in_progress');
984
-
985
- // Find orphans
986
- const orphanStatements = stmtMap.statements.filter(s => s.meaningful && s.topic_id === null);
987
-
988
- if (orphanStatements.length === 0) {
989
- // No orphans - 100% coverage
990
- const result = {
991
- orphans: [],
992
- resolved: [],
993
- new_topics_created: [],
994
- coverage: {
995
- total_meaningful: stmtMap.metadata.meaningful_statements,
996
- mapped: stmtMap.metadata.meaningful_statements,
997
- clarification_needed: 0,
998
- percentage: 100,
999
- target: 100
1000
- }
1001
- };
1002
-
1003
- saveOrphans(result);
1004
- updatePhase('orphan_check', 'completed', { orphans_resolved: 0 });
1005
- return result;
1006
- }
1007
-
1008
- const resolved = [];
1009
- const stillOrphans = [];
1010
- const newTopics = [];
1011
- let topics = [...topicsData.topics];
1012
-
1013
- // First pass: try to resolve each orphan
1014
- for (const orphan of orphanStatements) {
1015
- const resolution = resolveOrphan(orphan, topics);
1016
-
1017
- if (resolution.resolved) {
1018
- // Update statement in map
1019
- orphan.topic_id = resolution.topic_id;
1020
- orphan.confidence = resolution.confidence;
1021
- orphan.association_reason = resolution.reasons?.join(',') || resolution.method;
1022
-
1023
- resolved.push({
1024
- id: orphan.id,
1025
- original_topic_id: null,
1026
- resolved_topic_id: resolution.topic_id,
1027
- resolution_method: resolution.method,
1028
- confidence: resolution.confidence
1029
- });
1030
- } else {
1031
- stillOrphans.push({
1032
- ...orphan,
1033
- resolution_attempted: true,
1034
- resolution_result: resolution.method,
1035
- possible_topics: resolution.possible_topics || [],
1036
- needs_clarification: true,
1037
- clarification_question: `You mentioned "${orphan.text.slice(0, 50)}..." - which feature does this relate to?`
1038
- });
1039
- }
1040
- }
1041
-
1042
- // Second pass: cluster remaining orphans that might form new topics
1043
- const unresolved = stillOrphans.filter(o => o.resolution_result === 'no_match');
1044
- if (unresolved.length >= 2) {
1045
- // Simple clustering: group orphans with similar words
1046
- const clusters = [];
1047
- const used = new Set();
1048
-
1049
- for (let i = 0; i < unresolved.length; i++) {
1050
- if (used.has(i)) continue;
1051
-
1052
- const cluster = [unresolved[i]];
1053
- used.add(i);
1054
-
1055
- const words1 = new Set(unresolved[i].text.toLowerCase().split(/\s+/).filter(w => w.length > 3));
1056
-
1057
- for (let j = i + 1; j < unresolved.length; j++) {
1058
- if (used.has(j)) continue;
1059
-
1060
- const words2 = new Set(unresolved[j].text.toLowerCase().split(/\s+/).filter(w => w.length > 3));
1061
- const common = [...words1].filter(w => words2.has(w));
1062
-
1063
- if (common.length >= 2) {
1064
- cluster.push(unresolved[j]);
1065
- used.add(j);
1066
- }
1067
- }
1068
-
1069
- if (cluster.length >= 2) {
1070
- clusters.push(cluster);
1071
- }
1072
- }
1073
-
1074
- // Create new topics from clusters
1075
- for (const cluster of clusters) {
1076
- const newTopic = createTopicFromOrphans(cluster, topics);
1077
- topics.push(newTopic);
1078
- newTopics.push({
1079
- id: newTopic.id,
1080
- title: newTopic.title,
1081
- statements_assigned: cluster.map(o => o.id)
1082
- });
1083
-
1084
- // Update statements
1085
- for (const orphan of cluster) {
1086
- const stmtInMap = stmtMap.statements.find(s => s.id === orphan.id);
1087
- if (stmtInMap) {
1088
- stmtInMap.topic_id = newTopic.id;
1089
- stmtInMap.confidence = 0.7;
1090
- stmtInMap.association_reason = 'topic_clustering';
1091
- }
1092
-
1093
- resolved.push({
1094
- id: orphan.id,
1095
- original_topic_id: null,
1096
- resolved_topic_id: newTopic.id,
1097
- resolution_method: 'topic_clustering',
1098
- confidence: 0.7
1099
- });
1100
-
1101
- // Remove from stillOrphans
1102
- const idx = stillOrphans.findIndex(o => o.id === orphan.id);
1103
- if (idx >= 0) stillOrphans.splice(idx, 1);
1104
- }
1105
- }
1106
- }
1107
-
1108
- // Third pass: assign remaining low-priority orphans to General
1109
- const veryLowConfidence = stillOrphans.filter(o =>
1110
- o.resolution_result === 'no_match' || o.confidence < 0.3
1111
- );
1112
-
1113
- if (veryLowConfidence.length > 0) {
1114
- const general = ensureGeneralTopic(topics);
1115
-
1116
- for (const orphan of veryLowConfidence) {
1117
- const stmtInMap = stmtMap.statements.find(s => s.id === orphan.id);
1118
- if (stmtInMap) {
1119
- stmtInMap.topic_id = general.id;
1120
- stmtInMap.confidence = 0.5;
1121
- stmtInMap.association_reason = 'general_assignment';
1122
- }
1123
-
1124
- resolved.push({
1125
- id: orphan.id,
1126
- original_topic_id: null,
1127
- resolved_topic_id: general.id,
1128
- resolution_method: 'general_assignment',
1129
- confidence: 0.5
1130
- });
1131
-
1132
- // Remove from stillOrphans
1133
- const idx = stillOrphans.findIndex(o => o.id === orphan.id);
1134
- if (idx >= 0) stillOrphans.splice(idx, 1);
1135
- }
1136
- }
1137
-
1138
- // Update topics if new ones were created
1139
- if (newTopics.length > 0) {
1140
- saveTopics({ topics });
1141
- }
1142
-
1143
- // Save updated statement map
1144
- const meaningful = stmtMap.statements.filter(s => s.meaningful);
1145
- const mapped = meaningful.filter(s => s.topic_id !== null);
1146
- stmtMap.metadata.mapped_statements = mapped.length;
1147
- stmtMap.metadata.orphan_statements = stillOrphans.length;
1148
- stmtMap.metadata.coverage_percentage = meaningful.length > 0
1149
- ? Math.round((mapped.length / meaningful.length) * 100 * 10) / 10
1150
- : 0;
1151
-
1152
- const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
1153
- fs.writeFileSync(mapPath, JSON.stringify(stmtMap, null, 2));
1154
-
1155
- // Prepare result
1156
- const result = {
1157
- orphans: stillOrphans,
1158
- resolved,
1159
- new_topics_created: newTopics,
1160
- coverage: {
1161
- total_meaningful: meaningful.length,
1162
- mapped: mapped.length,
1163
- clarification_needed: stillOrphans.length,
1164
- percentage: stmtMap.metadata.coverage_percentage,
1165
- target: 100
1166
- }
1167
- };
1168
-
1169
- saveOrphans(result);
1170
- updatePhase('orphan_check', 'completed', {
1171
- orphans_resolved: resolved.length,
1172
- new_topics: newTopics.length,
1173
- remaining_orphans: stillOrphans.length
1174
- });
1175
-
1176
- return result;
1177
- }
1178
-
1179
- // ============================================
1180
- // Pass 4: Contradiction Resolution
1181
- // ============================================
1182
-
1183
- /**
1184
- * Check if statement contains a correction phrase
1185
- */
1186
- function detectCorrectionPhrase(text) {
1187
- for (const { pattern, name, weight } of CORRECTION_PATTERNS) {
1188
- if (pattern.test(text)) {
1189
- return { detected: true, phrase: name, weight };
1190
- }
1191
- }
1192
- return { detected: false, phrase: null, weight: 0 };
1193
- }
1194
-
1195
- /**
1196
- * Check if statement is additive (not a real contradiction)
1197
- */
1198
- function isAdditive(text) {
1199
- return ADDITIVE_PATTERNS.some(pattern => pattern.test(text));
1200
- }
1201
-
1202
- /**
1203
- * Calculate resolution confidence for a contradiction
1204
- */
1205
- function calculateResolutionConfidence(stmt1, stmt2, contradiction) {
1206
- let confidence = 0.5;
1207
- let reasons = [];
1208
-
1209
- // Check for correction phrase in stmt2 (later statement)
1210
- const correction = detectCorrectionPhrase(stmt2.text);
1211
- if (correction.detected) {
1212
- confidence += correction.weight;
1213
- reasons.push(`correction_phrase:${correction.phrase}`);
1214
- }
1215
-
1216
- // Same speaker increases confidence
1217
- if (stmt1.speaker && stmt2.speaker && stmt1.speaker === stmt2.speaker) {
1218
- confidence += 0.15;
1219
- reasons.push('same_speaker');
1220
- }
1221
-
1222
- // Position difference - later statements typically override
1223
- const positionDiff = stmt2.position - stmt1.position;
1224
- if (positionDiff > 500) { // Significant distance
1225
- confidence += 0.1;
1226
- reasons.push('later_position');
1227
- }
1228
-
1229
- // Check if stmt2 explicitly references the attribute
1230
- const attr = contradiction.attribute;
1231
- if (attr) {
1232
- const [word1, word2] = attr.split('/');
1233
- if (stmt2.text.toLowerCase().includes(word1) || stmt2.text.toLowerCase().includes(word2)) {
1234
- confidence += 0.1;
1235
- reasons.push('explicit_attribute_reference');
1236
- }
1237
- }
1238
-
1239
- // Check for additive pattern - might not be a real contradiction
1240
- if (isAdditive(stmt2.text)) {
1241
- confidence = 0.3; // Low confidence - likely not a contradiction
1242
- reasons = ['additive_pattern'];
1243
- }
1244
-
1245
- return {
1246
- confidence: Math.min(confidence, 1.0),
1247
- reasons,
1248
- winner: confidence >= 0.5 ? stmt2.id : null,
1249
- isAdditive: isAdditive(stmt2.text)
1250
- };
1251
- }
1252
-
1253
- /**
1254
- * Generate clarification question for unresolved contradiction
1255
- */
1256
- function generateContradictionQuestion(stmt1, stmt2, contradiction) {
1257
- const attr = contradiction.attribute || 'value';
1258
- const [val1, val2] = attr.split('/');
1259
-
1260
- // Extract the actual values from statements if possible
1261
- const extractValue = (text, hints) => {
1262
- for (const hint of hints || []) {
1263
- if (text.toLowerCase().includes(hint.toLowerCase())) {
1264
- return hint;
1265
- }
1266
- }
1267
- return text.slice(0, 50);
1268
- };
1269
-
1270
- const value1 = val1 || extractValue(stmt1.text, []);
1271
- const value2 = val2 || extractValue(stmt2.text, []);
1272
-
1273
- return {
1274
- question: `You mentioned "${stmt1.text.slice(0, 60)}" but later said "${stmt2.text.slice(0, 60)}". Which do you prefer?`,
1275
- options: [
1276
- { id: 'opt-1', text: value1, statement_id: stmt1.id },
1277
- { id: 'opt-2', text: value2, statement_id: stmt2.id },
1278
- { id: 'opt-3', text: 'Both are needed', resolution: 'keep_both' }
1279
- ],
1280
- attribute: attr
1281
- };
1282
- }
1283
-
1284
- /**
1285
- * Save clarifications to file
1286
- */
1287
- function saveClarifications(clarifications) {
1288
- const activeDigest = loadActiveDigest();
1289
- if (!activeDigest.session.digest_path) {
1290
- throw new Error('No active digest session');
1291
- }
1292
-
1293
- const clarPath = path.join(activeDigest.session.digest_path, 'clarifications.json');
1294
- fs.writeFileSync(clarPath, JSON.stringify(clarifications, null, 2));
1295
- return clarifications;
1296
- }
1297
-
1298
- /**
1299
- * Load clarifications from file
1300
- */
1301
- function loadClarifications() {
1302
- const activeDigest = loadActiveDigest();
1303
- if (!activeDigest.session.digest_path) {
1304
- return null;
1305
- }
1306
-
1307
- const clarPath = path.join(activeDigest.session.digest_path, 'clarifications.json');
1308
- try {
1309
- return JSON.parse(fs.readFileSync(clarPath, 'utf8'));
1310
- } catch (_err) {
1311
- return {
1312
- questions: [],
1313
- contradictions: [],
1314
- metadata: {
1315
- total_questions: 0,
1316
- answered_questions: 0,
1317
- pending_questions: 0,
1318
- total_contradictions: 0,
1319
- resolved_contradictions: 0,
1320
- auto_resolved_count: 0,
1321
- user_resolved_count: 0
1322
- }
1323
- };
1324
- }
1325
- }
1326
-
1327
- /**
1328
- * Process Pass 4: Contradiction Resolution
1329
- */
1330
- function runPass4() {
1331
- const activeDigest = loadActiveDigest();
1332
- if (!activeDigest.session.digest_path) {
1333
- throw new Error('No active digest session');
1334
- }
1335
-
1336
- // Load statement map
1337
- const stmtMap = loadStatementMap();
1338
- if (!stmtMap) {
1339
- throw new Error('No statement map found - run Pass 2 first');
1340
- }
1341
-
1342
- const contradictions = stmtMap.contradictions || [];
1343
- if (contradictions.length === 0) {
1344
- const result = {
1345
- resolved: [],
1346
- pending: [],
1347
- additive: [],
1348
- stats: {
1349
- total: 0,
1350
- auto_resolved: 0,
1351
- needs_clarification: 0,
1352
- additive_not_contradiction: 0
1353
- }
1354
- };
1355
- updatePhase('contradiction_resolution', 'completed', result.stats);
1356
- return result;
1357
- }
1358
-
1359
- // Update phase
1360
- updatePhase('contradiction_resolution', 'in_progress');
1361
-
1362
- const resolved = [];
1363
- const pending = [];
1364
- const additive = [];
1365
-
1366
- // Load or create clarifications
1367
- let clarifications = loadClarifications();
1368
-
1369
- // Process each contradiction
1370
- for (const contradiction of contradictions) {
1371
- const stmt1 = stmtMap.statements.find(s => s.id === contradiction.statement1_id);
1372
- const stmt2 = stmtMap.statements.find(s => s.id === contradiction.statement2_id);
1373
-
1374
- if (!stmt1 || !stmt2) continue;
1375
-
1376
- const resolution = calculateResolutionConfidence(stmt1, stmt2, contradiction);
1377
-
1378
- if (resolution.isAdditive) {
1379
- // Not actually a contradiction
1380
- contradiction.resolution = 'not_contradiction';
1381
- contradiction.reason = 'additive_pattern';
1382
- additive.push({
1383
- statement1_id: contradiction.statement1_id,
1384
- statement2_id: contradiction.statement2_id,
1385
- reason: 'Both statements are valid (additive)'
1386
- });
1387
-
1388
- // Remove from contradictions
1389
- continue;
1390
- }
1391
-
1392
- if (resolution.confidence >= 0.8) {
1393
- // Auto-resolve
1394
- contradiction.resolution = 'auto_resolved';
1395
- contradiction.winner = resolution.winner;
1396
- contradiction.reason = resolution.reasons.join(',');
1397
- contradiction.confidence = resolution.confidence;
1398
- contradiction.resolved_at = now();
1399
-
1400
- // Mark loser as superseded
1401
- const loser = resolution.winner === stmt2.id ? stmt1 : stmt2;
1402
- const winner = resolution.winner === stmt2.id ? stmt2 : stmt1;
1403
-
1404
- loser.superseded = true;
1405
- loser.superseded_by = winner.id;
1406
- loser.superseded_reason = resolution.reasons[0] || 'auto_resolved';
1407
-
1408
- winner.supersedes = loser.id;
1409
- winner.is_correction = true;
1410
-
1411
- resolved.push({
1412
- statement1_id: contradiction.statement1_id,
1413
- statement2_id: contradiction.statement2_id,
1414
- winner: resolution.winner,
1415
- confidence: resolution.confidence,
1416
- reason: resolution.reasons.join(',')
1417
- });
1418
- } else {
1419
- // Needs clarification
1420
- contradiction.resolution = 'clarification_needed';
1421
- contradiction.confidence = resolution.confidence;
1422
-
1423
- const question = generateContradictionQuestion(stmt1, stmt2, contradiction);
1424
-
1425
- // Add to clarifications
1426
- const clarId = `c-${String(clarifications.contradictions.length + 1).padStart(3, '0')}`;
1427
- clarifications.contradictions.push({
1428
- id: clarId,
1429
- type: contradiction.type,
1430
- attribute: contradiction.attribute,
1431
- statements: [contradiction.statement1_id, contradiction.statement2_id],
1432
- topic_id: stmt1.topic_id || stmt2.topic_id,
1433
- question: question.question,
1434
- options: question.options,
1435
- status: 'pending',
1436
- created_at: now()
1437
- });
1438
-
1439
- contradiction.clarification_id = clarId;
1440
-
1441
- pending.push({
1442
- statement1_id: contradiction.statement1_id,
1443
- statement2_id: contradiction.statement2_id,
1444
- clarification_id: clarId,
1445
- confidence: resolution.confidence
1446
- });
1447
- }
1448
- }
1449
-
1450
- // Filter out additive patterns from contradictions list
1451
- stmtMap.contradictions = contradictions.filter(c => c.resolution !== 'not_contradiction');
1452
-
1453
- // Update clarifications metadata
1454
- clarifications.metadata.total_contradictions = contradictions.length;
1455
- clarifications.metadata.auto_resolved_count = resolved.length;
1456
- clarifications.metadata.pending_questions = pending.length;
1457
-
1458
- // Save updated files
1459
- const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
1460
- fs.writeFileSync(mapPath, JSON.stringify(stmtMap, null, 2));
1461
-
1462
- saveClarifications(clarifications);
1463
-
1464
- const stats = {
1465
- total: contradictions.length,
1466
- auto_resolved: resolved.length,
1467
- needs_clarification: pending.length,
1468
- additive_not_contradiction: additive.length
1469
- };
1470
-
1471
- updatePhase('contradiction_resolution', 'completed', stats);
1472
-
1473
- return {
1474
- resolved,
1475
- pending,
1476
- additive,
1477
- stats
1478
- };
1479
- }
1480
-
1481
- // ============================================
1482
- // Question Generation (E2-S1)
1483
- // ============================================
1484
-
1485
- // ==========================================================================
1486
- // E5-S2: Multi-language Question Templates
1487
- // ==========================================================================
1488
-
1489
- /**
1490
- * Get question templates for a specific language (E5-S2)
1491
- */
1492
- function getQuestionTemplates(languageCode) {
1493
- // Check if we have templates for this language
1494
- if (QUESTION_TEMPLATES_BY_LANGUAGE[languageCode]) {
1495
- return QUESTION_TEMPLATES_BY_LANGUAGE[languageCode];
1496
- }
1497
- // Fall back to English
1498
- return QUESTION_TEMPLATES_BY_LANGUAGE.en;
1499
- }
1500
-
1501
- /**
1502
- * Generate a localized question (E5-S2)
1503
- */
1504
- function generateLocalizedQuestion(templateKey, detailKey, entity, language = 'en') {
1505
- // Check if language is directly supported
1506
- const isLanguageSupported = QUESTION_TEMPLATES_BY_LANGUAGE.hasOwnProperty(language);
1507
- const effectiveLang = isLanguageSupported ? language : 'en';
1508
-
1509
- const templates = getQuestionTemplates(language);
1510
- const template = templates[templateKey]?.[detailKey];
1511
-
1512
- if (!template) {
1513
- // Fall back to English if template not found
1514
- const enTemplate = QUESTION_TEMPLATES[templateKey]?.[detailKey];
1515
- if (enTemplate) {
1516
- return {
1517
- question: enTemplate.question.replace('{entity}', entity),
1518
- examples: enTemplate.examples || null,
1519
- priority: enTemplate.priority || 'P2',
1520
- language: 'en',
1521
- fallback: true
1522
- };
1523
- }
1524
- return null;
1525
- }
1526
-
1527
- return {
1528
- question: template.question.replace('{entity}', entity),
1529
- examples: template.examples || null,
1530
- priority: template.priority || 'P2',
1531
- language: effectiveLang,
1532
- fallback: !isLanguageSupported
1533
- };
1534
- }
1535
-
1536
- /**
1537
- * Detect and store session language (E5-S2)
1538
- */
1539
- function detectSessionLanguage() {
1540
- const activeDigest = loadActiveDigest();
1541
- if (!activeDigest.session.digest_path) {
1542
- throw new Error('No active digest session');
1543
- }
1544
-
1545
- // Load the original transcript
1546
- const digestDir = activeDigest.session.digest_path;
1547
- const transcriptPath = path.join(digestDir, 'transcript.txt');
1548
-
1549
- if (!fs.existsSync(transcriptPath)) {
1550
- return {
1551
- detected: false,
1552
- reason: 'No transcript file found'
1553
- };
1554
- }
1555
-
1556
- const transcript = fs.readFileSync(transcriptPath, 'utf8');
1557
-
1558
- // Detect primary language
1559
- const langResult = detectLanguage(transcript);
1560
-
1561
- // Detect if multi-language
1562
- const multiResult = detectMultipleLanguages(transcript, { segmentSize: 500 });
1563
-
1564
- // Update session with language info
1565
- activeDigest.session.detected_language = langResult.language;
1566
- activeDigest.session.language_confidence = langResult.confidence;
1567
- activeDigest.session.is_multilingual = multiResult.isMultilingual;
1568
- activeDigest.session.language_distribution = multiResult.distribution || {};
1569
-
1570
- saveActiveDigest(activeDigest);
1571
-
1572
- return {
1573
- detected: true,
1574
- language: langResult.language,
1575
- languageName: LANGUAGE_INFO[langResult.language]?.name || 'Unknown',
1576
- confidence: langResult.confidence,
1577
- isMultilingual: multiResult.isMultilingual,
1578
- distribution: multiResult.distribution
1579
- };
1580
- }
1581
-
1582
- /**
1583
- * Get language for a topic (E5-S2)
1584
- */
1585
- function getTopicLanguage(topicId) {
1586
- const topics = loadTopics();
1587
- const stmtMap = loadStatementMap();
1588
- const activeDigest = loadActiveDigest();
1589
-
1590
- if (!topics || !stmtMap) {
1591
- return activeDigest.session?.detected_language || 'en';
1592
- }
1593
-
1594
- // Find the topic
1595
- const topic = topics.topics.find(t => t.id === topicId);
1596
- if (!topic) {
1597
- return activeDigest.session?.detected_language || 'en';
1598
- }
1599
-
1600
- // If topic has a stored language, use it
1601
- if (topic.language) {
1602
- return topic.language;
1603
- }
1604
-
1605
- // Detect language from topic's statements
1606
- const topicStatements = stmtMap.statements.filter(s => s.topic_id === topicId && s.meaningful);
1607
- if (topicStatements.length === 0) {
1608
- return activeDigest.session?.detected_language || 'en';
1609
- }
1610
-
1611
- // Combine statement text and detect
1612
- const combinedText = topicStatements.map(s => s.text).join('\n');
1613
- const result = detectLanguage(combinedText);
1614
-
1615
- return result.language;
1616
- }
1617
-
1618
- /**
1619
- * Set user language preference (E5-S2)
1620
- */
1621
- function setLanguagePreference(languageCode) {
1622
- const activeDigest = loadActiveDigest();
1623
- if (!activeDigest.session.digest_path) {
1624
- throw new Error('No active digest session');
1625
- }
1626
-
1627
- // Validate language code
1628
- const info = getLanguageInfo(languageCode);
1629
- if (!info.supported) {
1630
- throw new Error(`Unsupported language code: ${languageCode}`);
1631
- }
1632
-
1633
- activeDigest.session.preferred_language = languageCode;
1634
- saveActiveDigest(activeDigest);
1635
-
1636
- return {
1637
- set: true,
1638
- language: languageCode,
1639
- languageName: info.name
1640
- };
1641
- }
1642
-
1643
- /**
1644
- * Get effective language for question generation (E5-S2)
1645
- */
1646
- function getEffectiveLanguage(topicId = null) {
1647
- const activeDigest = loadActiveDigest();
1648
-
1649
- // Priority 1: User preference
1650
- if (activeDigest.session?.preferred_language) {
1651
- return activeDigest.session.preferred_language;
1652
- }
1653
-
1654
- // Priority 2: Topic-specific language
1655
- if (topicId) {
1656
- const topicLang = getTopicLanguage(topicId);
1657
- if (topicLang && QUESTION_TEMPLATES_BY_LANGUAGE[topicLang]) {
1658
- return topicLang;
1659
- }
1660
- }
1661
-
1662
- // Priority 3: Session detected language
1663
- if (activeDigest.session?.detected_language &&
1664
- QUESTION_TEMPLATES_BY_LANGUAGE[activeDigest.session.detected_language]) {
1665
- return activeDigest.session.detected_language;
1666
- }
1667
-
1668
- // Default: English
1669
- return 'en';
1670
- }
1671
-
1672
- /**
1673
- * Get session language info (E5-S2)
1674
- */
1675
- function getSessionLanguageInfo() {
1676
- const activeDigest = loadActiveDigest();
1677
-
1678
- return {
1679
- detected: activeDigest.session?.detected_language || null,
1680
- detectedName: LANGUAGE_INFO[activeDigest.session?.detected_language]?.name || null,
1681
- confidence: activeDigest.session?.language_confidence || null,
1682
- preferred: activeDigest.session?.preferred_language || null,
1683
- preferredName: LANGUAGE_INFO[activeDigest.session?.preferred_language]?.name || null,
1684
- isMultilingual: activeDigest.session?.is_multilingual || false,
1685
- distribution: activeDigest.session?.language_distribution || {},
1686
- effective: getEffectiveLanguage()
1687
- };
1688
- }
1689
-
1690
- // ==========================================================================
1691
- // E3-S1: Complexity Detection Patterns
1692
- // ==========================================================================
1693
-
1694
- /**
1695
- * Check if a detail is already mentioned in topic statements
1696
- */
1697
- function isDetailProvided(detail, topicId, statements) {
1698
- const topicStatements = statements.filter(s => s.topic_id === topicId && s.meaningful);
1699
- const pattern = DETAIL_PATTERNS[detail];
1700
- if (!pattern) return false;
1701
- return topicStatements.some(s => pattern.test(s.text));
1702
- }
1703
-
1704
- /**
1705
- * Extract entity name from statement
1706
- */
1707
- function extractEntityFromStatement(statement, pattern) {
1708
- const match = statement.text.match(pattern.pattern);
1709
- if (match && pattern.entity !== null) {
1710
- return match[pattern.entity];
1711
- }
1712
- return pattern.type;
1713
- }
1714
-
1715
- /**
1716
- * Analyze statement for completeness gaps
1717
- */
1718
- function analyzeCompleteness(statement, topicId, allStatements) {
1719
- const gaps = [];
1720
- const text = statement.text.toLowerCase();
1721
-
1722
- for (const entityPattern of ENTITY_PATTERNS) {
1723
- if (entityPattern.pattern.test(text)) {
1724
- const entity = extractEntityFromStatement(statement, entityPattern);
1725
-
1726
- for (const detail of entityPattern.missing) {
1727
- if (!isDetailProvided(detail, topicId, allStatements)) {
1728
- gaps.push({
1729
- type: entityPattern.type,
1730
- entity,
1731
- detail,
1732
- statementId: statement.id
1733
- });
1734
- }
1735
- }
1736
- }
1737
- }
1738
-
1739
- return gaps;
1740
- }
1741
-
1742
- /**
1743
- * Check if statement is vague
1744
- */
1745
- function detectVagueness(statement) {
1746
- for (const vague of VAGUE_PATTERNS) {
1747
- if (vague.pattern.test(statement.text)) {
1748
- return {
1749
- isVague: true,
1750
- key: vague.key,
1751
- question: vague.question
1752
- };
1753
- }
1754
- }
1755
- return { isVague: false };
1756
- }
1757
-
1758
- /**
1759
- * Generate question ID
1760
- */
1761
458
  let questionCounter = 0;
1762
- function generateQuestionId() {
1763
- questionCounter++;
1764
- return `q-${String(questionCounter).padStart(3, '0')}`;
1765
- }
459
+ function generateQuestionId() { questionCounter++; return `q-${String(questionCounter).padStart(3, '0')}`; }
1766
460
 
1767
- /**
1768
- * Generate questions for a topic
1769
- */
1770
461
  function generateQuestionsForTopic(topic, statements, allStatements) {
1771
462
  const questions = [];
1772
463
  const topicStatements = statements.filter(s => s.topic_id === topic.id && s.meaningful && !s.superseded);
1773
-
1774
464
  for (const statement of topicStatements) {
1775
- // Check completeness
1776
465
  const gaps = analyzeCompleteness(statement, topic.id, allStatements);
1777
466
  for (const gap of gaps) {
1778
467
  const template = QUESTION_TEMPLATES[gap.type]?.[gap.detail];
1779
468
  if (template) {
1780
- questions.push({
1781
- id: generateQuestionId(),
1782
- type: 'completeness',
1783
- topic_id: topic.id,
1784
- topic_title: topic.title,
1785
- statement_id: statement.id,
1786
- question: template.question.replace('{entity}', gap.entity),
1787
- detail: gap.detail,
1788
- examples: template.examples || null,
1789
- priority: template.priority || 'P2',
1790
- status: 'pending',
1791
- answer: null,
1792
- created_at: now()
1793
- });
469
+ questions.push({ id: generateQuestionId(), type: 'completeness', topic_id: topic.id, topic_title: topic.title, statement_id: statement.id, question: template.question.replace('{entity}', gap.entity), detail: gap.detail, examples: template.examples || null, priority: template.priority || 'P2', status: 'pending', answer: null, created_at: now() });
1794
470
  }
1795
471
  }
1796
-
1797
- // Check vagueness
1798
472
  const vagueness = detectVagueness(statement);
1799
473
  if (vagueness.isVague) {
1800
- questions.push({
1801
- id: generateQuestionId(),
1802
- type: 'specificity',
1803
- topic_id: topic.id,
1804
- topic_title: topic.title,
1805
- statement_id: statement.id,
1806
- question: vagueness.question,
1807
- original_statement: statement.text,
1808
- priority: 'P2',
1809
- status: 'pending',
1810
- answer: null,
1811
- created_at: now()
1812
- });
474
+ questions.push({ id: generateQuestionId(), type: 'specificity', topic_id: topic.id, topic_title: topic.title, statement_id: statement.id, question: vagueness.question, original_statement: statement.text, priority: 'P2', status: 'pending', answer: null, created_at: now() });
1813
475
  }
1814
476
  }
1815
-
1816
477
  return questions;
1817
478
  }
1818
479
 
1819
- /**
1820
- * Run question generation
1821
- */
1822
480
  function generateAllQuestions() {
1823
481
  const activeDigest = loadActiveDigest();
1824
- if (!activeDigest.session.digest_path) {
1825
- throw new Error('No active digest session');
1826
- }
1827
-
1828
- // Load data
482
+ if (!activeDigest.session.digest_path) { throw new Error('No active digest session'); }
1829
483
  const topics = loadTopics();
1830
484
  const stmtMap = loadStatementMap();
1831
485
  let clarifications = loadClarifications();
1832
-
1833
- if (!topics || !stmtMap) {
1834
- throw new Error('Topics and statement map required - run passes 1-2 first');
1835
- }
1836
-
1837
- // Initialize clarifications if null (no file exists yet)
1838
- if (!clarifications) {
1839
- clarifications = {
1840
- questions: [],
1841
- contradictions: [],
1842
- by_topic: {},
1843
- metadata: {
1844
- total_questions: 0,
1845
- pending_questions: 0,
1846
- answered_questions: 0
1847
- }
1848
- };
1849
- }
1850
-
1851
- // Reset question counter based on existing questions
486
+ if (!topics || !stmtMap) { throw new Error('Topics and statement map required - run passes 1-2 first'); }
487
+ if (!clarifications) { clarifications = { questions: [], contradictions: [], by_topic: {}, metadata: { total_questions: 0, pending_questions: 0, answered_questions: 0 } }; }
1852
488
  questionCounter = clarifications.questions?.length || 0;
1853
-
1854
- // Generate questions for each topic
1855
489
  const allQuestions = [];
1856
490
  const byTopic = {};
1857
-
1858
491
  for (const topic of topics.topics) {
1859
492
  const topicQuestions = generateQuestionsForTopic(topic, stmtMap.statements, stmtMap.statements);
1860
-
1861
- if (topicQuestions.length > 0) {
1862
- allQuestions.push(...topicQuestions);
1863
- byTopic[topic.id] = topicQuestions.map(q => q.id);
1864
- }
493
+ if (topicQuestions.length > 0) { allQuestions.push(...topicQuestions); byTopic[topic.id] = topicQuestions.map(q => q.id); }
1865
494
  }
1866
-
1867
- // Merge with existing clarifications
1868
- clarifications.questions = [
1869
- ...(clarifications.questions || []),
1870
- ...allQuestions
1871
- ];
1872
- clarifications.by_topic = {
1873
- ...(clarifications.by_topic || {}),
1874
- ...byTopic
1875
- };
1876
-
1877
- // Update metadata
495
+ clarifications.questions = [...(clarifications.questions || []), ...allQuestions];
496
+ clarifications.by_topic = { ...(clarifications.by_topic || {}), ...byTopic };
1878
497
  const byType = { completeness: 0, specificity: 0, ambiguity: 0 };
1879
498
  const byPriority = { P1: 0, P2: 0, P3: 0 };
1880
-
1881
- for (const q of clarifications.questions) {
1882
- byType[q.type] = (byType[q.type] || 0) + 1;
1883
- byPriority[q.priority] = (byPriority[q.priority] || 0) + 1;
1884
- }
1885
-
1886
- clarifications.metadata = {
1887
- ...clarifications.metadata,
1888
- total_questions: clarifications.questions.length,
1889
- pending_questions: clarifications.questions.filter(q => q.status === 'pending').length,
1890
- answered_questions: clarifications.questions.filter(q => q.status === 'answered').length,
1891
- by_type: byType,
1892
- by_priority: byPriority
1893
- };
1894
-
1895
- // Save
499
+ for (const q of clarifications.questions) { byType[q.type] = (byType[q.type] || 0) + 1; byPriority[q.priority] = (byPriority[q.priority] || 0) + 1; }
500
+ clarifications.metadata = { ...clarifications.metadata, total_questions: clarifications.questions.length, pending_questions: clarifications.questions.filter(q => q.status === 'pending').length, answered_questions: clarifications.questions.filter(q => q.status === 'answered').length, by_type: byType, by_priority: byPriority };
1896
501
  saveClarifications(clarifications);
1897
-
1898
- // Update phase
1899
- updatePhase('clarification', 'in_progress', {
1900
- questions_total: allQuestions.length,
1901
- questions_answered: 0
1902
- });
1903
-
1904
- return {
1905
- questions: allQuestions,
1906
- by_topic: byTopic,
1907
- stats: {
1908
- total: allQuestions.length,
1909
- by_type: byType,
1910
- by_priority: byPriority,
1911
- topics_with_questions: Object.keys(byTopic).length
1912
- }
1913
- };
502
+ updatePhase('clarification', 'in_progress', { questions_total: allQuestions.length, questions_answered: 0 });
503
+ return { questions: allQuestions, by_topic: byTopic, stats: { total: allQuestions.length, by_type: byType, by_priority: byPriority, topics_with_questions: Object.keys(byTopic).length } };
1914
504
  }
1915
505
 
1916
506
  // ============================================
1917
- // E2-S2: Clarification Conversation Loop
507
+ // Conversation Loop (E2-S2)
1918
508
  // ============================================
1919
509
 
1920
- /**
1921
- * Keywords to extract from questions for matching
1922
- */
1923
510
  function extractKeywordsFromQuestion(question) {
1924
511
  const stopWords = ['what', 'which', 'how', 'should', 'the', 'a', 'an', 'for', 'to', 'of', 'in', 'be', 'are', 'is'];
1925
512
  const words = question.text || question.question;
1926
- return words.toLowerCase()
1927
- .replace(/[?.,!]/g, '')
1928
- .split(/\s+/)
1929
- .filter(w => w.length > 2 && !stopWords.includes(w));
513
+ return words.toLowerCase().replace(/[?.,!]/g, '').split(/\s+/).filter(w => w.length > 2 && !stopWords.includes(w));
1930
514
  }
1931
515
 
1932
- /**
1933
- * Parse user response and match answers to questions
1934
- */
1935
516
  function parseAnswers(userResponse, questions) {
1936
517
  const answers = [];
1937
518
  const text = userResponse.trim();
1938
-
1939
- // Try numbered responses first (1. answer, 2. answer)
1940
519
  const numberedPattern = /(?:^|\n)\s*(\d+)[.)]\s*(.+?)(?=\n\s*\d+[.)]|\n*$)/gs;
1941
520
  const numberedMatches = [...text.matchAll(numberedPattern)];
1942
-
1943
521
  if (numberedMatches.length > 0) {
1944
- for (const match of numberedMatches) {
1945
- const num = parseInt(match[1], 10);
1946
- const answer = match[2].trim();
1947
- if (num >= 1 && num <= questions.length) {
1948
- answers.push({
1949
- question_id: questions[num - 1].id,
1950
- answer,
1951
- confidence: 0.95,
1952
- match_method: 'numbered'
1953
- });
1954
- }
1955
- }
522
+ for (const match of numberedMatches) { const num = parseInt(match[1], 10); const answer = match[2].trim(); if (num >= 1 && num <= questions.length) { answers.push({ question_id: questions[num - 1].id, answer, confidence: 0.95, match_method: 'numbered' }); } }
1956
523
  return answers;
1957
524
  }
1958
-
1959
- // Try explicit keyword matches (for X, the Y should, etc.)
1960
525
  for (const question of questions) {
1961
526
  const keywords = extractKeywordsFromQuestion(question);
1962
-
1963
527
  for (const keyword of keywords) {
1964
- // Escape special regex characters in keyword
1965
528
  const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1966
-
1967
- // Pattern: "for [keyword], [answer]" or "[keyword]: [answer]"
1968
- const patterns = [
1969
- new RegExp(`(?:for\\s+(?:the\\s+)?)?${escapedKeyword}[,:]\\s*(.+?)(?:\\.|$|\\n)`, 'i'),
1970
- new RegExp(`${escapedKeyword}\\s+(?:should\\s+(?:be|have|show)\\s+)?(.+?)(?:\\.|$|\\n)`, 'i')
1971
- ];
1972
-
1973
- for (const pattern of patterns) {
1974
- const match = text.match(pattern);
1975
- if (match && match[1] && match[1].trim().length > 2) {
1976
- // Check if we already have an answer for this question
1977
- const existing = answers.find(a => a.question_id === question.id);
1978
- if (!existing) {
1979
- answers.push({
1980
- question_id: question.id,
1981
- answer: match[1].trim(),
1982
- confidence: 0.85,
1983
- match_method: 'keyword',
1984
- matched_keyword: keyword
1985
- });
1986
- }
1987
- break;
1988
- }
1989
- }
529
+ const patterns = [new RegExp(`(?:for\\s+(?:the\\s+)?)?${escapedKeyword}[,:]\\s*(.+?)(?:\\.|$|\\n)`, 'i'), new RegExp(`${escapedKeyword}\\s+(?:should\\s+(?:be|have|show)\\s+)?(.+?)(?:\\.|$|\\n)`, 'i')];
530
+ for (const pattern of patterns) { const match = text.match(pattern); if (match && match[1] && match[1].trim().length > 2) { const existing = answers.find(a => a.question_id === question.id); if (!existing) { answers.push({ question_id: question.id, answer: match[1].trim(), confidence: 0.85, match_method: 'keyword', matched_keyword: keyword }); } break; } }
1990
531
  }
1991
532
  }
1992
-
1993
- // If only one question and no matches yet, assume entire response is the answer
1994
- if (questions.length === 1 && answers.length === 0 && text.length > 2) {
1995
- answers.push({
1996
- question_id: questions[0].id,
1997
- answer: text,
1998
- confidence: 0.8,
1999
- match_method: 'single_question'
2000
- });
2001
- }
2002
-
2003
- // For sequential responses separated by periods or commas
533
+ if (questions.length === 1 && answers.length === 0 && text.length > 2) { answers.push({ question_id: questions[0].id, answer: text, confidence: 0.8, match_method: 'single_question' }); }
2004
534
  if (answers.length === 0 && questions.length > 1) {
2005
535
  const segments = text.split(/[.]\s+/).filter(s => s.trim().length > 2);
2006
- if (segments.length === questions.length) {
2007
- for (let i = 0; i < segments.length; i++) {
2008
- answers.push({
2009
- question_id: questions[i].id,
2010
- answer: segments[i].trim(),
2011
- confidence: 0.7,
2012
- match_method: 'sequential'
2013
- });
2014
- }
2015
- }
536
+ if (segments.length === questions.length) { for (let i = 0; i < segments.length; i++) { answers.push({ question_id: questions[i].id, answer: segments[i].trim(), confidence: 0.7, match_method: 'sequential' }); } }
2016
537
  }
2017
-
2018
538
  return answers;
2019
539
  }
2020
540
 
2021
- /**
2022
- * Capture answer for a specific question
2023
- */
2024
541
  function captureAnswer(questionId, answer, source = 'text') {
2025
542
  const clarifications = loadClarifications();
2026
- if (!clarifications) {
2027
- throw new Error('No clarifications found');
2028
- }
2029
-
543
+ if (!clarifications) { throw new Error('No clarifications found'); }
2030
544
  const question = clarifications.questions.find(q => q.id === questionId);
2031
- if (!question) {
2032
- throw new Error(`Question ${questionId} not found`);
2033
- }
2034
-
2035
- // Update question status
2036
- question.status = 'answered';
2037
- question.answer = answer;
2038
- question.answered_at = now();
2039
- question.answer_source = source;
2040
-
2041
- // Update metadata
545
+ if (!question) { throw new Error(`Question ${questionId} not found`); }
546
+ question.status = 'answered'; question.answer = answer; question.answered_at = now(); question.answer_source = source;
2042
547
  clarifications.metadata.answered_questions = (clarifications.metadata.answered_questions || 0) + 1;
2043
548
  clarifications.metadata.pending_questions = clarifications.questions.filter(q => q.status === 'pending').length;
2044
-
2045
549
  saveClarifications(clarifications);
2046
-
2047
550
  return question;
2048
551
  }
2049
552
 
2050
- /**
2051
- * Create a derived statement from clarification answer
2052
- */
2053
553
  function createDerivedStatement(question, answer) {
2054
554
  const activeDigest = loadActiveDigest();
2055
- if (!activeDigest.session.digest_path) {
2056
- throw new Error('No active digest session');
2057
- }
2058
-
2059
- // Load statement map
555
+ if (!activeDigest.session.digest_path) { throw new Error('No active digest session'); }
2060
556
  const stmtMap = loadStatementMap();
2061
- if (!stmtMap) {
2062
- throw new Error('No statement map found');
2063
- }
2064
-
2065
- // Generate ID
2066
- const maxId = stmtMap.statements
2067
- .map(s => parseInt(s.id.replace('s-', '').replace('derived-', ''), 10) || 0)
2068
- .reduce((max, id) => Math.max(max, id), 0);
2069
-
557
+ if (!stmtMap) { throw new Error('No statement map found'); }
558
+ const maxId = stmtMap.statements.map(s => parseInt(s.id.replace('s-', '').replace('derived-', ''), 10) || 0).reduce((max, id) => Math.max(max, id), 0);
2070
559
  const newId = `s-derived-${String(maxId + 1).padStart(3, '0')}`;
2071
-
2072
- // Create statement text from question and answer
2073
560
  let text;
2074
- if (question.detail) {
2075
- // Completeness question - create specific statement
2076
- const entity = question.question.match(/the (\w+) (table|form|button|list|modal)/i)?.[1] || '';
2077
- text = `The ${entity} ${question.detail} should be: ${answer}`;
2078
- } else {
2079
- // Specificity question - incorporate answer
2080
- text = answer;
2081
- }
2082
-
2083
- const derivedStatement = {
2084
- id: newId,
2085
- text,
2086
- topic_id: question.topic_id,
2087
- source: 'clarification',
2088
- clarification_id: question.id,
2089
- meaningful: true,
2090
- confidence: 1.0,
2091
- created_at: now()
2092
- };
2093
-
2094
- // Add to statement map
2095
- stmtMap.statements.push(derivedStatement);
2096
- stmtMap.metadata.total_statements++;
2097
- stmtMap.metadata.meaningful_statements++;
2098
- stmtMap.metadata.mapped_statements++;
2099
-
2100
- // Save
561
+ if (question.detail) { const entity = question.question.match(/the (\w+) (table|form|button|list|modal)/i)?.[1] || ''; text = `The ${entity} ${question.detail} should be: ${answer}`; } else { text = answer; }
562
+ const derivedStatement = { id: newId, text, topic_id: question.topic_id, source: 'clarification', clarification_id: question.id, meaningful: true, confidence: 1.0, created_at: now() };
563
+ stmtMap.statements.push(derivedStatement); stmtMap.metadata.total_statements++; stmtMap.metadata.meaningful_statements++; stmtMap.metadata.mapped_statements++;
2101
564
  const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
2102
565
  fs.writeFileSync(mapPath, JSON.stringify(stmtMap, null, 2));
2103
-
2104
566
  return derivedStatement;
2105
567
  }
2106
568
 
2107
- /**
2108
- * Check if an answer should generate follow-up questions
2109
- */
2110
569
  function checkFollowups(answer, question) {
2111
570
  const followups = [];
2112
-
2113
571
  for (const trigger of FOLLOWUP_TRIGGERS) {
2114
- if (trigger.pattern.test(answer)) {
2115
- // Don't generate follow-up if the answer already addresses it
2116
- const entity = question.detail || 'item';
2117
-
2118
- followups.push({
2119
- type: trigger.type,
2120
- triggered_by: trigger.pattern.source,
2121
- question: trigger.question.replace('{item}', entity),
2122
- parent_question_id: question.id,
2123
- topic_id: question.topic_id,
2124
- priority: 'P2'
2125
- });
2126
- }
572
+ if (trigger.pattern.test(answer)) { const entity = question.detail || 'item'; followups.push({ type: trigger.type, triggered_by: trigger.pattern.source, question: trigger.question.replace('{item}', entity), parent_question_id: question.id, topic_id: question.topic_id, priority: 'P2' }); }
2127
573
  }
2128
-
2129
574
  return followups;
2130
575
  }
2131
576
 
2132
- /**
2133
- * Add follow-up questions to clarifications
2134
- */
2135
577
  function addFollowupQuestions(followups) {
2136
578
  if (followups.length === 0) return [];
2137
-
2138
579
  const clarifications = loadClarifications();
2139
580
  const addedQuestions = [];
2140
-
2141
581
  for (const followup of followups) {
2142
- // Check if similar question already exists
2143
- const exists = clarifications.questions.some(q =>
2144
- q.topic_id === followup.topic_id &&
2145
- q.question.toLowerCase().includes(followup.question.toLowerCase().slice(0, 30))
2146
- );
2147
-
2148
- if (!exists) {
2149
- const newQuestion = {
2150
- id: generateQuestionId(),
2151
- type: 'followup',
2152
- topic_id: followup.topic_id,
2153
- parent_question_id: followup.parent_question_id,
2154
- question: followup.question,
2155
- priority: followup.priority,
2156
- status: 'pending',
2157
- answer: null,
2158
- created_at: now()
2159
- };
2160
-
2161
- clarifications.questions.push(newQuestion);
2162
- addedQuestions.push(newQuestion);
2163
- }
582
+ const exists = clarifications.questions.some(q => q.topic_id === followup.topic_id && q.question.toLowerCase().includes(followup.question.toLowerCase().slice(0, 30)));
583
+ if (!exists) { const newQuestion = { id: generateQuestionId(), type: 'followup', topic_id: followup.topic_id, parent_question_id: followup.parent_question_id, question: followup.question, priority: followup.priority, status: 'pending', answer: null, created_at: now() }; clarifications.questions.push(newQuestion); addedQuestions.push(newQuestion); }
2164
584
  }
2165
-
2166
- // Update metadata
2167
585
  clarifications.metadata.total_questions = clarifications.questions.length;
2168
586
  clarifications.metadata.pending_questions = clarifications.questions.filter(q => q.status === 'pending').length;
2169
-
2170
587
  saveClarifications(clarifications);
2171
588
  return addedQuestions;
2172
589
  }
2173
590
 
2174
- /**
2175
- * Check if all clarifications are complete
2176
- */
2177
591
  function checkCompletion() {
2178
592
  const clarifications = loadClarifications();
2179
- if (!clarifications) {
2180
- return { complete: false, error: 'No clarifications found' };
2181
- }
2182
-
593
+ if (!clarifications) { return { complete: false, error: 'No clarifications found' }; }
2183
594
  const pendingQuestions = clarifications.questions.filter(q => q.status === 'pending');
2184
595
  const pendingContradictions = clarifications.contradictions.filter(c => c.status === 'pending');
2185
-
2186
596
  const complete = pendingQuestions.length === 0 && pendingContradictions.length === 0;
2187
-
2188
- const result = {
2189
- complete,
2190
- pending_questions: pendingQuestions.length,
2191
- pending_contradictions: pendingContradictions.length,
2192
- answered_questions: clarifications.questions.filter(q => q.status === 'answered').length,
2193
- resolved_contradictions: clarifications.contradictions.filter(c => c.status === 'resolved').length,
2194
- total_questions: clarifications.questions.length,
2195
- total_contradictions: clarifications.contradictions.length
2196
- };
2197
-
2198
- // If complete, update phase
2199
- if (complete) {
2200
- updatePhase('clarification', 'completed', {
2201
- questions_total: result.total_questions,
2202
- questions_answered: result.answered_questions
2203
- });
2204
- }
2205
-
597
+ const result = { complete, pending_questions: pendingQuestions.length, pending_contradictions: pendingContradictions.length, answered_questions: clarifications.questions.filter(q => q.status === 'answered').length, resolved_contradictions: clarifications.contradictions.filter(c => c.status === 'resolved').length, total_questions: clarifications.questions.length, total_contradictions: clarifications.contradictions.length };
598
+ if (complete) { updatePhase('clarification', 'completed', { questions_total: result.total_questions, questions_answered: result.answered_questions }); }
2206
599
  return result;
2207
600
  }
2208
601
 
2209
- /**
2210
- * Get questions for presentation (grouped by topic, prioritized)
2211
- */
2212
602
  function getQuestionsForPresentation(topicId = null, limit = 5) {
2213
603
  const clarifications = loadClarifications();
2214
604
  if (!clarifications) return [];
2215
-
2216
605
  let pendingQuestions = clarifications.questions.filter(q => q.status === 'pending');
2217
-
2218
- // Filter by topic if specified
2219
- if (topicId) {
2220
- pendingQuestions = pendingQuestions.filter(q => q.topic_id === topicId);
2221
- }
2222
-
2223
- // Sort by priority (P1 first) then by creation time
606
+ if (topicId) { pendingQuestions = pendingQuestions.filter(q => q.topic_id === topicId); }
2224
607
  const priorityOrder = { P1: 0, P2: 1, P3: 2 };
2225
- pendingQuestions.sort((a, b) => {
2226
- const pDiff = (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2);
2227
- if (pDiff !== 0) return pDiff;
2228
- return new Date(a.created_at) - new Date(b.created_at);
2229
- });
2230
-
2231
- // Limit
608
+ pendingQuestions.sort((a, b) => { const pDiff = (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2); if (pDiff !== 0) return pDiff; return new Date(a.created_at) - new Date(b.created_at); });
2232
609
  return pendingQuestions.slice(0, limit);
2233
610
  }
2234
611
 
2235
- /**
2236
- * Format questions for display to user
2237
- */
2238
612
  function formatQuestionsForUser(questions) {
2239
613
  if (questions.length === 0) return null;
2240
-
2241
- // Group by topic
2242
614
  const byTopic = {};
2243
- for (const q of questions) {
2244
- const topicKey = q.topic_title || q.topic_id || 'General';
2245
- if (!byTopic[topicKey]) {
2246
- byTopic[topicKey] = [];
2247
- }
2248
- byTopic[topicKey].push(q);
2249
- }
2250
-
615
+ for (const q of questions) { const topicKey = q.topic_title || q.topic_id || 'General'; if (!byTopic[topicKey]) { byTopic[topicKey] = []; } byTopic[topicKey].push(q); }
2251
616
  let output = '';
2252
617
  for (const [topic, qs] of Object.entries(byTopic)) {
2253
618
  output += `## Topic: ${topic} (${qs.length} question${qs.length > 1 ? 's' : ''})\n\n`;
2254
-
2255
- for (let i = 0; i < qs.length; i++) {
2256
- const q = qs[i];
2257
- output += `${i + 1}. **[${q.priority}]** ${q.question}\n`;
2258
- if (q.examples && q.examples.length > 0) {
2259
- output += ` _Examples: "${q.examples.join('" or "')}"_\n`;
2260
- }
2261
- output += '\n';
2262
- }
619
+ for (let i = 0; i < qs.length; i++) { const q = qs[i]; output += `${i + 1}. **[${q.priority}]** ${q.question}\n`; if (q.examples && q.examples.length > 0) { output += ` _Examples: "${q.examples.join('" or "')}"_\n`; } output += '\n'; }
2263
620
  }
2264
-
2265
621
  output += '---\n\nYou can answer all at once or one at a time. Just reply naturally!';
2266
-
2267
622
  return output;
2268
623
  }
2269
624
 
2270
- /**
2271
- * Process user answers in conversation
2272
- * @param {string} userResponse - User's answer text
2273
- * @param {object} options - Processing options
2274
- * @param {boolean} options.forceVoice - Force voice processing
2275
- */
2276
625
  function processConversationResponse(userResponse, options = {}) {
2277
626
  const clarifications = loadClarifications();
2278
- if (!clarifications) {
2279
- return { error: 'No active clarification session' };
2280
- }
2281
-
2282
- // Get currently pending questions (prioritized)
627
+ if (!clarifications) { return { error: 'No active clarification session' }; }
2283
628
  const pendingQuestions = getQuestionsForPresentation(null, 10);
2284
- if (pendingQuestions.length === 0) {
2285
- return {
2286
- complete: true,
2287
- message: 'All questions have been answered!'
2288
- };
2289
- }
2290
-
2291
- // Process voice input if detected or forced
629
+ if (pendingQuestions.length === 0) { return { complete: true, message: 'All questions have been answered!' }; }
2292
630
  let processedInput = userResponse;
2293
631
  let voiceProcessing = null;
2294
-
2295
632
  const voiceResult = processVoiceAnswer(userResponse, options.forceVoice);
2296
- if (voiceResult.isVoice) {
2297
- processedInput = voiceResult.normalized;
2298
- voiceProcessing = voiceResult.processing;
2299
- }
2300
-
2301
- // Parse the user's response (using normalized text if voice)
633
+ if (voiceResult.isVoice) { processedInput = voiceResult.normalized; voiceProcessing = voiceResult.processing; }
2302
634
  const parsedAnswers = parseAnswers(processedInput, pendingQuestions);
2303
-
2304
- const results = {
2305
- captured: [],
2306
- derived_statements: [],
2307
- followups_added: [],
2308
- remaining_questions: 0,
2309
- complete: false,
2310
- voice: voiceProcessing ? {
2311
- detected: true,
2312
- original: userResponse,
2313
- normalized: processedInput,
2314
- processing: voiceProcessing
2315
- } : null
2316
- };
2317
-
2318
- // Determine source (voice or text)
635
+ const results = { captured: [], derived_statements: [], followups_added: [], remaining_questions: 0, complete: false, voice: voiceProcessing ? { detected: true, original: userResponse, normalized: processedInput, processing: voiceProcessing } : null };
2319
636
  const answerSource = voiceProcessing ? 'voice' : 'conversation';
2320
-
2321
- // Record the answer received interaction
2322
- recordInteraction('answer_received', {
2323
- raw_input: userResponse,
2324
- source: answerSource,
2325
- voice_processed: !!voiceProcessing,
2326
- parsed_count: parsedAnswers.length
2327
- });
2328
-
2329
- // Process each parsed answer
637
+ recordInteraction('answer_received', { raw_input: userResponse, source: answerSource, voice_processed: !!voiceProcessing, parsed_count: parsedAnswers.length });
2330
638
  for (const parsed of parsedAnswers) {
2331
639
  const question = pendingQuestions.find(q => q.id === parsed.question_id);
2332
640
  if (!question) continue;
2333
-
2334
- // Capture the answer
2335
641
  captureAnswer(parsed.question_id, parsed.answer, answerSource);
2336
- results.captured.push({
2337
- question_id: parsed.question_id,
2338
- question: question.question,
2339
- answer: parsed.answer,
2340
- confidence: parsed.confidence
2341
- });
2342
-
2343
- // Create derived statement
642
+ results.captured.push({ question_id: parsed.question_id, question: question.question, answer: parsed.answer, confidence: parsed.confidence });
2344
643
  const derivedStmt = createDerivedStatement(question, parsed.answer);
2345
644
  results.derived_statements.push(derivedStmt);
2346
-
2347
- // Check for follow-ups
2348
645
  const followups = checkFollowups(parsed.answer, question);
2349
- if (followups.length > 0) {
2350
- const added = addFollowupQuestions(followups);
2351
- results.followups_added.push(...added);
2352
- }
646
+ if (followups.length > 0) { const added = addFollowupQuestions(followups); results.followups_added.push(...added); }
2353
647
  }
2354
-
2355
- // Check completion
2356
648
  const completion = checkCompletion();
2357
649
  results.complete = completion.complete;
2358
650
  results.remaining_questions = completion.pending_questions + completion.pending_contradictions;
2359
-
2360
- // Get next questions if not complete
2361
- if (!completion.complete) {
2362
- results.next_questions = getQuestionsForPresentation(null, 5);
2363
- results.formatted_questions = formatQuestionsForUser(results.next_questions);
2364
- }
2365
-
651
+ if (!completion.complete) { results.next_questions = getQuestionsForPresentation(null, 5); results.formatted_questions = formatQuestionsForUser(results.next_questions); }
2366
652
  return results;
2367
653
  }
2368
654
 
2369
- /**
2370
- * Resolve a contradiction with user's choice
2371
- */
2372
655
  function resolveContradictionWithChoice(contradictionId, choice) {
2373
656
  const clarifications = loadClarifications();
2374
- if (!clarifications) {
2375
- throw new Error('No clarifications found');
2376
- }
2377
-
657
+ if (!clarifications) { throw new Error('No clarifications found'); }
2378
658
  const contradiction = clarifications.contradictions.find(c => c.id === contradictionId);
2379
- if (!contradiction) {
2380
- throw new Error(`Contradiction ${contradictionId} not found`);
2381
- }
2382
-
2383
- // Load statement map to update
659
+ if (!contradiction) { throw new Error(`Contradiction ${contradictionId} not found`); }
2384
660
  const stmtMap = loadStatementMap();
2385
- if (!stmtMap) {
2386
- throw new Error('No statement map found');
2387
- }
2388
-
2389
- if (choice === 'keep_both') {
2390
- // Both are valid - not a real contradiction
2391
- contradiction.status = 'resolved';
2392
- contradiction.resolution = 'keep_both';
2393
- contradiction.resolved_at = now();
2394
- } else {
2395
- // One wins, other is superseded
661
+ if (!stmtMap) { throw new Error('No statement map found'); }
662
+ if (choice === 'keep_both') { contradiction.status = 'resolved'; contradiction.resolution = 'keep_both'; contradiction.resolved_at = now(); } else {
2396
663
  const winnerStmtId = contradiction.options?.find(o => o.id === choice)?.statement_id;
2397
664
  const loserStmtId = contradiction.statements.find(id => id !== winnerStmtId);
2398
-
2399
- if (winnerStmtId && loserStmtId) {
2400
- const winner = stmtMap.statements.find(s => s.id === winnerStmtId);
2401
- const loser = stmtMap.statements.find(s => s.id === loserStmtId);
2402
-
2403
- if (winner && loser) {
2404
- loser.superseded = true;
2405
- loser.superseded_by = winnerStmtId;
2406
- loser.superseded_reason = 'user_choice';
2407
- winner.supersedes = loserStmtId;
2408
- }
2409
- }
2410
-
2411
- contradiction.status = 'resolved';
2412
- contradiction.resolution = 'user_choice';
2413
- contradiction.winner = winnerStmtId;
2414
- contradiction.resolved_at = now();
665
+ if (winnerStmtId && loserStmtId) { const winner = stmtMap.statements.find(s => s.id === winnerStmtId); const loser = stmtMap.statements.find(s => s.id === loserStmtId); if (winner && loser) { loser.superseded = true; loser.superseded_by = winnerStmtId; loser.superseded_reason = 'user_choice'; winner.supersedes = loserStmtId; } }
666
+ contradiction.status = 'resolved'; contradiction.resolution = 'user_choice'; contradiction.winner = winnerStmtId; contradiction.resolved_at = now();
2415
667
  }
2416
-
2417
- // Update metadata
2418
668
  clarifications.metadata.resolved_contradictions = (clarifications.metadata.resolved_contradictions || 0) + 1;
2419
669
  clarifications.metadata.user_resolved_count = (clarifications.metadata.user_resolved_count || 0) + 1;
2420
-
2421
- // Save both
2422
670
  const activeDigest = loadActiveDigest();
2423
671
  const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
2424
672
  fs.writeFileSync(mapPath, JSON.stringify(stmtMap, null, 2));
2425
673
  saveClarifications(clarifications);
2426
-
2427
674
  return contradiction;
2428
675
  }
2429
676
 
2430
- // E2-S3 (Voice Answer Integration) extracted to flow-long-input-voice.js
2431
-
2432
677
  // ============================================
2433
- // E2-S4: Clarification State Persistence
678
+ // State Persistence (E2-S4)
2434
679
  // ============================================
2435
680
 
2436
- /**
2437
- * Generate unique interaction ID
2438
- */
2439
- function generateInteractionId() {
2440
- return `i-${Date.now().toString(36)}`;
2441
- }
2442
-
2443
- /**
2444
- * Generate unique checkpoint ID
2445
- */
2446
- function generateCheckpointId() {
2447
- return `cp-${Date.now().toString(36)}`;
2448
- }
681
+ function generateInteractionId() { return `i-${Date.now().toString(36)}`; }
682
+ function generateCheckpointId() { return `cp-${Date.now().toString(36)}`; }
2449
683
 
2450
- /**
2451
- * Load conversation history
2452
- */
2453
684
  function loadConversation() {
2454
685
  const activeDigest = loadActiveDigest();
2455
- if (!activeDigest.session?.digest_path) {
2456
- return null;
2457
- }
2458
-
686
+ if (!activeDigest.session?.digest_path) { return null; }
2459
687
  const convPath = path.join(activeDigest.session.digest_path, 'conversation.json');
2460
- if (!fs.existsSync(convPath)) {
2461
- return null;
2462
- }
2463
-
2464
- return JSON.parse(fs.readFileSync(convPath, 'utf8'));
688
+ return safeJsonParse(convPath, null);
2465
689
  }
2466
690
 
2467
- /**
2468
- * Save conversation history
2469
- */
2470
691
  function saveConversation(conversation) {
2471
692
  const activeDigest = loadActiveDigest();
2472
- if (!activeDigest.session?.digest_path) {
2473
- throw new Error('No active digest session');
2474
- }
2475
-
693
+ if (!activeDigest.session?.digest_path) { throw new Error('No active digest session'); }
2476
694
  const convPath = path.join(activeDigest.session.digest_path, 'conversation.json');
2477
695
  fs.writeFileSync(convPath, JSON.stringify(conversation, null, 2));
2478
696
  return conversation;
2479
697
  }
2480
698
 
2481
- /**
2482
- * Initialize conversation history for new session
2483
- */
2484
699
  function initializeConversation(sessionId) {
2485
- const conversation = {
2486
- session_id: sessionId,
2487
- started_at: now(),
2488
- last_interaction: now(),
2489
- interactions: [],
2490
- checkpoints: []
2491
- };
2492
-
2493
- return saveConversation(conversation);
700
+ return saveConversation({ session_id: sessionId, started_at: now(), last_interaction: now(), interactions: [], checkpoints: [] });
2494
701
  }
2495
702
 
2496
- /**
2497
- * Record an interaction in conversation history
2498
- */
2499
703
  function recordInteraction(type, data = {}) {
2500
704
  let conversation = loadConversation();
2501
-
2502
- if (!conversation) {
2503
- const activeDigest = loadActiveDigest();
2504
- if (activeDigest.session?.id) {
2505
- conversation = initializeConversation(activeDigest.session.id);
2506
- } else {
2507
- return null;
2508
- }
2509
- }
2510
-
2511
- const interaction = {
2512
- id: generateInteractionId(),
2513
- type,
2514
- timestamp: now(),
2515
- data
2516
- };
2517
-
2518
- conversation.interactions.push(interaction);
2519
- conversation.last_interaction = now();
2520
-
705
+ if (!conversation) { const activeDigest = loadActiveDigest(); if (activeDigest.session?.id) { conversation = initializeConversation(activeDigest.session.id); } else { return null; } }
706
+ const interaction = { id: generateInteractionId(), type, timestamp: now(), data };
707
+ conversation.interactions.push(interaction); conversation.last_interaction = now();
2521
708
  saveConversation(conversation);
2522
709
  return interaction;
2523
710
  }
2524
711
 
2525
- /**
2526
- * Create a checkpoint for recovery
2527
- */
2528
712
  function createCheckpoint(reason = 'manual') {
2529
713
  const conversation = loadConversation();
2530
- if (!conversation) {
2531
- return null;
2532
- }
2533
-
714
+ if (!conversation) { return null; }
2534
715
  const clarifications = loadClarifications();
2535
716
  const topics = loadTopics();
2536
717
  const activeDigest = loadActiveDigest();
2537
-
2538
718
  const checkpoint = {
2539
- id: generateCheckpointId(),
2540
- timestamp: now(),
2541
- reason,
2542
- phase: activeDigest.phases ? Object.keys(activeDigest.phases).find(p =>
2543
- activeDigest.phases[p]?.status === 'in_progress'
2544
- ) || 'unknown' : 'unknown',
2545
- questions: {
2546
- total: clarifications?.questions?.length || 0,
2547
- answered: clarifications?.questions?.filter(q => q.status === 'answered').length || 0,
2548
- pending: clarifications?.questions?.filter(q => q.status === 'pending').length || 0
2549
- },
2550
- contradictions: {
2551
- total: clarifications?.contradictions?.length || 0,
2552
- resolved: clarifications?.contradictions?.filter(c => c.status === 'resolved').length || 0
2553
- },
2554
- topics: {
2555
- total: topics?.topics?.length || 0,
2556
- clarified: topics?.topics?.filter(t => t.clarification_complete).length || 0
2557
- },
719
+ id: generateCheckpointId(), timestamp: now(), reason,
720
+ phase: activeDigest.phases ? Object.keys(activeDigest.phases).find(p => activeDigest.phases[p]?.status === 'in_progress') || 'unknown' : 'unknown',
721
+ questions: { total: clarifications?.questions?.length || 0, answered: clarifications?.questions?.filter(q => q.status === 'answered').length || 0, pending: clarifications?.questions?.filter(q => q.status === 'pending').length || 0 },
722
+ contradictions: { total: clarifications?.contradictions?.length || 0, resolved: clarifications?.contradictions?.filter(c => c.status === 'resolved').length || 0 },
723
+ topics: { total: topics?.topics?.length || 0, clarified: topics?.topics?.filter(t => t.clarification_complete).length || 0 },
2558
724
  awaiting_response: false
2559
725
  };
2560
-
2561
- // Check if we're awaiting response (last interaction was questions_presented)
2562
726
  const lastInteraction = conversation.interactions.slice(-1)[0];
2563
- if (lastInteraction?.type === 'questions_presented') {
2564
- checkpoint.awaiting_response = true;
2565
- checkpoint.last_questions_presented = lastInteraction.data.question_ids;
2566
- }
2567
-
727
+ if (lastInteraction?.type === 'questions_presented') { checkpoint.awaiting_response = true; checkpoint.last_questions_presented = lastInteraction.data.question_ids; }
2568
728
  conversation.checkpoints.push(checkpoint);
2569
729
  saveConversation(conversation);
2570
-
2571
730
  return checkpoint;
2572
731
  }
2573
732
 
2574
- /**
2575
- * Detect if there's an interrupted session
2576
- */
2577
733
  function detectInterruptedSession() {
2578
734
  const activeDigest = loadActiveDigest();
2579
-
2580
- if (!activeDigest.session?.digest_path) {
2581
- return { interrupted: false };
2582
- }
2583
-
2584
- // Check if session is already complete
2585
- if (activeDigest.session?.status === 'completed') {
2586
- return { interrupted: false };
2587
- }
2588
-
735
+ if (!activeDigest.session?.digest_path) { return { interrupted: false }; }
736
+ if (activeDigest.session?.status === 'completed') { return { interrupted: false }; }
2589
737
  const conversation = loadConversation();
2590
- if (!conversation) {
2591
- return { interrupted: false };
2592
- }
2593
-
2594
- // Check if there are pending questions
738
+ if (!conversation) { return { interrupted: false }; }
2595
739
  const clarifications = loadClarifications();
2596
- if (!clarifications) {
2597
- return { interrupted: false };
2598
- }
2599
-
740
+ if (!clarifications) { return { interrupted: false }; }
2600
741
  const pendingQuestions = clarifications.questions?.filter(q => q.status === 'pending') || [];
2601
- if (pendingQuestions.length === 0) {
2602
- return { interrupted: false };
2603
- }
2604
-
2605
- // Calculate time since last interaction
742
+ if (pendingQuestions.length === 0) { return { interrupted: false }; }
2606
743
  const lastInteraction = new Date(conversation.last_interaction);
2607
744
  const timeSinceMs = Date.now() - lastInteraction.getTime();
2608
745
  const timeSinceMinutes = Math.floor(timeSinceMs / 60000);
2609
-
2610
- // Get last checkpoint
2611
746
  const lastCheckpoint = conversation.checkpoints.slice(-1)[0];
2612
-
2613
- // Check if we were waiting for user input
2614
747
  const lastInteractionData = conversation.interactions.slice(-1)[0];
2615
748
  const wasAwaitingResponse = lastInteractionData?.type === 'questions_presented';
2616
-
2617
- return {
2618
- interrupted: true,
2619
- session_id: activeDigest.session.id,
2620
- digest_path: activeDigest.session.digest_path,
2621
- reason: wasAwaitingResponse ? 'awaiting_response' : 'incomplete',
2622
- last_interaction: conversation.last_interaction,
2623
- time_since_minutes: timeSinceMinutes,
2624
- time_since_formatted: formatTimeSince(timeSinceMs),
2625
- checkpoint: lastCheckpoint,
2626
- pending_questions: pendingQuestions.length,
2627
- answered_questions: clarifications.questions?.filter(q => q.status === 'answered').length || 0,
2628
- total_questions: clarifications.questions?.length || 0
2629
- };
749
+ return { interrupted: true, session_id: activeDigest.session.id, digest_path: activeDigest.session.digest_path, reason: wasAwaitingResponse ? 'awaiting_response' : 'incomplete', last_interaction: conversation.last_interaction, time_since_minutes: timeSinceMinutes, time_since_formatted: formatTimeSince(timeSinceMs), checkpoint: lastCheckpoint, pending_questions: pendingQuestions.length, answered_questions: clarifications.questions?.filter(q => q.status === 'answered').length || 0, total_questions: clarifications.questions?.length || 0 };
2630
750
  }
2631
751
 
2632
- /**
2633
- * Format time since last interaction
2634
- */
2635
752
  function formatTimeSince(ms) {
2636
- const minutes = Math.floor(ms / 60000);
2637
- const hours = Math.floor(minutes / 60);
2638
- const days = Math.floor(hours / 24);
2639
-
753
+ const minutes = Math.floor(ms / 60000); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24);
2640
754
  if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
2641
755
  if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
2642
756
  if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
2643
757
  return 'just now';
2644
758
  }
2645
759
 
2646
- /**
2647
- * Generate recovery summary for interrupted session
2648
- */
2649
- function generateRecoverySummary() {
2650
- const interrupted = detectInterruptedSession();
2651
- if (!interrupted.interrupted) {
2652
- return null;
2653
- }
2654
-
2655
- const clarifications = loadClarifications();
2656
- const topics = loadTopics();
2657
- const conversation = loadConversation();
2658
-
2659
- // Get recent answered questions for context
2660
- const recentAnswers = clarifications.questions
2661
- .filter(q => q.status === 'answered')
2662
- .slice(-5)
2663
- .map(q => ({
2664
- topic: q.topic_title,
2665
- question: q.question,
2666
- answer: q.answer,
2667
- answered_at: q.answered_at
2668
- }));
2669
-
2670
- // Get pending questions
2671
- const pendingByTopic = {};
2672
- for (const q of clarifications.questions.filter(q => q.status === 'pending')) {
2673
- const topicKey = q.topic_title || q.topic_id;
2674
- if (!pendingByTopic[topicKey]) {
2675
- pendingByTopic[topicKey] = [];
2676
- }
2677
- pendingByTopic[topicKey].push(q);
2678
- }
2679
-
2680
- // Get topics status
2681
- const topicsStatus = (topics.topics || []).map(t => ({
2682
- id: t.id,
2683
- title: t.title,
2684
- pending_questions: clarifications.questions.filter(q => q.topic_id === t.id && q.status === 'pending').length,
2685
- answered_questions: clarifications.questions.filter(q => q.topic_id === t.id && q.status === 'answered').length
2686
- }));
2687
-
2688
- return {
2689
- session_id: interrupted.session_id,
2690
- started_at: conversation.started_at,
2691
- last_active: interrupted.last_interaction,
2692
- time_since: interrupted.time_since_formatted,
2693
- progress: {
2694
- answered: interrupted.answered_questions,
2695
- pending: interrupted.pending_questions,
2696
- total: interrupted.total_questions,
2697
- percentage: Math.round((interrupted.answered_questions / interrupted.total_questions) * 100)
2698
- },
2699
- recent_answers: recentAnswers,
2700
- pending_by_topic: pendingByTopic,
2701
- topics_status: topicsStatus,
2702
- checkpoint: interrupted.checkpoint
2703
- };
760
+ function generateRecoverySummary() {
761
+ const interrupted = detectInterruptedSession();
762
+ if (!interrupted.interrupted) { return null; }
763
+ const clarifications = loadClarifications();
764
+ const topics = loadTopics();
765
+ const conversation = loadConversation();
766
+ const recentAnswers = clarifications.questions.filter(q => q.status === 'answered').slice(-5).map(q => ({ topic: q.topic_title, question: q.question, answer: q.answer, answered_at: q.answered_at }));
767
+ const pendingByTopic = {};
768
+ for (const q of clarifications.questions.filter(q => q.status === 'pending')) { const topicKey = q.topic_title || q.topic_id; if (!pendingByTopic[topicKey]) { pendingByTopic[topicKey] = []; } pendingByTopic[topicKey].push(q); }
769
+ const topicsStatus = (topics.topics || []).map(t => ({ id: t.id, title: t.title, pending_questions: clarifications.questions.filter(q => q.topic_id === t.id && q.status === 'pending').length, answered_questions: clarifications.questions.filter(q => q.topic_id === t.id && q.status === 'answered').length }));
770
+ return { session_id: interrupted.session_id, started_at: conversation.started_at, last_active: interrupted.last_interaction, time_since: interrupted.time_since_formatted, progress: { answered: interrupted.answered_questions, pending: interrupted.pending_questions, total: interrupted.total_questions, percentage: Math.round((interrupted.answered_questions / interrupted.total_questions) * 100) }, recent_answers: recentAnswers, pending_by_topic: pendingByTopic, topics_status: topicsStatus, checkpoint: interrupted.checkpoint };
2704
771
  }
2705
772
 
2706
- /**
2707
- * Resume an interrupted session
2708
- */
2709
773
  function resumeSession() {
2710
774
  const interrupted = detectInterruptedSession();
2711
- if (!interrupted.interrupted) {
2712
- return { error: 'No interrupted session to resume' };
2713
- }
2714
-
2715
- // Record the resume
2716
- recordInteraction('session_resumed', {
2717
- resumed_from: interrupted.checkpoint?.id,
2718
- time_since: interrupted.time_since_formatted
2719
- });
2720
-
2721
- // Create a new checkpoint
775
+ if (!interrupted.interrupted) { return { error: 'No interrupted session to resume' }; }
776
+ recordInteraction('session_resumed', { resumed_from: interrupted.checkpoint?.id, time_since: interrupted.time_since_formatted });
2722
777
  createCheckpoint('resume');
2723
-
2724
- // Get next questions to present
2725
778
  const nextQuestions = getQuestionsForPresentation(null, 5);
2726
-
2727
- return {
2728
- resumed: true,
2729
- session_id: interrupted.session_id,
2730
- summary: generateRecoverySummary(),
2731
- next_questions: nextQuestions,
2732
- formatted_questions: formatQuestionsForUser(nextQuestions)
2733
- };
779
+ return { resumed: true, session_id: interrupted.session_id, summary: generateRecoverySummary(), next_questions: nextQuestions, formatted_questions: formatQuestionsForUser(nextQuestions) };
2734
780
  }
2735
781
 
2736
- /**
2737
- * Mark questions as presented (for tracking)
2738
- */
2739
- function markQuestionsPresented(questionIds, topic = null) {
2740
- recordInteraction('questions_presented', {
2741
- question_ids: questionIds,
2742
- topic
2743
- });
2744
-
2745
- createCheckpoint('questions_presented');
2746
- }
782
+ function markQuestionsPresented(questionIds, topic = null) { recordInteraction('questions_presented', { question_ids: questionIds, topic }); createCheckpoint('questions_presented'); }
2747
783
 
2748
- /**
2749
- * Get session history summary
2750
- */
2751
784
  function getSessionHistory() {
2752
785
  const conversation = loadConversation();
2753
- if (!conversation) {
2754
- return null;
2755
- }
2756
-
786
+ if (!conversation) { return null; }
2757
787
  const clarifications = loadClarifications();
2758
-
2759
- // Group interactions by type
2760
- const summary = {
2761
- session_id: conversation.session_id,
2762
- started_at: conversation.started_at,
2763
- last_interaction: conversation.last_interaction,
2764
- duration_ms: new Date(conversation.last_interaction) - new Date(conversation.started_at),
2765
- interaction_count: conversation.interactions.length,
2766
- checkpoint_count: conversation.checkpoints.length,
2767
- answers_given: clarifications?.questions?.filter(q => q.status === 'answered').length || 0,
2768
- interactions_by_type: {}
2769
- };
2770
-
2771
- for (const interaction of conversation.interactions) {
2772
- summary.interactions_by_type[interaction.type] = (summary.interactions_by_type[interaction.type] || 0) + 1;
2773
- }
2774
-
788
+ const summary = { session_id: conversation.session_id, started_at: conversation.started_at, last_interaction: conversation.last_interaction, duration_ms: new Date(conversation.last_interaction) - new Date(conversation.started_at), interaction_count: conversation.interactions.length, checkpoint_count: conversation.checkpoints.length, answers_given: clarifications?.questions?.filter(q => q.status === 'answered').length || 0, interactions_by_type: {} };
789
+ for (const interaction of conversation.interactions) { summary.interactions_by_type[interaction.type] = (summary.interactions_by_type[interaction.type] || 0) + 1; }
2775
790
  return summary;
2776
791
  }
2777
792
 
2778
- /**
2779
- * Export session state for backup
2780
- */
2781
793
  function exportSession(format = 'json') {
2782
794
  const activeDigest = loadActiveDigest();
2783
- if (!activeDigest.session?.digest_path) {
2784
- return { error: 'No active session' };
2785
- }
2786
-
2787
- const topics = loadTopics();
2788
- const statements = loadStatementMap();
2789
- const clarifications = loadClarifications();
2790
- const conversation = loadConversation();
2791
-
2792
- const exportData = {
2793
- exported_at: now(),
2794
- session: activeDigest.session,
2795
- phases: activeDigest.phases,
2796
- topics,
2797
- statements,
2798
- clarifications,
2799
- conversation
2800
- };
2801
-
2802
- if (format === 'json') {
2803
- return exportData;
2804
- }
2805
-
2806
- if (format === 'md') {
2807
- return formatExportAsMarkdown(exportData);
2808
- }
2809
-
795
+ if (!activeDigest.session?.digest_path) { return { error: 'No active session' }; }
796
+ const topics = loadTopics(); const statements = loadStatementMap(); const clarifications = loadClarifications(); const conversation = loadConversation();
797
+ const exportData = { exported_at: now(), session: activeDigest.session, phases: activeDigest.phases, topics, statements, clarifications, conversation };
798
+ if (format === 'json') { return exportData; }
799
+ if (format === 'md') { return formatExportAsMarkdown(exportData); }
2810
800
  return exportData;
2811
801
  }
2812
802
 
2813
- /**
2814
- * Format export as markdown
2815
- */
2816
803
  function formatExportAsMarkdown(data) {
2817
804
  let md = `# Transcript Digest Export\n\n`;
2818
- md += `**Session ID:** ${data.session.id}\n`;
2819
- md += `**Exported:** ${data.exported_at}\n\n`;
2820
-
805
+ md += `**Session ID:** ${data.session.id}\n`; md += `**Exported:** ${data.exported_at}\n\n`;
2821
806
  md += `## Progress\n\n`;
2822
807
  const answered = data.clarifications?.questions?.filter(q => q.status === 'answered').length || 0;
2823
808
  const total = data.clarifications?.questions?.length || 0;
2824
- md += `- Questions answered: ${answered}/${total}\n`;
2825
- md += `- Topics: ${data.topics?.topics?.length || 0}\n`;
2826
- md += `- Statements: ${data.statements?.statements?.length || 0}\n\n`;
2827
-
809
+ md += `- Questions answered: ${answered}/${total}\n`; md += `- Topics: ${data.topics?.topics?.length || 0}\n`; md += `- Statements: ${data.statements?.statements?.length || 0}\n\n`;
2828
810
  md += `## Topics\n\n`;
2829
- for (const topic of (data.topics?.topics || [])) {
2830
- md += `### ${topic.title}\n`;
2831
- md += `- Entities: ${(topic.entities || []).join(', ')}\n`;
2832
- md += `- Keywords: ${(topic.keywords || []).join(', ')}\n\n`;
2833
- }
2834
-
811
+ for (const topic of (data.topics?.topics || [])) { md += `### ${topic.title}\n`; md += `- Entities: ${(topic.entities || []).join(', ')}\n`; md += `- Keywords: ${(topic.keywords || []).join(', ')}\n\n`; }
2835
812
  md += `## Answered Questions\n\n`;
2836
- for (const q of (data.clarifications?.questions || []).filter(q => q.status === 'answered')) {
2837
- md += `### ${q.topic_title || 'General'}\n`;
2838
- md += `**Q:** ${q.question}\n`;
2839
- md += `**A:** ${q.answer}\n\n`;
2840
- }
2841
-
813
+ for (const q of (data.clarifications?.questions || []).filter(q => q.status === 'answered')) { md += `### ${q.topic_title || 'General'}\n`; md += `**Q:** ${q.question}\n`; md += `**A:** ${q.answer}\n\n`; }
2842
814
  md += `## Pending Questions\n\n`;
2843
- for (const q of (data.clarifications?.questions || []).filter(q => q.status === 'pending')) {
2844
- md += `- [${q.priority}] ${q.question}\n`;
2845
- }
2846
-
815
+ for (const q of (data.clarifications?.questions || []).filter(q => q.status === 'pending')) { md += `- [${q.priority}] ${q.question}\n`; }
2847
816
  return md;
2848
817
  }
2849
818
 
2850
- /**
2851
- * Review all answered questions
2852
- */
2853
819
  function reviewAnswers() {
2854
820
  const clarifications = loadClarifications();
2855
- if (!clarifications) {
2856
- return { error: 'No clarifications found' };
2857
- }
2858
-
821
+ if (!clarifications) { return { error: 'No clarifications found' }; }
2859
822
  const answered = clarifications.questions.filter(q => q.status === 'answered');
2860
-
2861
- // Group by topic
2862
823
  const byTopic = {};
2863
- for (const q of answered) {
2864
- const topicKey = q.topic_title || q.topic_id || 'General';
2865
- if (!byTopic[topicKey]) {
2866
- byTopic[topicKey] = [];
2867
- }
2868
- byTopic[topicKey].push({
2869
- id: q.id,
2870
- question: q.question,
2871
- answer: q.answer,
2872
- answered_at: q.answered_at,
2873
- source: q.answer_source
2874
- });
2875
- }
2876
-
2877
- return {
2878
- total_answered: answered.length,
2879
- by_topic: byTopic
2880
- };
2881
- }
2882
-
2883
- /**
2884
- * Save topics to digest
2885
- */
2886
- function saveTopics(topics) {
2887
- const activeDigest = loadActiveDigest();
2888
- if (!activeDigest.session.digest_path) {
2889
- throw new Error('No active digest session');
2890
- }
2891
-
2892
- const topicsPath = path.join(activeDigest.session.digest_path, 'topics.json');
2893
-
2894
- // Ensure proper structure
2895
- const topicsData = {
2896
- topics: topics.topics || topics,
2897
- metadata: {
2898
- total_topics: (topics.topics || topics).length,
2899
- active_topics: (topics.topics || topics).filter(t => t.status === 'active').length,
2900
- clarified_topics: (topics.topics || topics).filter(t => t.clarification_complete).length,
2901
- generated_topics: (topics.topics || topics).filter(t => t.stories_generated).length,
2902
- detected_at: now(),
2903
- last_updated: now(),
2904
- transcript_word_count: activeDigest.input?.word_count || 0,
2905
- detection_method: 'pass-1-extraction'
2906
- }
2907
- };
2908
-
2909
- fs.writeFileSync(topicsPath, JSON.stringify(topicsData, null, 2));
2910
-
2911
- // Update phase
2912
- updatePhase('topic_extraction', 'completed', { topics_found: topicsData.topics.length });
2913
-
2914
- return topicsData;
2915
- }
2916
-
2917
- /**
2918
- * Load topics from digest
2919
- */
2920
- function loadTopics() {
2921
- const activeDigest = loadActiveDigest();
2922
- if (!activeDigest.session.digest_path) {
2923
- return null;
2924
- }
2925
-
2926
- const topicsPath = path.join(activeDigest.session.digest_path, 'topics.json');
2927
- try {
2928
- return JSON.parse(fs.readFileSync(topicsPath, 'utf8'));
2929
- } catch (_err) {
2930
- return null;
2931
- }
2932
- }
2933
-
2934
- /**
2935
- * Get digest status
2936
- */
2937
- function getStatus() {
2938
- const activeDigest = loadActiveDigest();
2939
-
2940
- if (activeDigest.session.status === 'inactive') {
2941
- return { active: false };
2942
- }
2943
-
2944
- return {
2945
- active: true,
2946
- id: activeDigest.session.id,
2947
- phase: activeDigest.session.phase,
2948
- phases: activeDigest.phases,
2949
- input: activeDigest.input
2950
- };
2951
- }
2952
-
2953
- /**
2954
- * Check if input should trigger digestion
2955
- */
2956
- function shouldTriggerDigestion(text) {
2957
- const config = loadConfig();
2958
- const threshold = config.autoTriggerThreshold || 2000;
2959
- const wordCount = countWords(text);
2960
-
2961
- if (wordCount < threshold) {
2962
- return { trigger: false, reason: 'below_threshold', wordCount };
2963
- }
2964
-
2965
- // Check content type
2966
- const contentType = classifyContent(text);
2967
-
2968
- if (contentType.type === 'requirements' || contentType.type === 'transcript') {
2969
- return { trigger: true, reason: 'auto', wordCount, contentType };
2970
- }
2971
-
2972
- if (contentType.type === 'code') {
2973
- return { trigger: false, reason: 'code_detected', wordCount, contentType };
2974
- }
2975
-
2976
- // Ambiguous - suggest asking
2977
- return { trigger: 'ask', reason: 'ambiguous', wordCount, contentType };
2978
- }
2979
-
2980
- /**
2981
- * Basic content classification
2982
- */
2983
- function classifyContent(text) {
2984
- // Check for code patterns
2985
- const codePatterns = [
2986
- /```[\s\S]*```/g,
2987
- /function\s+\w+\s*\(/g,
2988
- /const\s+\w+\s*=/g,
2989
- /import\s+.*from/g,
2990
- /class\s+\w+/g
2991
- ];
2992
-
2993
- let codeMatches = 0;
2994
- for (const pattern of codePatterns) {
2995
- const matches = text.match(pattern);
2996
- if (matches) codeMatches += matches.length;
2997
- }
2998
-
2999
- if (codeMatches > 10) {
3000
- return { type: 'code', confidence: 0.8 };
3001
- }
3002
-
3003
- // Check for requirements patterns
3004
- const reqPatterns = [
3005
- /we need/gi,
3006
- /should have/gi,
3007
- /must support/gi,
3008
- /add a feature/gi,
3009
- /implement/gi,
3010
- /the \w+ should/gi
3011
- ];
3012
-
3013
- let reqMatches = 0;
3014
- for (const pattern of reqPatterns) {
3015
- const matches = text.match(pattern);
3016
- if (matches) reqMatches += matches.length;
3017
- }
3018
-
3019
- if (reqMatches > 5) {
3020
- return { type: 'requirements', confidence: 0.85 };
3021
- }
3022
-
3023
- // Check for transcript patterns
3024
- const transcriptPatterns = [
3025
- /^\d{2}:\d{2}/gm, // Timestamps
3026
- /^speaker \d+:/gim,
3027
- /^\[.*\]:/gm,
3028
- /^[A-Z][a-z]+:/gm // Speaker names
3029
- ];
3030
-
3031
- let transcriptMatches = 0;
3032
- for (const pattern of transcriptPatterns) {
3033
- const matches = text.match(pattern);
3034
- if (matches) transcriptMatches += matches.length;
3035
- }
3036
-
3037
- if (transcriptMatches > 10) {
3038
- return { type: 'transcript', confidence: 0.9 };
3039
- }
3040
-
3041
- return { type: 'unknown', confidence: 0.5 };
824
+ for (const q of answered) { const topicKey = q.topic_title || q.topic_id || 'General'; if (!byTopic[topicKey]) { byTopic[topicKey] = []; } byTopic[topicKey].push({ id: q.id, question: q.question, answer: q.answer, answered_at: q.answered_at, source: q.answer_source }); }
825
+ return { total_answered: answered.length, by_topic: byTopic };
3042
826
  }
3043
827
 
3044
- // E4-S1 (Large Input Detection), E4-S2 (Content Type Classification),
3045
- // and E3-S1 (Complexity Detection) functions extracted to:
3046
- // flow-long-input-detection.js, flow-long-input-complexity.js
828
+ // ============================================
829
+ // Complexity Analysis (stays here orchestrates sub-modules)
830
+ // ============================================
3047
831
 
3048
- /**
3049
- * Main complexity analysis function
3050
- */
3051
832
  function analyzeComplexity() {
3052
833
  const topics = loadTopics();
3053
834
  const statementMap = loadStatementMap();
3054
835
  const clarifications = loadClarifications();
3055
-
3056
- if (!topics || !topics.topics) {
3057
- return { error: 'No topics found. Run Pass 1 first.' };
3058
- }
3059
-
836
+ if (!topics || !topics.topics) { return { error: 'No topics found. Run Pass 1 first.' }; }
3060
837
  const statements = statementMap?.statements || [];
3061
-
3062
- // Build digest object
3063
- const digest = {
3064
- topics: topics.topics,
3065
- statements,
3066
- clarifications: clarifications || { questions: [], contradictions: [] }
3067
- };
3068
-
3069
- // Calculate overall complexity
838
+ const digest = { topics: topics.topics, statements, clarifications: clarifications || { questions: [], contradictions: [] } };
3070
839
  const overallScore = calculateComplexityScore(digest);
3071
840
  const level = getComplexityLevel(overallScore);
3072
-
3073
- // Analyze each topic
3074
- const topicAnalysis = topics.topics
3075
- .filter(t => t.status === 'active')
3076
- .map(t => analyzeTopicComplexity(t, statements, clarifications));
3077
-
3078
- // Get output recommendation
841
+ const topicAnalysis = topics.topics.filter(t => t.status === 'active').map(t => analyzeTopicComplexity(t, statements, clarifications));
3079
842
  const recommendation = recommendOutputStructure(overallScore, topicAnalysis);
3080
-
3081
- // Extract entity summary
3082
843
  const entitySummary = extractEntities(statements);
3083
-
3084
- // Build result
3085
- const result = {
3086
- overall: {
3087
- score: overallScore,
3088
- level: level.level,
3089
- description: level.description,
3090
- confidence: 0.85
3091
- },
3092
- factors: {
3093
- topic_count: topics.topics.filter(t => t.status === 'active').length,
3094
- statement_count: statements.filter(s => s.meaningful !== false).length,
3095
- question_count: clarifications?.questions?.length || 0,
3096
- contradiction_count: clarifications?.contradictions?.length || 0,
3097
- entity_types: countEntityTypes(statements),
3098
- ui_components: entitySummary.ui_components.length,
3099
- data_entities: entitySummary.data_entities.length,
3100
- interactions: entitySummary.interactions.length
3101
- },
3102
- topic_analysis: topicAnalysis,
3103
- recommendation,
3104
- entity_summary: entitySummary
844
+ return {
845
+ overall: { score: overallScore, level: level.level, description: level.description, confidence: 0.85 },
846
+ factors: { topic_count: topics.topics.filter(t => t.status === 'active').length, statement_count: statements.filter(s => s.meaningful !== false).length, question_count: clarifications?.questions?.length || 0, contradiction_count: clarifications?.contradictions?.length || 0, entity_types: countEntityTypes(statements), ui_components: entitySummary.ui_components.length, data_entities: entitySummary.data_entities.length, interactions: entitySummary.interactions.length },
847
+ topic_analysis: topicAnalysis, recommendation, entity_summary: entitySummary
3105
848
  };
3106
-
3107
- return result;
3108
849
  }
3109
850
 
3110
- // Initialize story module with core functions
3111
- transcriptStories.init({
851
+ // ============================================
852
+ // Module initialization (wire dependencies)
853
+ // ============================================
854
+
855
+ longInputAssociation.init({
3112
856
  loadActiveDigest,
3113
- saveActiveDigest,
3114
- loadTopics,
3115
- saveTopics,
3116
- loadStatementMap,
3117
- loadClarifications,
3118
- isRequirement,
3119
- isVagueStatement,
3120
- analyzeComplexity,
3121
- REQUIREMENT_PATTERNS,
3122
- VAGUE_PATTERNS,
3123
- ENTITY_PATTERNS
857
+ updatePhase
3124
858
  });
3125
859
 
3126
- // Initialize chunking module with core functions
3127
- transcriptChunking.init({
860
+ longInputContradictions.init({
3128
861
  loadActiveDigest,
3129
- saveActiveDigest,
3130
- countWords,
3131
- now
862
+ calculateAssociationConfidence
3132
863
  });
3133
864
 
3134
- // Initialize detection module with functions from main module
3135
- longInputDetection.init({
3136
- countWords,
3137
- classifyContent
865
+ longInputPasses.init({
866
+ loadActiveDigest, updatePhase, now, countWords,
867
+ loadTopics, saveTopics,
868
+ splitIntoStatements, isMeaningfulStatement, associateStatements,
869
+ saveStatementMap, loadStatementMap,
870
+ detectContradictions, resolveOrphan,
871
+ calculateResolutionConfidence, generateContradictionQuestion,
872
+ isAdditive, detectCorrectionPhrase,
873
+ saveClarifications, loadClarifications,
874
+ createSession
3138
875
  });
3139
876
 
3140
- // ==========================================================================
3141
- // Quick Processing Mode
3142
- // ==========================================================================
3143
-
3144
- /**
3145
- * Quick process mode - single-pass extraction without interactive clarification.
3146
- * Used by the long input gate for fast feedback.
3147
- *
3148
- * @param {string} input - The input text to process
3149
- * @param {Object} options - Processing options
3150
- * @returns {Object} Quick scan results
3151
- */
3152
- function quickProcess(input, _options = {}) {
3153
- if (!input || typeof input !== 'string') {
3154
- return { error: 'No input provided' };
3155
- }
3156
-
3157
- const startTime = Date.now();
3158
-
3159
- // 1. Split into statements (returns objects with .text property)
3160
- const statements = splitIntoStatements(input);
3161
- // isMeaningfulStatement returns {meaningful: bool, reason: string}, filter on .meaningful
3162
- const meaningfulStatements = statements.filter(s => isMeaningfulStatement(s.text).meaningful);
3163
-
3164
- // 2. Quick topic extraction (keyword-based, no full analysis)
3165
- const topicKeywords = new Set();
3166
- const topicPatterns = [
3167
- /\b(add|create|build|implement)\s+(?:a\s+)?(\w+(?:\s+\w+)?)/gi,
3168
- /\b(\w+)\s+(feature|component|page|button|form|table|list)/gi,
3169
- /\b(user|admin|guest)\s+(?:can|should|must|wants?)\s+(\w+)/gi
3170
- ];
3171
-
3172
- for (const statement of meaningfulStatements) {
3173
- const text = statement.text;
3174
- for (const pattern of topicPatterns) {
3175
- const matches = text.matchAll(pattern);
3176
- for (const match of matches) {
3177
- const keyword = (match[2] || match[1]).toLowerCase();
3178
- if (keyword.length > 2) {
3179
- topicKeywords.add(keyword);
3180
- }
3181
- }
3182
- }
3183
- }
3184
-
3185
- // 3. Quick contradiction detection
3186
- const contradictions = [];
3187
- const seenValues = new Map(); // attribute -> { value, text }
3188
-
3189
- const valuePatterns = [
3190
- { pattern: /(\d+)\s*(columns?|rows?|items?|pages?)/gi, attr: 'count' },
3191
- { pattern: /(primary|secondary|danger|success)\s*(?:color|button)/gi, attr: 'style' },
3192
- { pattern: /(left|right|center|top|bottom)/gi, attr: 'position' }
3193
- ];
3194
-
3195
- for (const statement of meaningfulStatements) {
3196
- const text = statement.text;
3197
- for (const { pattern, attr } of valuePatterns) {
3198
- const match = text.match(pattern);
3199
- if (match && match[1]) {
3200
- const value = match[1].toLowerCase();
3201
- const key = `${attr}`;
3202
-
3203
- if (seenValues.has(key) && seenValues.get(key).value !== value) {
3204
- // Check for correction phrase
3205
- const isCorrection = detectCorrectionPhrase(text);
3206
-
3207
- contradictions.push({
3208
- attribute: attr,
3209
- value1: seenValues.get(key).value,
3210
- value2: value,
3211
- statement1: seenValues.get(key).text.slice(0, 50),
3212
- statement2: text.slice(0, 50),
3213
- autoResolved: isCorrection,
3214
- resolution: isCorrection ? `Later statement (${value}) supersedes` : 'needs_review'
3215
- });
3216
- }
3217
-
3218
- seenValues.set(key, { value, text });
3219
- }
3220
- }
3221
- }
3222
-
3223
- const elapsed = Date.now() - startTime;
3224
-
3225
- return {
3226
- mode: 'quick',
3227
- success: true,
3228
- metrics: {
3229
- totalStatements: statements.length,
3230
- meaningfulStatements: meaningfulStatements.length,
3231
- topicsDetected: topicKeywords.size,
3232
- contradictionsFound: contradictions.length,
3233
- autoResolved: contradictions.filter(c => c.autoResolved).length,
3234
- processingTimeMs: elapsed
3235
- },
3236
- topics: Array.from(topicKeywords),
3237
- contradictions: contradictions.filter(c => !c.autoResolved),
3238
- summary: generateQuickSummary(meaningfulStatements.length, topicKeywords.size, contradictions)
3239
- };
3240
- }
3241
-
3242
- /**
3243
- * Generate human-readable summary for quick scan
3244
- */
3245
- function generateQuickSummary(statementCount, topicCount, contradictions) {
3246
- const unresolvedCount = contradictions.filter(c => !c.autoResolved).length;
3247
-
3248
- let summary = `Quick scan complete: ${statementCount} statements, ${topicCount} topics detected.`;
3249
-
3250
- if (contradictions.length > 0) {
3251
- const autoResolved = contradictions.filter(c => c.autoResolved).length;
3252
- summary += `\n${contradictions.length} potential contradictions found`;
3253
- if (autoResolved > 0) {
3254
- summary += ` (${autoResolved} auto-resolved as corrections)`;
3255
- }
3256
- if (unresolvedCount > 0) {
3257
- summary += `.\n${unresolvedCount} need review.`;
3258
- }
3259
- } else {
3260
- summary += '\nNo obvious contradictions detected.';
3261
- }
3262
-
3263
- return summary;
3264
- }
3265
-
3266
- // =============================================================================
3267
- // UNIFIED PIPELINE (runs all passes in sequence)
3268
- // =============================================================================
3269
-
3270
- /**
3271
- * Run the full 4-pass pipeline in one call.
3272
- *
3273
- * Chains: createSession → runPass2 → runPass3 → runPass4
3274
- *
3275
- * Pass 1 (topic extraction) is handled by the AI via the command spec —
3276
- * the AI reads the confirmed statements and generates topics.json before
3277
- * calling this function. This function handles Passes 2-4.
3278
- *
3279
- * @param {Object} options
3280
- * @param {string} options.transcript - The full transcript text
3281
- * @param {Object[]} options.topics - Topics array (from AI or extractTopics)
3282
- * @param {string} options.contentType - Content type classification
3283
- * @returns {Object} Consolidated pipeline result
3284
- */
3285
- function runFullPipeline(options = {}) {
3286
- const { transcript, topics, contentType } = options;
3287
-
3288
- if (!transcript) throw new Error('transcript is required');
3289
- if (!topics || !topics.length) throw new Error('topics array is required');
3290
-
3291
- // Step 1: Create session with transcript
3292
- const session = createSession(transcript, { contentType: contentType || 'transcript' });
3293
- const digestId = session.id || session.session?.id;
3294
-
3295
- // Step 2: Save the topics (normally done by AI in Pass 1)
3296
- const topicsData = {
3297
- topics: topics.map((t, i) => ({
3298
- id: t.id || `topic-${String(i + 1).padStart(3, '0')}`,
3299
- title: t.title,
3300
- keywords: t.keywords || [],
3301
- description: t.description || '',
3302
- priority: t.priority || 'medium',
3303
- statement_count: 0,
3304
- status: 'active'
3305
- })),
3306
- metadata: {
3307
- total_topics: topics.length,
3308
- active_topics: topics.length,
3309
- clarified_topics: 0,
3310
- generated_topics: 0,
3311
- detected_at: now(),
3312
- last_updated: now(),
3313
- transcript_word_count: countWords(transcript),
3314
- detection_method: 'unified-pipeline'
3315
- }
3316
- };
3317
- saveTopics(topicsData);
3318
- updatePhase('topic_extraction', 'completed', { topics_found: topics.length });
3319
-
3320
- // Step 3: Run Pass 2 — statement mapping + contradiction detection
3321
- let pass2Result;
3322
- try {
3323
- pass2Result = runPass2();
3324
- } catch (err) {
3325
- return { error: `Pass 2 failed: ${err.message}`, phase: 'statement_mapping' };
3326
- }
3327
-
3328
- // Step 4: Run Pass 3 — orphan check and resolution
3329
- let pass3Result;
3330
- try {
3331
- pass3Result = runPass3();
3332
- } catch (err) {
3333
- return { error: `Pass 3 failed: ${err.message}`, phase: 'orphan_check' };
3334
- }
3335
-
3336
- // Step 5: Run Pass 4 — contradiction resolution
3337
- let pass4Result;
3338
- try {
3339
- pass4Result = runPass4();
3340
- } catch (err) {
3341
- return { error: `Pass 4 failed: ${err.message}`, phase: 'contradiction_resolution' };
3342
- }
877
+ // Initialize story module with core functions
878
+ transcriptStories.init({
879
+ loadActiveDigest, saveActiveDigest, loadTopics, saveTopics,
880
+ loadStatementMap, loadClarifications,
881
+ isRequirement, isVagueStatement, analyzeComplexity,
882
+ REQUIREMENT_PATTERNS, VAGUE_PATTERNS, ENTITY_PATTERNS
883
+ });
3343
884
 
3344
- // Step 6: Collect clarification questions (from contradictions + orphans)
3345
- const clarifications = loadClarifications();
3346
- const pendingContradictions = (clarifications?.contradictions || [])
3347
- .filter(c => c.status === 'pending');
3348
-
3349
- const orphanQuestions = (pass3Result?.orphans || [])
3350
- .filter(o => o.needs_clarification)
3351
- .map(o => ({
3352
- type: 'orphan',
3353
- statement_id: o.id,
3354
- question: o.clarification_question,
3355
- text: o.text
3356
- }));
3357
-
3358
- const allClarificationQuestions = [
3359
- ...pendingContradictions.map(c => ({
3360
- type: 'contradiction',
3361
- id: c.id,
3362
- question: c.question,
3363
- options: c.options
3364
- })),
3365
- ...orphanQuestions
3366
- ];
3367
-
3368
- // Step 7: Load final state for summary
3369
- const stmtMap = loadStatementMap();
3370
- const finalTopics = loadTopics();
885
+ // Initialize chunking module with core functions
886
+ transcriptChunking.init({ loadActiveDigest, saveActiveDigest, countWords, now });
3371
887
 
3372
- return {
3373
- success: true,
3374
- digest_id: digestId,
3375
- summary: {
3376
- topics_count: finalTopics?.topics?.length || 0,
3377
- statements_total: stmtMap?.metadata?.total_statements || 0,
3378
- statements_meaningful: stmtMap?.metadata?.meaningful_statements || 0,
3379
- statements_mapped: stmtMap?.metadata?.mapped_statements || 0,
3380
- orphans_found: pass3Result?.orphans?.length || 0,
3381
- orphans_resolved: pass3Result?.resolved?.length || 0,
3382
- new_topics_created: pass3Result?.new_topics_created?.length || 0,
3383
- contradictions_total: pass4Result?.stats?.total || 0,
3384
- contradictions_auto_resolved: pass4Result?.stats?.auto_resolved || 0,
3385
- contradictions_needs_clarification: pass4Result?.stats?.needs_clarification || 0,
3386
- additive_not_contradiction: pass4Result?.stats?.additive_not_contradiction || 0,
3387
- coverage_percentage: pass3Result?.coverage?.percentage || 0
3388
- },
3389
- clarification_questions: allClarificationQuestions,
3390
- topics: finalTopics?.topics || [],
3391
- pass2: pass2Result,
3392
- pass3: pass3Result,
3393
- pass4: pass4Result
3394
- };
3395
- }
888
+ // Initialize detection module with functions from main module
889
+ longInputDetection.init({ countWords, classifyContent });
3396
890
 
3397
891
  // Export for use as module
3398
892
  module.exports = {
3399
893
  // Utilities
3400
894
  now,
3401
895
  // Core session management
3402
- createSession,
3403
- loadActiveDigest,
3404
- saveActiveDigest,
3405
- updatePhase,
3406
- saveTopics,
3407
- loadTopics,
3408
- getStatus,
3409
- shouldTriggerDigestion,
3410
- classifyContent,
3411
- countWords,
3412
- // Pass 2: Statement Association
3413
- isMeaningfulStatement,
3414
- splitIntoStatements,
3415
- associateStatements,
3416
- detectContradictions,
3417
- saveStatementMap,
3418
- loadStatementMap,
3419
- runPass2,
3420
- // Pass 3: Orphan Check
3421
- resolveOrphan,
3422
- createTopicFromOrphans,
3423
- ensureGeneralTopic,
3424
- saveOrphans,
3425
- loadOrphans,
3426
- runPass3,
3427
- // Pass 4: Contradiction Resolution
3428
- detectCorrectionPhrase,
3429
- isAdditive,
3430
- calculateResolutionConfidence,
3431
- generateContradictionQuestion,
3432
- saveClarifications,
3433
- loadClarifications,
3434
- runPass4,
896
+ createSession, loadActiveDigest, saveActiveDigest, updatePhase,
897
+ saveTopics, loadTopics, getStatus, shouldTriggerDigestion, classifyContent, countWords,
898
+ // Pass 2: Statement Association (from flow-long-input-association.js)
899
+ isMeaningfulStatement, splitIntoStatements, associateStatements,
900
+ detectContradictions, saveStatementMap, loadStatementMap, runPass2,
901
+ // Pass 3: Orphan Check (from flow-long-input-passes.js)
902
+ resolveOrphan, createTopicFromOrphans, ensureGeneralTopic,
903
+ saveOrphans, loadOrphans, runPass3,
904
+ // Pass 4: Contradiction Resolution (from flow-long-input-contradictions.js)
905
+ detectCorrectionPhrase, isAdditive, calculateResolutionConfidence,
906
+ generateContradictionQuestion, saveClarifications, loadClarifications, runPass4,
3435
907
  // Question Generation (E2-S1)
3436
- analyzeCompleteness,
3437
- detectVagueness,
3438
- generateQuestionsForTopic,
3439
- generateAllQuestions,
908
+ analyzeCompleteness, detectVagueness, generateQuestionsForTopic, generateAllQuestions,
3440
909
  // Conversation Loop (E2-S2)
3441
- parseAnswers,
3442
- captureAnswer,
3443
- createDerivedStatement,
3444
- checkFollowups,
3445
- addFollowupQuestions,
3446
- checkCompletion,
3447
- getQuestionsForPresentation,
3448
- formatQuestionsForUser,
3449
- processConversationResponse,
3450
- resolveContradictionWithChoice,
910
+ parseAnswers, captureAnswer, createDerivedStatement, checkFollowups,
911
+ addFollowupQuestions, checkCompletion, getQuestionsForPresentation,
912
+ formatQuestionsForUser, processConversationResponse, resolveContradictionWithChoice,
3451
913
  // Voice Answer Integration (E2-S3)
3452
- isVoiceInput,
3453
- removeFillers,
3454
- applySelfCorrections,
3455
- normalizeNumbers,
3456
- detectUncertainty,
3457
- detectYesNo,
3458
- addPunctuation,
3459
- normalizeVoiceInput,
3460
- calculateVoiceConfidence,
3461
- processVoiceAnswer,
914
+ isVoiceInput, removeFillers, applySelfCorrections, normalizeNumbers,
915
+ detectUncertainty, detectYesNo, addPunctuation, normalizeVoiceInput,
916
+ calculateVoiceConfidence, processVoiceAnswer,
3462
917
  // State Persistence (E2-S4)
3463
- loadConversation,
3464
- saveConversation,
3465
- initializeConversation,
3466
- recordInteraction,
3467
- createCheckpoint,
3468
- detectInterruptedSession,
3469
- generateRecoverySummary,
3470
- resumeSession,
3471
- markQuestionsPresented,
3472
- getSessionHistory,
3473
- exportSession,
3474
- reviewAnswers,
918
+ loadConversation, saveConversation, initializeConversation, recordInteraction,
919
+ createCheckpoint, detectInterruptedSession, generateRecoverySummary, resumeSession,
920
+ markQuestionsPresented, getSessionHistory, exportSession, reviewAnswers,
3475
921
  // Complexity Detection (E3-S1)
3476
- countEntityTypes,
3477
- extractEntities,
3478
- getComplexityLevel,
3479
- calculateComplexityScore,
3480
- isRequirement,
3481
- isVagueStatement,
3482
- hasUIComponent,
3483
- hasDataModel,
3484
- hasUserInteraction,
3485
- analyzeTopicComplexity,
3486
- groupRelatedTopics,
3487
- generateEpicStructure,
3488
- recommendOutputStructure,
922
+ countEntityTypes, extractEntities, getComplexityLevel, calculateComplexityScore,
923
+ isRequirement, isVagueStatement, hasUIComponent, hasDataModel, hasUserInteraction,
924
+ analyzeTopicComplexity, groupRelatedTopics, generateEpicStructure, recommendOutputStructure,
3489
925
  analyzeComplexity,
3490
- // Story Generation (E3-S2) - re-exported from flow-transcript-stories.js
926
+ // Story Generation (E3-S2) - re-exported from flow-long-input-stories.js
3491
927
  USER_TYPE_PATTERNS: transcriptStories.USER_TYPE_PATTERNS,
3492
928
  SCENARIO_PATTERNS: transcriptStories.SCENARIO_PATTERNS,
3493
929
  generateStoryId: transcriptStories.generateStoryId,
@@ -3509,7 +945,7 @@ module.exports = {
3509
945
  loadStory: transcriptStories.loadStory,
3510
946
  loadAllStories: transcriptStories.loadAllStories,
3511
947
  formatStoryAsMarkdown: transcriptStories.formatStoryAsMarkdown,
3512
- // Presentation Flow (E3-S3) - re-exported from flow-transcript-stories.js
948
+ // Presentation Flow (E3-S3)
3513
949
  loadQueue: transcriptStories.loadQueue,
3514
950
  saveQueue: transcriptStories.saveQueue,
3515
951
  initializePresentation: transcriptStories.initializePresentation,
@@ -3523,7 +959,7 @@ module.exports = {
3523
959
  formatActionsPrompt: transcriptStories.formatActionsPrompt,
3524
960
  getCompletionSummary: transcriptStories.getCompletionSummary,
3525
961
  resetPresentation: transcriptStories.resetPresentation,
3526
- // Edit and Change Handling (E3-S4) - re-exported from flow-transcript-stories.js
962
+ // Edit and Change Handling (E3-S4)
3527
963
  generateEditSessionId: transcriptStories.generateEditSessionId,
3528
964
  generateChangeId: transcriptStories.generateChangeId,
3529
965
  loadEditSessions: transcriptStories.loadEditSessions,
@@ -3543,7 +979,7 @@ module.exports = {
3543
979
  getEditChanges: transcriptStories.getEditChanges,
3544
980
  getEditHistory: transcriptStories.getEditHistory,
3545
981
  listEditableStories: transcriptStories.listEditableStories,
3546
- // ready.json Integration (E3-S5) - re-exported from flow-transcript-stories.js
982
+ // ready.json Integration (E3-S5)
3547
983
  generateWorkflowId: transcriptStories.generateWorkflowId,
3548
984
  generateSubTaskId: transcriptStories.generateSubTaskId,
3549
985
  mapPriority: transcriptStories.mapPriority,
@@ -3559,23 +995,13 @@ module.exports = {
3559
995
  finalizeDigestion: transcriptStories.finalizeDigestion,
3560
996
  generateAndExportStories: transcriptStories.generateAndExportStories,
3561
997
  // Large Input Detection (E4-S1)
3562
- measureInputMetrics,
3563
- estimateTokens,
3564
- isVTTFormat,
3565
- isSRTFormat,
3566
- detectMeetingFormat,
3567
- detectInputFormat,
3568
- analyzeInput,
3569
- evaluateTrigger,
3570
- generateRecommendationMessage,
3571
- detectLargeInput,
998
+ measureInputMetrics, estimateTokens, isVTTFormat, isSRTFormat,
999
+ detectMeetingFormat, detectInputFormat, analyzeInput, evaluateTrigger,
1000
+ generateRecommendationMessage, detectLargeInput,
3572
1001
  // Content Type Classification (E4-S2)
3573
- scoreContentType,
3574
- normalizeScore,
3575
- classifyContentTypes,
3576
- getDetailedClassification,
3577
- shouldExcludeContent,
3578
- // VTT/SRT Format Parsing (E4-S3) - re-exported from flow-transcript-parsing.js
1002
+ scoreContentType, normalizeScore, classifyContentTypes,
1003
+ getDetailedClassification, shouldExcludeContent,
1004
+ // VTT/SRT Format Parsing (E4-S3)
3579
1005
  timestampToMs: transcriptParsing.timestampToMs,
3580
1006
  msToTimestamp: transcriptParsing.msToTimestamp,
3581
1007
  cleanSubtitleText: transcriptParsing.cleanSubtitleText,
@@ -3587,7 +1013,7 @@ module.exports = {
3587
1013
  parseSubtitle: transcriptParsing.parseSubtitle,
3588
1014
  formatCuesAsText: transcriptParsing.formatCuesAsText,
3589
1015
  getSubtitleStats: transcriptParsing.getSubtitleStats,
3590
- // Zoom/Teams Parsing (E4-S4) - re-exported from flow-transcript-parsing.js
1016
+ // Zoom/Teams Parsing (E4-S4)
3591
1017
  ZOOM_PATTERNS: transcriptParsing.ZOOM_PATTERNS,
3592
1018
  TEAMS_PATTERNS: transcriptParsing.TEAMS_PATTERNS,
3593
1019
  isSystemMessage: transcriptParsing.isSystemMessage,
@@ -3604,7 +1030,7 @@ module.exports = {
3604
1030
  mergeMeetingEntries: transcriptParsing.mergeMeetingEntries,
3605
1031
  formatMeetingAsText: transcriptParsing.formatMeetingAsText,
3606
1032
  getMeetingStats: transcriptParsing.getMeetingStats,
3607
- // Language Detection (E5-S1) - re-exported from flow-transcript-language.js
1033
+ // Language Detection (E5-S1)
3608
1034
  SCRIPT_PATTERNS: transcriptLanguage.SCRIPT_PATTERNS,
3609
1035
  LANGUAGE_INFO: transcriptLanguage.LANGUAGE_INFO,
3610
1036
  COMMON_WORDS: transcriptLanguage.COMMON_WORDS,
@@ -3622,14 +1048,9 @@ module.exports = {
3622
1048
  listSupportedLanguages: transcriptLanguage.listSupportedLanguages,
3623
1049
  // Multi-language Clarification (E5-S2)
3624
1050
  QUESTION_TEMPLATES_BY_LANGUAGE,
3625
- getQuestionTemplates,
3626
- generateLocalizedQuestion,
3627
- detectSessionLanguage,
3628
- getTopicLanguage,
3629
- setLanguagePreference,
3630
- getEffectiveLanguage,
3631
- getSessionLanguageInfo,
3632
- // Durable Session Persistence (E5-S3) - re-exported from flow-transcript-chunking.js
1051
+ getQuestionTemplates, generateLocalizedQuestion, detectSessionLanguage,
1052
+ getTopicLanguage, setLanguagePreference, getEffectiveLanguage, getSessionLanguageInfo,
1053
+ // Durable Session Persistence (E5-S3)
3633
1054
  DURABLE_DIGEST_PATH: transcriptChunking.DURABLE_DIGEST_PATH,
3634
1055
  DURABLE_DIGEST_VERSION: transcriptChunking.DURABLE_DIGEST_VERSION,
3635
1056
  loadDurableSessions: transcriptChunking.loadDurableSessions,
@@ -3649,7 +1070,7 @@ module.exports = {
3649
1070
  archiveDurableSession: transcriptChunking.archiveDurableSession,
3650
1071
  deleteDurableSession: transcriptChunking.deleteDurableSession,
3651
1072
  completeDurableSession: transcriptChunking.completeDurableSession,
3652
- // Large Transcript Chunking (E5-S4) - re-exported from flow-transcript-chunking.js
1073
+ // Large Transcript Chunking (E5-S4)
3653
1074
  CHUNKING_DEFAULTS: transcriptChunking.CHUNKING_DEFAULTS,
3654
1075
  SPEAKER_BOUNDARY_PATTERNS: transcriptChunking.SPEAKER_BOUNDARY_PATTERNS,
3655
1076
  needsChunking: transcriptChunking.needsChunking,
@@ -3668,8 +1089,7 @@ module.exports = {
3668
1089
  getChunkContent: transcriptChunking.getChunkContent,
3669
1090
  getChunkingStatus: transcriptChunking.getChunkingStatus,
3670
1091
  // Quick Processing Mode (for gate integration)
3671
- quickProcess,
3672
- generateQuickSummary,
1092
+ quickProcess, generateQuickSummary,
3673
1093
  // Unified Pipeline (runs all passes in sequence)
3674
1094
  runFullPipeline
3675
1095
  };