wogiflow 2.4.1 → 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 (198) 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/commit-log-gate.js +21 -10
  167. package/scripts/hooks/core/config-change.js +1 -0
  168. package/scripts/hooks/core/extension-registry.js +0 -2
  169. package/scripts/hooks/core/instructions-loaded.js +1 -1
  170. package/scripts/hooks/core/observation-capture.js +5 -5
  171. package/scripts/hooks/core/phase-gate.js +5 -0
  172. package/scripts/hooks/core/post-compact.js +1 -12
  173. package/scripts/hooks/core/research-gate.js +2 -12
  174. package/scripts/hooks/core/routing-gate.js +6 -0
  175. package/scripts/hooks/core/task-completed.js +12 -0
  176. package/scripts/hooks/core/task-gate.js +27 -0
  177. package/scripts/hooks/core/worktree-lifecycle.js +1 -1
  178. package/scripts/hooks/entry/claude-code/config-change.js +6 -29
  179. package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
  180. package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
  181. package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
  182. package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
  183. package/scripts/hooks/entry/claude-code/session-end.js +4 -28
  184. package/scripts/hooks/entry/claude-code/session-start.js +205 -243
  185. package/scripts/hooks/entry/claude-code/setup.js +8 -49
  186. package/scripts/hooks/entry/claude-code/stop.js +40 -72
  187. package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
  188. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
  189. package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
  190. package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
  191. package/scripts/hooks/entry/shared/hook-runner.js +99 -0
  192. package/scripts/hooks/entry/shared/read-stdin.js +0 -2
  193. package/scripts/registries/api-registry.js +0 -2
  194. package/scripts/registries/component-registry.js +5 -9
  195. package/scripts/registries/contract-scanner.js +2 -9
  196. package/scripts/registries/function-registry.js +0 -2
  197. package/scripts/registries/schema-registry.js +14 -18
  198. package/scripts/registries/service-registry.js +23 -27
@@ -1,10 +1,13 @@
1
- 'use strict';
2
-
3
1
  /**
4
- * Learning Orchestrator Facade
2
+ * Learning Orchestrator — Centralized Write Mediator
3
+ *
4
+ * ALL writes to feedback-patterns.md and decisions.md MUST go through this module.
5
+ * Direct fs.writeFileSync to these files from learning modules is prohibited.
5
6
  *
6
- * Coordinates all learning pipeline modules with a unified API.
7
- * Existing direct imports continue working — this facade is additive.
7
+ * Features:
8
+ * - Centralized write API with dedup checking
9
+ * - Write locking via acquireLock to prevent race conditions
10
+ * - Fuzzy dedup: rejects writes that duplicate existing entries
8
11
  *
9
12
  * Sub-modules (lazy-loaded to avoid startup cost):
10
13
  * - flow-skill-learn.js — Skill discovery and learning
@@ -20,6 +23,16 @@
20
23
  * - flow-standards-learner.js — Standards violation learning
21
24
  */
22
25
 
26
+ const fs = require('node:fs');
27
+ const { PATHS } = require('./flow-paths');
28
+ const { acquireLock, readFile, writeFile, fileExists } = require('./flow-io');
29
+
30
+ // ============================================================
31
+ // Constants
32
+ // ============================================================
33
+
34
+ const DEDUP_SIMILARITY_THRESHOLD = 0.85; // 85% similarity = duplicate
35
+
23
36
  // ============================================================
24
37
  // Lazy Loaders
25
38
  // ============================================================
@@ -36,6 +49,287 @@ function getFailureLearning() { return require('./flow-failure-learning'); }
36
49
  function getAutoLearn() { return require('./flow-auto-learn'); }
37
50
  function getStandardsLearner() { return require('./flow-standards-learner'); }
38
51
 
52
+ // ============================================================
53
+ // Dedup Utilities
54
+ // ============================================================
55
+
56
+ /**
57
+ * Normalize text for dedup comparison: lowercase, strip punctuation, collapse whitespace.
58
+ * @param {string} text
59
+ * @returns {string}
60
+ */
61
+ function normalizeForDedup(text) {
62
+ if (!text || typeof text !== 'string') return '';
63
+ return text
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9\s]/g, ' ')
66
+ .replace(/\s+/g, ' ')
67
+ .trim();
68
+ }
69
+
70
+ /**
71
+ * Compute bigram similarity between two strings (Dice coefficient).
72
+ * Returns 0..1 where 1 = identical.
73
+ * @param {string} a
74
+ * @param {string} b
75
+ * @returns {number}
76
+ */
77
+ function bigramSimilarity(a, b) {
78
+ const na = normalizeForDedup(a);
79
+ const nb = normalizeForDedup(b);
80
+
81
+ if (na === nb) return 1.0;
82
+ if (na.length < 2 || nb.length < 2) return 0;
83
+
84
+ const bigramsA = new Set();
85
+ for (let i = 0; i < na.length - 1; i++) bigramsA.add(na.slice(i, i + 2));
86
+ const bigramsB = new Set();
87
+ for (let i = 0; i < nb.length - 1; i++) bigramsB.add(nb.slice(i, i + 2));
88
+
89
+ let intersection = 0;
90
+ for (const bg of bigramsA) {
91
+ if (bigramsB.has(bg)) intersection++;
92
+ }
93
+
94
+ return (2 * intersection) / (bigramsA.size + bigramsB.size);
95
+ }
96
+
97
+ /**
98
+ * Check if an entry already exists in file content (fuzzy match).
99
+ * Checks both exact substring and bigram similarity against existing entries.
100
+ *
101
+ * @param {string} content - Current file content
102
+ * @param {string} entryText - The new entry text to check
103
+ * @returns {{ isDuplicate: boolean, matchedText?: string, similarity?: number }}
104
+ */
105
+ function checkDuplicate(content, entryText) {
106
+ if (!content || !entryText) return { isDuplicate: false };
107
+
108
+ const normalizedEntry = normalizeForDedup(entryText);
109
+ if (!normalizedEntry || normalizedEntry.length < 5) return { isDuplicate: false };
110
+
111
+ // Exact substring check (case-insensitive)
112
+ if (content.toLowerCase().includes(normalizedEntry)) {
113
+ return { isDuplicate: true, matchedText: entryText, similarity: 1.0 };
114
+ }
115
+
116
+ // Extract existing entries from tables (pipe-delimited rows) and headings
117
+ const lines = content.split('\n');
118
+ for (const line of lines) {
119
+ const trimmed = line.trim();
120
+
121
+ // Skip header rows and separators
122
+ if (!trimmed || trimmed.startsWith('|---') || trimmed.startsWith('| Date')) continue;
123
+
124
+ // Table row: extract content cells
125
+ if (trimmed.startsWith('|')) {
126
+ const cells = trimmed.split('|').slice(1, -1).map(c => c.trim());
127
+ // Check cells 1 and 2 (pattern/correction columns)
128
+ for (let i = 1; i < Math.min(cells.length, 3); i++) {
129
+ const sim = bigramSimilarity(cells[i], entryText);
130
+ if (sim >= DEDUP_SIMILARITY_THRESHOLD) {
131
+ return { isDuplicate: true, matchedText: cells[i], similarity: sim };
132
+ }
133
+ }
134
+ }
135
+
136
+ // Heading: ### Pattern Name
137
+ if (trimmed.startsWith('###')) {
138
+ const heading = trimmed.replace(/^###\s*/, '').replace(/\s*\([\d-]+\)$/, '');
139
+ const sim = bigramSimilarity(heading, entryText);
140
+ if (sim >= DEDUP_SIMILARITY_THRESHOLD) {
141
+ return { isDuplicate: true, matchedText: heading, similarity: sim };
142
+ }
143
+ }
144
+ }
145
+
146
+ return { isDuplicate: false };
147
+ }
148
+
149
+ // ============================================================
150
+ // Centralized Write API
151
+ // ============================================================
152
+
153
+ /**
154
+ * Write to feedback-patterns.md through the orchestrator.
155
+ * Acquires a lock, checks for duplicates, then applies the write.
156
+ *
157
+ * @param {Object} params
158
+ * @param {string} params.content - Full new content to write (replaces file)
159
+ * @param {string} [params.entryText] - Key text of the new entry (for dedup)
160
+ * @param {string} params.caller - Calling module name (for logging)
161
+ * @param {boolean} [params.skipDedup=false] - Skip dedup check (for bulk rewrites)
162
+ * @returns {Promise<{ success: boolean, reason?: string }>}
163
+ */
164
+ async function writeToFeedbackPatterns({ content, entryText, caller, skipDedup = false }) {
165
+ const filePath = PATHS.feedbackPatterns;
166
+
167
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
168
+ try {
169
+ // Dedup check
170
+ if (!skipDedup && entryText) {
171
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
172
+ const dupCheck = checkDuplicate(currentContent, entryText);
173
+ if (dupCheck.isDuplicate) {
174
+ if (process.env.DEBUG) {
175
+ console.log(`[orchestrator] Dedup rejected write from ${caller}: "${entryText}" matches "${dupCheck.matchedText}" (${(dupCheck.similarity * 100).toFixed(0)}%)`);
176
+ }
177
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText, similarity: dupCheck.similarity };
178
+ }
179
+ }
180
+
181
+ writeFile(filePath, content);
182
+ if (process.env.DEBUG) {
183
+ console.log(`[orchestrator] ${caller} wrote to feedback-patterns.md`);
184
+ }
185
+ return { success: true };
186
+ } catch (err) {
187
+ return { success: false, reason: err.message };
188
+ } finally {
189
+ release();
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Write to decisions.md through the orchestrator.
195
+ * Acquires a lock, checks for duplicates, then applies the write.
196
+ *
197
+ * @param {Object} params
198
+ * @param {string} params.content - Full new content to write (replaces file)
199
+ * @param {string} [params.entryText] - Key text of the new entry (for dedup)
200
+ * @param {string} params.caller - Calling module name (for logging)
201
+ * @param {boolean} [params.skipDedup=false] - Skip dedup check (for bulk rewrites)
202
+ * @param {boolean} [params.syncRules=false] - Trigger rules sync after write
203
+ * @returns {Promise<{ success: boolean, reason?: string }>}
204
+ */
205
+ async function writeToDecisions({ content, entryText, caller, skipDedup = false, syncRules = false }) {
206
+ const filePath = PATHS.decisions;
207
+
208
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
209
+ try {
210
+ // Dedup check
211
+ if (!skipDedup && entryText) {
212
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
213
+ const dupCheck = checkDuplicate(currentContent, entryText);
214
+ if (dupCheck.isDuplicate) {
215
+ if (process.env.DEBUG) {
216
+ console.log(`[orchestrator] Dedup rejected write from ${caller}: "${entryText}" matches "${dupCheck.matchedText}" (${(dupCheck.similarity * 100).toFixed(0)}%)`);
217
+ }
218
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText, similarity: dupCheck.similarity };
219
+ }
220
+ }
221
+
222
+ writeFile(filePath, content);
223
+ if (process.env.DEBUG) {
224
+ console.log(`[orchestrator] ${caller} wrote to decisions.md`);
225
+ }
226
+
227
+ // Optionally sync rules after write
228
+ if (syncRules) {
229
+ try {
230
+ require('./flow-rules-sync');
231
+ } catch (_err) {
232
+ // Non-fatal — rules sync is optional
233
+ }
234
+ }
235
+
236
+ return { success: true };
237
+ } catch (err) {
238
+ return { success: false, reason: err.message };
239
+ } finally {
240
+ release();
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Read-modify-write helper for feedback-patterns.md.
246
+ * Acquires lock, reads current content, calls modifier, writes result.
247
+ *
248
+ * @param {Function} modifier - (currentContent: string) => { content: string, entryText?: string }
249
+ * @param {Object} opts - { caller: string, skipDedup?: boolean }
250
+ * @returns {Promise<{ success: boolean, reason?: string }>}
251
+ */
252
+ async function modifyFeedbackPatterns(modifier, { caller, skipDedup = false } = {}) {
253
+ const filePath = PATHS.feedbackPatterns;
254
+
255
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
256
+ try {
257
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
258
+ const result = modifier(currentContent);
259
+
260
+ if (!result || !result.content) {
261
+ return { success: false, reason: 'modifier returned no content' };
262
+ }
263
+
264
+ // Dedup check
265
+ if (!skipDedup && result.entryText) {
266
+ const dupCheck = checkDuplicate(currentContent, result.entryText);
267
+ if (dupCheck.isDuplicate) {
268
+ if (process.env.DEBUG) {
269
+ console.log(`[orchestrator] Dedup rejected modify from ${caller}: "${result.entryText}" matches "${dupCheck.matchedText}"`);
270
+ }
271
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText };
272
+ }
273
+ }
274
+
275
+ writeFile(filePath, result.content);
276
+ return { success: true };
277
+ } catch (err) {
278
+ return { success: false, reason: err.message };
279
+ } finally {
280
+ release();
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Read-modify-write helper for decisions.md.
286
+ * Acquires lock, reads current content, calls modifier, writes result.
287
+ *
288
+ * @param {Function} modifier - (currentContent: string) => { content: string, entryText?: string }
289
+ * @param {Object} opts - { caller: string, skipDedup?: boolean, syncRules?: boolean }
290
+ * @returns {Promise<{ success: boolean, reason?: string }>}
291
+ */
292
+ async function modifyDecisions(modifier, { caller, skipDedup = false, syncRules = false } = {}) {
293
+ const filePath = PATHS.decisions;
294
+
295
+ const release = await acquireLock(filePath, { retries: 5, retryDelay: 100 });
296
+ try {
297
+ const currentContent = fileExists(filePath) ? readFile(filePath, '') : '';
298
+ const result = modifier(currentContent);
299
+
300
+ if (!result || !result.content) {
301
+ return { success: false, reason: 'modifier returned no content' };
302
+ }
303
+
304
+ // Dedup check
305
+ if (!skipDedup && result.entryText) {
306
+ const dupCheck = checkDuplicate(currentContent, result.entryText);
307
+ if (dupCheck.isDuplicate) {
308
+ if (process.env.DEBUG) {
309
+ console.log(`[orchestrator] Dedup rejected modify from ${caller}: "${result.entryText}" matches "${dupCheck.matchedText}"`);
310
+ }
311
+ return { success: false, reason: 'duplicate', matchedText: dupCheck.matchedText };
312
+ }
313
+ }
314
+
315
+ writeFile(filePath, result.content);
316
+
317
+ if (syncRules) {
318
+ try {
319
+ require('./flow-rules-sync');
320
+ } catch (_err) {
321
+ // Non-fatal
322
+ }
323
+ }
324
+
325
+ return { success: true };
326
+ } catch (err) {
327
+ return { success: false, reason: err.message };
328
+ } finally {
329
+ release();
330
+ }
331
+ }
332
+
39
333
  // ============================================================
40
334
  // Unified API
41
335
  // ============================================================
@@ -114,10 +408,10 @@ async function learn(type, context = {}) {
114
408
  function getStats() {
115
409
  const stats = {};
116
410
 
117
- try { stats.tiered = getTieredLearning().getLearningStats(); } catch (err) { stats.tiered = null; }
118
- try { stats.failure = getFailureLearning().getLearningStats(); } catch (err) { stats.failure = null; }
119
- try { stats.loop = getLoopRetryLearning().getLearningStats(); } catch (err) { stats.loop = null; }
120
- try { stats.auto = getAutoLearn().showStatus(); } catch (err) { stats.auto = null; }
411
+ try { stats.tiered = getTieredLearning().getLearningStats(); } catch (_err) { stats.tiered = null; }
412
+ try { stats.failure = getFailureLearning().getLearningStats(); } catch (_err) { stats.failure = null; }
413
+ try { stats.loop = getLoopRetryLearning().getLearningStats(); } catch (_err) { stats.loop = null; }
414
+ try { stats.auto = getAutoLearn().showStatus(); } catch (_err) { stats.auto = null; }
121
415
 
122
416
  return stats;
123
417
  }
@@ -134,19 +428,19 @@ function loadRelevantPatterns(context = {}) {
134
428
 
135
429
  try {
136
430
  patterns.decisions = getPatternEnforcer().loadDecisionPatterns();
137
- } catch (err) {
431
+ } catch (_err) {
138
432
  patterns.decisions = [];
139
433
  }
140
434
 
141
435
  try {
142
436
  patterns.skills = getPatternEnforcer().loadSkillPatterns(context.skills || []);
143
- } catch (err) {
437
+ } catch (_err) {
144
438
  patterns.skills = [];
145
439
  }
146
440
 
147
441
  try {
148
442
  patterns.tiered = getTieredLearning().getPatternsByTier(context.tier || 'all');
149
- } catch (err) {
443
+ } catch (_err) {
150
444
  patterns.tiered = [];
151
445
  }
152
446
 
@@ -158,7 +452,18 @@ function loadRelevantPatterns(context = {}) {
158
452
  // ============================================================
159
453
 
160
454
  module.exports = {
161
- // Unified API
455
+ // Centralized write API (MUST be used for all learning file writes)
456
+ writeToFeedbackPatterns,
457
+ writeToDecisions,
458
+ modifyFeedbackPatterns,
459
+ modifyDecisions,
460
+
461
+ // Dedup utilities (exposed for testing)
462
+ checkDuplicate,
463
+ bigramSimilarity,
464
+ normalizeForDedup,
465
+
466
+ // Unified learning API
162
467
  learn,
163
468
  getStats,
164
469
  loadRelevantPatterns,
@@ -25,14 +25,12 @@ const path = require('node:path');
25
25
  const https = require('node:https');
26
26
  const http = require('node:http');
27
27
  const dns = require('dns');
28
- const { getProjectRoot, colors: c, readJson } = require('./flow-utils');
28
+ const { getProjectRoot, colors: c, readJson, PATHS } = require('./flow-utils');
29
29
  const { success: printSuccess } = require('./flow-output');
30
30
 
31
- const PROJECT_ROOT = getProjectRoot();
32
- const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
33
- const LINKS_PATH = path.join(WORKFLOW_DIR, 'links.yaml');
34
- const LINKS_JSON_PATH = path.join(WORKFLOW_DIR, 'links.json');
35
- const CACHE_DIR = path.join(WORKFLOW_DIR, 'cache', 'links');
31
+ const LINKS_PATH = path.join(PATHS.workflow, 'links.yaml');
32
+ const LINKS_JSON_PATH = path.join(PATHS.workflow, 'links.json');
33
+ const CACHE_DIR = path.join(PATHS.workflow, 'cache', 'links');
36
34
 
37
35
  /**
38
36
  * Link types
@@ -357,7 +355,7 @@ async function fetchLink(name, links = null) {
357
355
  const filePath = url.startsWith('file://') ? url.slice(7) : url;
358
356
  const absPath = path.isAbsolute(filePath)
359
357
  ? filePath
360
- : path.join(PROJECT_ROOT, filePath);
358
+ : path.join(PATHS.root, filePath);
361
359
 
362
360
  if (fs.existsSync(absPath)) {
363
361
  content = fs.readFileSync(absPath, 'utf-8');
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Long Input Processing - Statement Association
3
+ *
4
+ * Extracted from flow-long-input.js
5
+ * Handles statement splitting, meaningfulness checks, association with topics,
6
+ * and statement map persistence.
7
+ */
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const { safeJsonParse } = require('./flow-utils');
12
+
13
+ const { FILLER_PATTERNS, REQUIREMENT_PATTERNS } = require('./flow-long-input-constants');
14
+
15
+ // These are injected via init()
16
+ let _loadActiveDigest = null;
17
+ let _updatePhase = null;
18
+
19
+ /**
20
+ * Initialize with core functions from the main module.
21
+ * @param {Object} deps
22
+ */
23
+ function init(deps) {
24
+ _loadActiveDigest = deps.loadActiveDigest;
25
+ _updatePhase = deps.updatePhase;
26
+ }
27
+
28
+ /**
29
+ * Check if statement is meaningful (contains requirements/substance)
30
+ */
31
+ function isMeaningfulStatement(text) {
32
+ const trimmed = text.trim();
33
+
34
+ // Too short
35
+ if (trimmed.length < 5) return { meaningful: false, reason: 'too_short' };
36
+
37
+ // Check filler patterns
38
+ for (const pattern of FILLER_PATTERNS) {
39
+ if (pattern.test(trimmed)) {
40
+ return { meaningful: false, reason: 'filler' };
41
+ }
42
+ }
43
+
44
+ // Check for requirement signals
45
+ for (const pattern of REQUIREMENT_PATTERNS) {
46
+ if (pattern.test(trimmed)) {
47
+ return { meaningful: true, reason: 'requirement_signal' };
48
+ }
49
+ }
50
+
51
+ // Check word count - very short statements without requirement signals are likely filler
52
+ const wordCount = trimmed.split(/\s+/).length;
53
+
54
+ if (wordCount < 4) {
55
+ return { meaningful: false, reason: 'too_brief' };
56
+ }
57
+
58
+ // Default to meaningful if substantial enough
59
+ return { meaningful: true, reason: 'substantial' };
60
+ }
61
+
62
+ /**
63
+ * Split transcript into statements
64
+ */
65
+ function splitIntoStatements(text) {
66
+ const statements = [];
67
+ let position = 0;
68
+
69
+ // Split by sentence boundaries and speaker changes
70
+ const segments = text.split(/(?<=[.!?])\s+|(?=^[A-Z][a-z]+:|\[\d{2}:\d{2})/gm);
71
+
72
+ for (const segment of segments) {
73
+ const trimmed = segment.trim();
74
+ if (!trimmed) continue;
75
+
76
+ // Extract speaker if present
77
+ const speakerMatch = trimmed.match(/^([A-Z][a-z]+):\s*/);
78
+ const speaker = speakerMatch ? speakerMatch[1] : null;
79
+ const content = speakerMatch ? trimmed.slice(speakerMatch[0].length) : trimmed;
80
+
81
+ // Extract timestamp if present
82
+ const timestampMatch = trimmed.match(/^\[?(\d{2}:\d{2}(?::\d{2})?)\]?\s*/);
83
+ const timestamp = timestampMatch ? timestampMatch[1] : null;
84
+
85
+ if (content.trim()) {
86
+ statements.push({
87
+ text: content.trim(),
88
+ speaker,
89
+ timestamp,
90
+ position
91
+ });
92
+ }
93
+
94
+ position += segment.length;
95
+ }
96
+
97
+ return statements;
98
+ }
99
+
100
+ /**
101
+ * Calculate association confidence between statement and topic
102
+ */
103
+ function calculateAssociationConfidence(statement, topic) {
104
+ let confidence = 0.5; // Base confidence
105
+ const reasons = [];
106
+
107
+ const statementLower = statement.text.toLowerCase();
108
+ const topicTitle = topic.title.toLowerCase();
109
+
110
+ // Entity match - highest confidence
111
+ if (topic.entities) {
112
+ for (const entity of topic.entities) {
113
+ if (statementLower.includes(entity.toLowerCase())) {
114
+ confidence = Math.max(confidence, 0.9);
115
+ reasons.push(`entity_match:${entity}`);
116
+ }
117
+ }
118
+ }
119
+
120
+ // Title word match
121
+ const titleWords = topicTitle.split(/\s+/).filter(w => w.length > 3);
122
+ for (const word of titleWords) {
123
+ if (statementLower.includes(word)) {
124
+ confidence = Math.max(confidence, 0.8);
125
+ reasons.push(`title_match:${word}`);
126
+ }
127
+ }
128
+
129
+ // Keyword match
130
+ if (topic.keywords) {
131
+ for (const keyword of topic.keywords) {
132
+ if (statementLower.includes(keyword.toLowerCase())) {
133
+ confidence = Math.max(confidence, 0.75);
134
+ reasons.push(`keyword_match:${keyword}`);
135
+ }
136
+ }
137
+ }
138
+
139
+ return { confidence, reasons };
140
+ }
141
+
142
+ /**
143
+ * Associate statements with topics
144
+ */
145
+ function associateStatements(statements, topics) {
146
+ const mappedStatements = [];
147
+ let currentTopicId = null;
148
+ let statementId = 1;
149
+
150
+ for (const stmt of statements) {
151
+ const meaningfulCheck = isMeaningfulStatement(stmt.text);
152
+
153
+ const mappedStatement = {
154
+ id: `s-${String(statementId).padStart(3, '0')}`,
155
+ text: stmt.text,
156
+ position: stmt.position,
157
+ timestamp: stmt.timestamp,
158
+ speaker: stmt.speaker,
159
+ meaningful: meaningfulCheck.meaningful
160
+ };
161
+
162
+ if (!meaningfulCheck.meaningful) {
163
+ mappedStatement.topic_id = null;
164
+ mappedStatement.skip_reason = meaningfulCheck.reason;
165
+ } else {
166
+ // Find best matching topic
167
+ let bestMatch = { topicId: null, confidence: 0, reasons: [] };
168
+
169
+ for (const topic of topics) {
170
+ const { confidence, reasons } = calculateAssociationConfidence(stmt, topic);
171
+ if (confidence > bestMatch.confidence) {
172
+ bestMatch = { topicId: topic.id, confidence, reasons };
173
+ }
174
+ }
175
+
176
+ // Use context continuity if no strong match
177
+ if (bestMatch.confidence < 0.6 && currentTopicId) {
178
+ bestMatch = {
179
+ topicId: currentTopicId,
180
+ confidence: 0.6,
181
+ reasons: ['context_continuity']
182
+ };
183
+ }
184
+
185
+ mappedStatement.topic_id = bestMatch.topicId;
186
+ mappedStatement.confidence = bestMatch.confidence;
187
+ mappedStatement.association_reason = bestMatch.reasons.join(',') || 'context_continuity';
188
+ mappedStatement.clarification_needed = bestMatch.confidence < 0.7;
189
+
190
+ if (mappedStatement.clarification_needed) {
191
+ mappedStatement.clarification_question =
192
+ `You mentioned "${stmt.text.slice(0, 50)}..." - which feature does this relate to?`;
193
+ }
194
+
195
+ // Update current topic for context continuity
196
+ if (bestMatch.confidence >= 0.7) {
197
+ currentTopicId = bestMatch.topicId;
198
+ }
199
+ }
200
+
201
+ mappedStatements.push(mappedStatement);
202
+ statementId++;
203
+ }
204
+
205
+ return mappedStatements;
206
+ }
207
+
208
+ /**
209
+ * Save statement map to digest
210
+ */
211
+ function saveStatementMap(statementMap) {
212
+ const activeDigest = _loadActiveDigest();
213
+ if (!activeDigest.session.digest_path) {
214
+ throw new Error('No active digest session');
215
+ }
216
+
217
+ const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
218
+
219
+ // Calculate metadata
220
+ const meaningful = statementMap.statements.filter(s => s.meaningful);
221
+ const mapped = meaningful.filter(s => s.topic_id !== null);
222
+ const orphans = meaningful.filter(s => s.topic_id === null);
223
+
224
+ const data = {
225
+ statements: statementMap.statements,
226
+ contradictions: statementMap.contradictions || [],
227
+ metadata: {
228
+ total_statements: statementMap.statements.length,
229
+ meaningful_statements: meaningful.length,
230
+ mapped_statements: mapped.length,
231
+ orphan_statements: orphans.length,
232
+ contradictions_detected: (statementMap.contradictions || []).length,
233
+ contradictions_resolved: 0,
234
+ coverage_percentage: meaningful.length > 0
235
+ ? Math.round((mapped.length / meaningful.length) * 100 * 10) / 10
236
+ : 0
237
+ }
238
+ };
239
+
240
+ fs.writeFileSync(mapPath, JSON.stringify(data, null, 2));
241
+
242
+ // Update phase
243
+ _updatePhase('statement_mapping', 'completed', {
244
+ statements_mapped: mapped.length,
245
+ orphans_found: orphans.length,
246
+ contradictions_found: (statementMap.contradictions || []).length
247
+ });
248
+
249
+ return data;
250
+ }
251
+
252
+ /**
253
+ * Load statement map from digest
254
+ */
255
+ function loadStatementMap() {
256
+ const activeDigest = _loadActiveDigest();
257
+ if (!activeDigest.session.digest_path) {
258
+ return null;
259
+ }
260
+
261
+ const mapPath = path.join(activeDigest.session.digest_path, 'statement-map.json');
262
+ const data = safeJsonParse(mapPath, null);
263
+ if (!data) return null;
264
+ return data;
265
+ }
266
+
267
+ module.exports = {
268
+ init,
269
+ isMeaningfulStatement,
270
+ splitIntoStatements,
271
+ calculateAssociationConfidence,
272
+ associateStatements,
273
+ saveStatementMap,
274
+ loadStatementMap
275
+ };
@@ -12,6 +12,7 @@
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
14
  const { readJson } = require('./flow-io');
15
+ const { PATHS } = require('./flow-utils');
15
16
 
16
17
  // Core functions are injected via init() to avoid circular dependencies
17
18
  let digestCore = null;
@@ -1,6 +1,4 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
-
4
2
  /**
5
3
  * Long Input Processing - CLI handler
6
4
  *
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  /**
4
2
  * Long Input Processing - Complexity Detection (E3-S1)
5
3
  *