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.
- package/.claude/docs/claude-code-compatibility.md +27 -0
- package/.claude/settings.json +1 -1
- package/.workflow/models/registry.json +1 -1
- package/package.json +4 -4
- package/scripts/MEMORY-ARCHITECTURE.md +1 -1
- package/scripts/base-workflow-step.js +136 -0
- package/scripts/flow-adaptive-learning.js +8 -9
- package/scripts/flow-aggregate.js +11 -6
- package/scripts/flow-api-index.js +4 -6
- package/scripts/flow-assumption-detector.js +0 -2
- package/scripts/flow-audit.js +15 -2
- package/scripts/flow-auto-context.js +8 -12
- package/scripts/flow-auto-learn.js +49 -49
- package/scripts/flow-background.js +5 -6
- package/scripts/flow-bridge-state.js +8 -10
- package/scripts/flow-bulk-loop.js +1 -3
- package/scripts/flow-bulk-orchestrator.js +1 -3
- package/scripts/flow-cascade-completion.js +0 -2
- package/scripts/flow-cascade.js +4 -4
- package/scripts/flow-checkpoint.js +10 -13
- package/scripts/flow-code-intelligence.js +10 -12
- package/scripts/flow-community-sync.js +4 -4
- package/scripts/flow-community.js +12 -20
- package/scripts/flow-config-defaults.js +0 -2
- package/scripts/flow-config-interactive.js +9 -5
- package/scripts/flow-config-loader.js +49 -92
- package/scripts/flow-config-substitution.js +0 -2
- package/scripts/flow-context-estimator.js +4 -4
- package/scripts/flow-context-init.js +10 -12
- package/scripts/flow-context-manager.js +0 -2
- package/scripts/flow-context-scoring.js +2 -2
- package/scripts/flow-contract-scan.js +6 -9
- package/scripts/flow-correct.js +29 -27
- package/scripts/flow-correction-detector.js +5 -1
- package/scripts/flow-damage-control.js +47 -54
- package/scripts/flow-decisions-merge.js +4 -14
- package/scripts/flow-diff.js +5 -8
- package/scripts/flow-done-gates.js +786 -0
- package/scripts/flow-done-report.js +123 -0
- package/scripts/flow-done.js +71 -717
- package/scripts/flow-entropy-monitor.js +1 -3
- package/scripts/flow-eval.js +5 -5
- package/scripts/flow-extraction-review.js +1 -0
- package/scripts/flow-failure-categories.js +0 -2
- package/scripts/flow-figma-confirm.js +5 -9
- package/scripts/flow-figma-generate.js +8 -10
- package/scripts/flow-figma-index.js +8 -10
- package/scripts/flow-figma-match.js +3 -5
- package/scripts/flow-figma-mcp-server.js +2 -4
- package/scripts/flow-figma-orchestrator.js +2 -3
- package/scripts/flow-figma-registry.js +2 -3
- package/scripts/flow-framework-resolver.js +0 -2
- package/scripts/flow-function-index.js +4 -6
- package/scripts/flow-gate-confidence.js +2 -2
- package/scripts/flow-gitignore.js +0 -2
- package/scripts/flow-guided-edit.js +5 -6
- package/scripts/flow-health.js +5 -6
- package/scripts/flow-hook-errors.js +6 -0
- package/scripts/flow-hook-status.js +263 -0
- package/scripts/flow-hooks.js +17 -29
- package/scripts/flow-http-client.js +9 -8
- package/scripts/flow-hybrid-interactive.js +7 -12
- package/scripts/flow-hybrid-test.js +12 -13
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-io.js +21 -4
- package/scripts/flow-knowledge-router.js +9 -3
- package/scripts/flow-learning-orchestrator.js +318 -13
- package/scripts/flow-links.js +5 -7
- package/scripts/flow-long-input-association.js +275 -0
- package/scripts/flow-long-input-chunking.js +1 -0
- package/scripts/flow-long-input-cli.js +0 -2
- package/scripts/flow-long-input-complexity.js +0 -2
- package/scripts/flow-long-input-constants.js +0 -2
- package/scripts/flow-long-input-contradictions.js +351 -0
- package/scripts/flow-long-input-detection.js +0 -2
- package/scripts/flow-long-input-passes.js +885 -0
- package/scripts/flow-long-input-stories.js +1 -1
- package/scripts/flow-long-input-voice.js +0 -2
- package/scripts/flow-long-input.js +425 -3005
- package/scripts/flow-loop-retry-learning.js +2 -3
- package/scripts/flow-lsp.js +3 -3
- package/scripts/flow-mcp-docs.js +3 -4
- package/scripts/flow-memory-db.js +6 -8
- package/scripts/flow-memory-sync.js +18 -11
- package/scripts/flow-metrics.js +1 -2
- package/scripts/flow-model-adapter.js +2 -3
- package/scripts/flow-model-config.js +72 -104
- package/scripts/flow-model-router.js +2 -2
- package/scripts/flow-model-types.js +0 -2
- package/scripts/flow-multi-approach.js +5 -6
- package/scripts/flow-orchestrate-context.js +3 -7
- package/scripts/flow-orchestrate-rollback.js +3 -8
- package/scripts/flow-orchestrate-state.js +8 -14
- package/scripts/flow-orchestrate-templates.js +2 -6
- package/scripts/flow-orchestrate-validator.js +5 -9
- package/scripts/flow-orchestrate.js +126 -103
- package/scripts/flow-output.js +0 -2
- package/scripts/flow-parallel.js +1 -1
- package/scripts/flow-paths.js +23 -2
- package/scripts/flow-pattern-enforcer.js +30 -28
- package/scripts/flow-pattern-extractor.js +3 -4
- package/scripts/flow-pending.js +0 -2
- package/scripts/flow-permissions.js +2 -3
- package/scripts/flow-plugin-registry.js +10 -12
- package/scripts/flow-prd-manager.js +1 -1
- package/scripts/flow-progress.js +7 -9
- package/scripts/flow-prompt-composer.js +3 -3
- package/scripts/flow-prompt-template.js +2 -2
- package/scripts/flow-providers.js +7 -4
- package/scripts/flow-registry-manager.js +7 -12
- package/scripts/flow-regression.js +9 -11
- package/scripts/flow-roadmap.js +2 -2
- package/scripts/flow-run-trace.js +16 -15
- package/scripts/flow-safety.js +2 -5
- package/scripts/flow-scanner-base.js +5 -7
- package/scripts/flow-scenario-engine.js +1 -5
- package/scripts/flow-security.js +29 -0
- package/scripts/flow-session-end.js +32 -41
- package/scripts/flow-session-learning.js +53 -49
- package/scripts/flow-setup-hooks.js +2 -3
- package/scripts/flow-skill-create.js +7 -12
- package/scripts/flow-skill-generator.js +12 -16
- package/scripts/flow-skill-learn.js +17 -8
- package/scripts/flow-skill-matcher.js +1 -2
- package/scripts/flow-spec-generator.js +2 -4
- package/scripts/flow-stack-wizard.js +5 -7
- package/scripts/flow-standards-learner.js +35 -16
- package/scripts/flow-start.js +2 -0
- package/scripts/flow-stats-collector.js +2 -2
- package/scripts/flow-status.js +10 -10
- package/scripts/flow-statusline-setup.js +2 -2
- package/scripts/flow-step-changelog.js +2 -3
- package/scripts/flow-step-comments.js +66 -81
- package/scripts/flow-step-complexity.js +50 -70
- package/scripts/flow-step-coverage.js +3 -5
- package/scripts/flow-step-knowledge.js +2 -3
- package/scripts/flow-step-pr-tests.js +64 -74
- package/scripts/flow-step-regression.js +3 -5
- package/scripts/flow-step-review.js +86 -103
- package/scripts/flow-step-security.js +111 -121
- package/scripts/flow-step-silent-failures.js +56 -83
- package/scripts/flow-step-simplifier.js +52 -70
- package/scripts/flow-story.js +4 -7
- package/scripts/flow-strict-adherence.js +3 -4
- package/scripts/flow-task-checkpoint.js +36 -5
- package/scripts/flow-task-enforcer.js +2 -24
- package/scripts/flow-tech-debt.js +1 -1
- package/scripts/flow-template-extractor.js +1 -0
- package/scripts/flow-templates.js +11 -13
- package/scripts/flow-test-api.js +9 -13
- package/scripts/flow-test-discovery.js +1 -1
- package/scripts/flow-test-generate.js +5 -9
- package/scripts/flow-test-integrity.js +3 -7
- package/scripts/flow-test-ui.js +5 -9
- package/scripts/flow-testing-deps.js +1 -3
- package/scripts/flow-tiered-learning.js +4 -4
- package/scripts/flow-todowrite-sync.js +1 -1
- package/scripts/flow-tokens.js +0 -2
- package/scripts/flow-verification-profile.js +6 -10
- package/scripts/flow-verify.js +12 -16
- package/scripts/flow-version-check.js +4 -12
- package/scripts/flow-webmcp-generator.js +3 -5
- package/scripts/flow-workflow-steps.js +0 -2
- package/scripts/flow-workflow.js +9 -11
- package/scripts/hooks/adapters/claude-code.js +2 -0
- package/scripts/hooks/core/config-change.js +1 -0
- package/scripts/hooks/core/extension-registry.js +0 -2
- package/scripts/hooks/core/instructions-loaded.js +1 -1
- package/scripts/hooks/core/observation-capture.js +5 -5
- package/scripts/hooks/core/phase-gate.js +5 -0
- package/scripts/hooks/core/post-compact.js +1 -12
- package/scripts/hooks/core/research-gate.js +2 -12
- package/scripts/hooks/core/routing-gate.js +6 -0
- package/scripts/hooks/core/task-completed.js +12 -0
- package/scripts/hooks/core/worktree-lifecycle.js +1 -1
- package/scripts/hooks/entry/claude-code/config-change.js +6 -29
- package/scripts/hooks/entry/claude-code/instructions-loaded.js +5 -30
- package/scripts/hooks/entry/claude-code/post-compact.js +4 -31
- package/scripts/hooks/entry/claude-code/post-tool-use.js +121 -172
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +260 -361
- package/scripts/hooks/entry/claude-code/session-end.js +4 -28
- package/scripts/hooks/entry/claude-code/session-start.js +205 -243
- package/scripts/hooks/entry/claude-code/setup.js +8 -49
- package/scripts/hooks/entry/claude-code/stop.js +40 -72
- package/scripts/hooks/entry/claude-code/task-completed.js +4 -28
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +113 -195
- package/scripts/hooks/entry/claude-code/worktree-create.js +6 -25
- package/scripts/hooks/entry/claude-code/worktree-remove.js +6 -25
- package/scripts/hooks/entry/shared/hook-runner.js +99 -0
- package/scripts/hooks/entry/shared/read-stdin.js +0 -2
- package/scripts/registries/api-registry.js +0 -2
- package/scripts/registries/component-registry.js +5 -9
- package/scripts/registries/contract-scanner.js +2 -9
- package/scripts/registries/function-registry.js +0 -2
- package/scripts/registries/schema-registry.js +14 -18
- package/scripts/registries/service-registry.js +23 -27
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Learning Orchestrator
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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 (
|
|
118
|
-
try { stats.failure = getFailureLearning().getLearningStats(); } catch (
|
|
119
|
-
try { stats.loop = getLoopRetryLearning().getLearningStats(); } catch (
|
|
120
|
-
try { stats.auto = getAutoLearn().showStatus(); } catch (
|
|
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 (
|
|
431
|
+
} catch (_err) {
|
|
138
432
|
patterns.decisions = [];
|
|
139
433
|
}
|
|
140
434
|
|
|
141
435
|
try {
|
|
142
436
|
patterns.skills = getPatternEnforcer().loadSkillPatterns(context.skills || []);
|
|
143
|
-
} catch (
|
|
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 (
|
|
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
|
-
//
|
|
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,
|
package/scripts/flow-links.js
CHANGED
|
@@ -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
|
|
32
|
-
const
|
|
33
|
-
const
|
|
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(
|
|
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;
|