mindforge-cc 1.0.4 → 2.0.0-alpha.4
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/.agent/CLAUDE.md +53 -0
- package/.agent/mindforge/auto.md +22 -0
- package/.agent/mindforge/browse.md +26 -0
- package/.agent/mindforge/costs.md +11 -0
- package/.agent/mindforge/cross-review.md +17 -0
- package/.agent/mindforge/execute-phase.md +5 -3
- package/.agent/mindforge/qa.md +16 -0
- package/.agent/mindforge/remember.md +14 -0
- package/.agent/mindforge/research.md +11 -0
- package/.agent/mindforge/steer.md +13 -0
- package/.agent/workflows/publish-release.md +36 -0
- package/.claude/CLAUDE.md +53 -0
- package/.claude/commands/mindforge/auto.md +22 -0
- package/.claude/commands/mindforge/browse.md +26 -0
- package/.claude/commands/mindforge/costs.md +11 -0
- package/.claude/commands/mindforge/cross-review.md +17 -0
- package/.claude/commands/mindforge/execute-phase.md +5 -3
- package/.claude/commands/mindforge/qa.md +16 -0
- package/.claude/commands/mindforge/remember.md +14 -0
- package/.claude/commands/mindforge/research.md +11 -0
- package/.claude/commands/mindforge/steer.md +13 -0
- package/.mindforge/MINDFORGE-V2-SCHEMA.json +47 -0
- package/.mindforge/browser/daemon-protocol.md +24 -0
- package/.mindforge/browser/qa-engine.md +16 -0
- package/.mindforge/browser/session-manager.md +18 -0
- package/.mindforge/browser/visual-verify-spec.md +31 -0
- package/.mindforge/engine/autonomous/auto-executor.md +266 -0
- package/.mindforge/engine/autonomous/headless-adapter.md +66 -0
- package/.mindforge/engine/autonomous/node-repair.md +190 -0
- package/.mindforge/engine/autonomous/progress-reporter.md +58 -0
- package/.mindforge/engine/autonomous/steering-manager.md +64 -0
- package/.mindforge/engine/autonomous/stuck-detector.md +89 -0
- package/.mindforge/memory/MEMORY-SCHEMA.md +155 -0
- package/.mindforge/memory/decision-library.jsonl +0 -0
- package/.mindforge/memory/engine/capture-protocol.md +36 -0
- package/.mindforge/memory/engine/global-sync-spec.md +42 -0
- package/.mindforge/memory/engine/retrieval-spec.md +44 -0
- package/.mindforge/memory/knowledge-base.jsonl +7 -0
- package/.mindforge/memory/pattern-library.jsonl +1 -0
- package/.mindforge/memory/team-preferences.jsonl +4 -0
- package/.mindforge/models/model-registry.md +48 -0
- package/.mindforge/models/model-router.md +30 -0
- package/.mindforge/personas/research-agent.md +24 -0
- package/.planning/browser-daemon.log +32 -0
- package/.planning/decisions/ADR-021-autonomy-boundary.md +17 -0
- package/.planning/decisions/ADR-022-node-repair-hierarchy.md +19 -0
- package/.planning/decisions/ADR-023-gate-3-timing.md +15 -0
- package/CHANGELOG.md +73 -0
- package/MINDFORGE.md +26 -3
- package/README.md +59 -18
- package/bin/autonomous/auto-runner.js +95 -0
- package/bin/autonomous/headless.js +36 -0
- package/bin/autonomous/progress-stream.js +49 -0
- package/bin/autonomous/repair-operator.js +213 -0
- package/bin/autonomous/steer.js +71 -0
- package/bin/autonomous/stuck-monitor.js +77 -0
- package/bin/browser/browser-daemon.js +139 -0
- package/bin/browser/daemon-manager.js +91 -0
- package/bin/browser/qa-engine.js +47 -0
- package/bin/browser/qa-report-writer.js +32 -0
- package/bin/browser/regression-writer.js +27 -0
- package/bin/browser/screenshot-store.js +49 -0
- package/bin/browser/session-manager.js +93 -0
- package/bin/browser/visual-verify-executor.js +89 -0
- package/bin/install.js +7 -6
- package/bin/installer-core.js +64 -26
- package/bin/memory/cli.js +99 -0
- package/bin/memory/global-sync.js +107 -0
- package/bin/memory/knowledge-capture.js +278 -0
- package/bin/memory/knowledge-indexer.js +172 -0
- package/bin/memory/knowledge-store.js +319 -0
- package/bin/memory/session-memory-loader.js +137 -0
- package/bin/migrations/0.1.0-to-0.5.0.js +2 -3
- package/bin/migrations/0.5.0-to-0.6.0.js +1 -1
- package/bin/migrations/0.6.0-to-1.0.0.js +3 -3
- package/bin/migrations/migrate.js +15 -11
- package/bin/models/anthropic-provider.js +77 -0
- package/bin/models/cost-tracker.js +118 -0
- package/bin/models/gemini-provider.js +79 -0
- package/bin/models/model-client.js +98 -0
- package/bin/models/model-router.js +111 -0
- package/bin/models/openai-provider.js +78 -0
- package/bin/research/research-engine.js +115 -0
- package/bin/review/cross-review-engine.js +81 -0
- package/bin/review/finding-synthesizer.js +116 -0
- package/bin/review/review-report-writer.js +49 -0
- package/bin/updater/self-update.js +13 -13
- package/bin/wizard/setup-wizard.js +2 -1
- package/docs/adr/ADR-024-browser-localhost-only.md +17 -0
- package/docs/adr/ADR-025-visual-verify-failure-treatment.md +19 -0
- package/docs/adr/ADR-026-session-persistence-security.md +20 -0
- package/docs/architecture/README.md +4 -2
- package/docs/publishing-guide.md +78 -0
- package/docs/reference/commands.md +17 -2
- package/docs/reference/sdk-api.md +6 -1
- package/docs/user-guide.md +98 -9
- package/docs/usp-features.md +56 -8
- package/package.json +3 -2
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Knowledge Capture Engine
|
|
3
|
+
* Automatically extracts and stores knowledge from MindForge lifecycle events.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const Store = require('./knowledge-store');
|
|
10
|
+
const Indexer = require('./knowledge-indexer');
|
|
11
|
+
|
|
12
|
+
const PLANNING_DIR = path.join(process.cwd(), '.planning');
|
|
13
|
+
const DECISIONS_DIR = path.join(PLANNING_DIR, 'decisions');
|
|
14
|
+
|
|
15
|
+
// ── Capture helpers ───────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function getProjectName() {
|
|
18
|
+
const projectMd = path.join(PLANNING_DIR, 'PROJECT.md');
|
|
19
|
+
if (!fs.existsSync(projectMd)) return 'unknown';
|
|
20
|
+
const match = fs.readFileSync(projectMd, 'utf8').match(/^# (.+)/m);
|
|
21
|
+
return match?.[1]?.trim().slice(0, 50) || 'unknown';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function inferTagsFromText(text) {
|
|
25
|
+
const DOMAIN_TAGS = {
|
|
26
|
+
auth: /auth|login|logout|jwt|token|session|password|bcrypt|argon/i,
|
|
27
|
+
database: /database|sql|query|migration|prisma|drizzle|postgres|mysql|mongo/i,
|
|
28
|
+
api: /api|endpoint|route|rest|graphql|webhook|request|response/i,
|
|
29
|
+
security: /security|owasp|xss|csrf|injection|vulnerability|encryption/i,
|
|
30
|
+
performance: /performance|cache|cdn|lazy|async|concurrent|throttle|debounce/i,
|
|
31
|
+
testing: /test|spec|mock|stub|fixture|coverage|jest|vitest|playwright/i,
|
|
32
|
+
ui: /component|react|vue|svelte|css|style|render|layout/i,
|
|
33
|
+
infra: /docker|kubernetes|ci|deploy|environment|config|env/i,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const tags = [];
|
|
37
|
+
for (const [tag, pattern] of Object.entries(DOMAIN_TAGS)) {
|
|
38
|
+
if (pattern.test(text)) tags.push(tag);
|
|
39
|
+
}
|
|
40
|
+
return tags;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function deduplicateOrAdd(entry) {
|
|
44
|
+
const existing = Indexer.search(`${entry.topic} ${entry.content}`, {
|
|
45
|
+
type: entry.type,
|
|
46
|
+
minConfidence: 0.0,
|
|
47
|
+
includeGlobal: false,
|
|
48
|
+
}, 3);
|
|
49
|
+
|
|
50
|
+
// Check if we have a near-duplicate
|
|
51
|
+
for (const e of existing) {
|
|
52
|
+
if (!e.deprecated && e.id) {
|
|
53
|
+
// High similarity — reinforce instead of duplicate
|
|
54
|
+
if (e.confidence >= entry.confidence) {
|
|
55
|
+
Store.reinforce(e.id);
|
|
56
|
+
return { action: 'reinforced', id: e.id };
|
|
57
|
+
} else {
|
|
58
|
+
// New entry has higher confidence — supersede old
|
|
59
|
+
Store.deprecate(e.id, 'Superseded by higher-confidence entry', null);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const id = Store.add(entry);
|
|
65
|
+
return { action: 'added', id };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Event-specific capture functions ─────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Capture architectural decisions from ADR files after phase completion.
|
|
72
|
+
*/
|
|
73
|
+
function captureFromPhaseCompletion(phaseNum) {
|
|
74
|
+
if (!fs.existsSync(DECISIONS_DIR)) return [];
|
|
75
|
+
|
|
76
|
+
const captured = [];
|
|
77
|
+
const project = getProjectName();
|
|
78
|
+
|
|
79
|
+
const adrFiles = fs.readdirSync(DECISIONS_DIR)
|
|
80
|
+
.filter(f => f.startsWith('ADR-') && f.endsWith('.md'))
|
|
81
|
+
.sort();
|
|
82
|
+
|
|
83
|
+
for (const adrFile of adrFiles) {
|
|
84
|
+
const filePath = path.join(DECISIONS_DIR, adrFile);
|
|
85
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
86
|
+
|
|
87
|
+
// Extract decision content
|
|
88
|
+
const titleMatch = content.match(/^# ADR-\d+: (.+)/m);
|
|
89
|
+
const decision = (content.match(/## Decision\n+([\s\S]*?)(?=\n##)/)?.[1] || '').trim().slice(0, 500);
|
|
90
|
+
const rationale = (content.match(/## Rationale\n+([\s\S]*?)(?=\n##)/)?.[1] || '').trim().slice(0, 500);
|
|
91
|
+
const status = (content.match(/\*\*Status:\*\*\s*(.+)/)?.[1] || 'Unknown').trim();
|
|
92
|
+
|
|
93
|
+
if (!decision || status === 'Superseded') continue;
|
|
94
|
+
|
|
95
|
+
const topic = titleMatch?.[1]?.trim() || adrFile.replace('.md', '');
|
|
96
|
+
|
|
97
|
+
const result = deduplicateOrAdd({
|
|
98
|
+
type: 'architectural_decision',
|
|
99
|
+
topic: topic.slice(0, 80),
|
|
100
|
+
content: `${decision}\n\nRationale: ${rationale}`,
|
|
101
|
+
source: `${adrFile} (Phase ${phaseNum})`,
|
|
102
|
+
project,
|
|
103
|
+
confidence: 0.90,
|
|
104
|
+
tags: inferTagsFromText(content),
|
|
105
|
+
linked_adrs: [adrFile.replace('.md', '')],
|
|
106
|
+
adr_reference: adrFile.replace('.md', ''),
|
|
107
|
+
decision,
|
|
108
|
+
rationale,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
captured.push({ file: adrFile, ...result });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return captured;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Capture domain knowledge from smart compaction Block D (implicit knowledge).
|
|
119
|
+
*/
|
|
120
|
+
function captureFromCompaction(handoffPath) {
|
|
121
|
+
if (!fs.existsSync(handoffPath)) return [];
|
|
122
|
+
|
|
123
|
+
const handoff = JSON.parse(fs.readFileSync(handoffPath, 'utf8'));
|
|
124
|
+
const items = handoff.implicit_knowledge || [];
|
|
125
|
+
const project = getProjectName();
|
|
126
|
+
const captured = [];
|
|
127
|
+
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
if (!item || typeof item !== 'object') continue;
|
|
130
|
+
|
|
131
|
+
const confidence = item.confidence ?? 0.5;
|
|
132
|
+
if (confidence < 0.5) continue; // Skip low-confidence items
|
|
133
|
+
|
|
134
|
+
const result = deduplicateOrAdd({
|
|
135
|
+
type: 'domain_knowledge',
|
|
136
|
+
topic: item.topic || item.text?.slice(0, 80) || 'Unknown topic',
|
|
137
|
+
content: item.content || item.text || String(item),
|
|
138
|
+
source: 'Smart compaction Block D',
|
|
139
|
+
project,
|
|
140
|
+
confidence: confidence * 0.9, // Slight discount for auto-captured
|
|
141
|
+
tags: inferTagsFromText(item.content || item.text || ''),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
captured.push(result);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return captured;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Capture bug patterns from debug reports.
|
|
152
|
+
*/
|
|
153
|
+
function captureFromDebugReport(debugReportPath) {
|
|
154
|
+
if (!fs.existsSync(debugReportPath)) return null;
|
|
155
|
+
|
|
156
|
+
const content = fs.readFileSync(debugReportPath, 'utf8');
|
|
157
|
+
const project = getProjectName();
|
|
158
|
+
|
|
159
|
+
const rootCause = (content.match(/## Root [Cc]ause\n+([\s\S]*?)(?=\n##)/)?.[1] || '').trim();
|
|
160
|
+
const fix = (content.match(/## Fix\n+([\s\S]*?)(?=\n##)/)?.[1] || '').trim();
|
|
161
|
+
const title = (content.match(/^# Debug[:\s]+(.+)/m)?.[1] || 'Unknown bug').trim();
|
|
162
|
+
|
|
163
|
+
if (!rootCause) return null;
|
|
164
|
+
|
|
165
|
+
const result = deduplicateOrAdd({
|
|
166
|
+
type: 'bug_pattern',
|
|
167
|
+
topic: title.slice(0, 80),
|
|
168
|
+
content: `Root cause: ${rootCause}\n\nFix: ${fix}`,
|
|
169
|
+
source: `Debug session: ${path.basename(debugReportPath)}`,
|
|
170
|
+
project,
|
|
171
|
+
confidence: 0.80,
|
|
172
|
+
tags: inferTagsFromText(content),
|
|
173
|
+
bug_category: inferBugCategory(content),
|
|
174
|
+
root_cause: rootCause.slice(0, 500),
|
|
175
|
+
fix: fix.slice(0, 500),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Capture team preferences from retrospective reports.
|
|
183
|
+
*/
|
|
184
|
+
function captureFromRetrospective(retroReportPath) {
|
|
185
|
+
if (!fs.existsSync(retroReportPath)) return [];
|
|
186
|
+
|
|
187
|
+
const content = fs.readFileSync(retroReportPath, 'utf8');
|
|
188
|
+
const project = getProjectName();
|
|
189
|
+
const captured = [];
|
|
190
|
+
|
|
191
|
+
// Extract "keep doing" items (positive practices)
|
|
192
|
+
const keepSection = content.match(/## (Keep|What (?:we )?should we keep|Plus|Went well)\n+([\s\S]*?)(?=\n##)/i);
|
|
193
|
+
if (keepSection) {
|
|
194
|
+
const items = keepSection[2].split('\n')
|
|
195
|
+
.filter(l => l.startsWith('- ') || l.startsWith('* '))
|
|
196
|
+
.map(l => l.replace(/^[-*]\s+/, '').trim())
|
|
197
|
+
.filter(l => l.length > 20); // Skip trivial items
|
|
198
|
+
|
|
199
|
+
for (const item of items.slice(0, 5)) {
|
|
200
|
+
const result = deduplicateOrAdd({
|
|
201
|
+
type: 'team_preference',
|
|
202
|
+
topic: item.slice(0, 80),
|
|
203
|
+
content: item,
|
|
204
|
+
source: `Retrospective: ${path.basename(retroReportPath)}`,
|
|
205
|
+
project,
|
|
206
|
+
confidence: 0.70,
|
|
207
|
+
tags: inferTagsFromText(item),
|
|
208
|
+
preference: item,
|
|
209
|
+
strength: 'moderate',
|
|
210
|
+
preference_type: 'process',
|
|
211
|
+
});
|
|
212
|
+
captured.push(result);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return captured;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Capture from cross-review consensus findings.
|
|
221
|
+
*/
|
|
222
|
+
function captureFromCrossReview(crossReviewPath) {
|
|
223
|
+
if (!fs.existsSync(crossReviewPath)) return [];
|
|
224
|
+
|
|
225
|
+
const content = fs.readFileSync(crossReviewPath, 'utf8');
|
|
226
|
+
const project = getProjectName();
|
|
227
|
+
const captured = [];
|
|
228
|
+
|
|
229
|
+
// Extract consensus findings table rows
|
|
230
|
+
const tableRows = content.match(/\|\s*\d+\s*\|\s*\*\*\w+\*\*\s*\|.+/g) || [];
|
|
231
|
+
|
|
232
|
+
for (const row of tableRows.slice(0, 10)) {
|
|
233
|
+
const cells = row.split('|').map(c => c.trim()).filter(Boolean);
|
|
234
|
+
if (cells.length < 3) continue;
|
|
235
|
+
|
|
236
|
+
const severity = cells[1]?.replace(/\*\*/g, '') || 'MEDIUM';
|
|
237
|
+
const description = cells[2] || '';
|
|
238
|
+
if (description.length < 20) continue;
|
|
239
|
+
|
|
240
|
+
const result = deduplicateOrAdd({
|
|
241
|
+
type: 'bug_pattern',
|
|
242
|
+
topic: description.slice(0, 80),
|
|
243
|
+
content: description,
|
|
244
|
+
source: `Cross-review consensus: ${path.basename(crossReviewPath)}`,
|
|
245
|
+
project,
|
|
246
|
+
confidence: 0.80,
|
|
247
|
+
tags: [...inferTagsFromText(description), 'security'],
|
|
248
|
+
bug_category: 'security',
|
|
249
|
+
root_cause: description,
|
|
250
|
+
severity_when_missed: severity,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
captured.push(result);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return captured;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function inferBugCategory(text) {
|
|
260
|
+
if (/auth|login|session|jwt|token|password/i.test(text)) return 'auth';
|
|
261
|
+
if (/sql|database|query|migration/i.test(text)) return 'database';
|
|
262
|
+
if (/api|endpoint|route|request/i.test(text)) return 'api';
|
|
263
|
+
if (/ui|component|render|css/i.test(text)) return 'ui';
|
|
264
|
+
if (/performance|slow|timeout/i.test(text)) return 'performance';
|
|
265
|
+
if (/security|xss|injection|csrf/i.test(text)) return 'security';
|
|
266
|
+
return 'general';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
captureFromPhaseCompletion,
|
|
271
|
+
captureFromCompaction,
|
|
272
|
+
captureFromDebugReport,
|
|
273
|
+
captureFromRetrospective,
|
|
274
|
+
captureFromCrossReview,
|
|
275
|
+
deduplicateOrAdd,
|
|
276
|
+
inferTagsFromText,
|
|
277
|
+
inferBugCategory,
|
|
278
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2 — Knowledge Indexer
|
|
3
|
+
* TF-IDF inspired relevance scoring for fast knowledge retrieval.
|
|
4
|
+
* Provides tag-based and text-based search across the knowledge graph.
|
|
5
|
+
*
|
|
6
|
+
* Design note: We use a simple in-memory index rebuilt on each query
|
|
7
|
+
* (not persisted) because the knowledge base stays small (< 10K entries
|
|
8
|
+
* for a typical project). Rebuild time < 50ms for 1K entries.
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const Store = require('./knowledge-store');
|
|
13
|
+
|
|
14
|
+
// ── Stopwords (excluded from TF-IDF scoring) ──────────────────────────────────
|
|
15
|
+
const STOPWORDS = new Set([
|
|
16
|
+
'the', 'a', 'an', 'is', 'it', 'in', 'on', 'at', 'to', 'for', 'of', 'and',
|
|
17
|
+
'or', 'but', 'not', 'this', 'that', 'with', 'from', 'by', 'be', 'are',
|
|
18
|
+
'was', 'were', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
|
|
19
|
+
'could', 'should', 'may', 'might', 'can', 'use', 'using', 'used', 'when',
|
|
20
|
+
'where', 'which', 'what', 'how', 'why', 'who', 'all', 'any', 'some', 'we',
|
|
21
|
+
'our', 'they', 'their', 'we', 'you', 'your', 'my', 'its',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// ── Tokenizer ─────────────────────────────────────────────────────────────────
|
|
25
|
+
function tokenize(text) {
|
|
26
|
+
if (!text) return [];
|
|
27
|
+
return text
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9\s_-]/g, ' ')
|
|
30
|
+
.split(/\s+/)
|
|
31
|
+
.filter(w => w.length > 2 && !STOPWORDS.has(w));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Build in-memory index ─────────────────────────────────────────────────────
|
|
35
|
+
function buildIndex(entries) {
|
|
36
|
+
const index = new Map(); // token → [{ id, count }]
|
|
37
|
+
const docTokenCounts = new Map(); // id → token count
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.deprecated) continue;
|
|
41
|
+
|
|
42
|
+
const text = `${entry.topic} ${entry.content} ${(entry.tags || []).join(' ')}`;
|
|
43
|
+
const tokens = tokenize(text);
|
|
44
|
+
const counts = {};
|
|
45
|
+
|
|
46
|
+
for (const tok of tokens) {
|
|
47
|
+
counts[tok] = (counts[tok] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
docTokenCounts.set(entry.id, tokens.length);
|
|
51
|
+
|
|
52
|
+
for (const [tok, count] of Object.entries(counts)) {
|
|
53
|
+
if (!index.has(tok)) index.set(tok, []);
|
|
54
|
+
index.get(tok).push({ id: entry.id, count });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { index, docTokenCounts, N: entries.length };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── TF-IDF scoring ────────────────────────────────────────────────────────────
|
|
62
|
+
function tfidfScore(queryTokens, entryId, index, docTokenCounts, N) {
|
|
63
|
+
let score = 0;
|
|
64
|
+
const docLen = docTokenCounts.get(entryId) || 1;
|
|
65
|
+
|
|
66
|
+
for (const qTok of queryTokens) {
|
|
67
|
+
const postings = index.get(qTok) || [];
|
|
68
|
+
const df = postings.length; // Document frequency
|
|
69
|
+
if (df === 0) continue;
|
|
70
|
+
|
|
71
|
+
const posting = postings.find(p => p.id === entryId);
|
|
72
|
+
if (!posting) continue;
|
|
73
|
+
|
|
74
|
+
const tf = posting.count / docLen; // Term frequency (normalized)
|
|
75
|
+
const idf = Math.log((N + 1) / (df + 1)) + 1; // Smoothed IDF
|
|
76
|
+
score += tf * idf;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return score;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Main search function ──────────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Search knowledge base with TF-IDF scoring.
|
|
85
|
+
* @param {string} queryText - Natural language query
|
|
86
|
+
* @param {object} filters - Optional filters { type, tags, minConfidence }
|
|
87
|
+
* @param {number} limit - Max results to return
|
|
88
|
+
* @returns {object[]} Ranked results
|
|
89
|
+
*/
|
|
90
|
+
function search(queryText, filters = {}, limit = 10) {
|
|
91
|
+
const allEntries = Store.readAll(filters.includeGlobal);
|
|
92
|
+
const active = allEntries.filter(e => !e.deprecated);
|
|
93
|
+
|
|
94
|
+
// Apply filters
|
|
95
|
+
let candidates = active;
|
|
96
|
+
if (filters.type) candidates = candidates.filter(e => e.type === filters.type);
|
|
97
|
+
if (filters.minConfidence) candidates = candidates.filter(e => e.confidence >= filters.minConfidence);
|
|
98
|
+
if (filters.tags?.length) {
|
|
99
|
+
const filterTags = filters.tags.map(t => t.toLowerCase());
|
|
100
|
+
candidates = candidates.filter(e =>
|
|
101
|
+
(e.tags || []).some(t => filterTags.includes(t.toLowerCase()))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (candidates.length === 0) return [];
|
|
106
|
+
|
|
107
|
+
const queryTokens = tokenize(queryText);
|
|
108
|
+
if (queryTokens.length === 0) {
|
|
109
|
+
// No meaningful query tokens — return by confidence
|
|
110
|
+
return candidates
|
|
111
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
112
|
+
.slice(0, limit);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { index, docTokenCounts, N } = buildIndex(candidates);
|
|
116
|
+
|
|
117
|
+
// Score each candidate
|
|
118
|
+
const scored = candidates.map(entry => {
|
|
119
|
+
const textScore = tfidfScore(queryTokens, entry.id, index, docTokenCounts, N);
|
|
120
|
+
// Combine TF-IDF score with confidence, but only if there's a text match
|
|
121
|
+
const finalScore = textScore > 0
|
|
122
|
+
? textScore * 0.7 + entry.confidence * 0.3
|
|
123
|
+
: 0;
|
|
124
|
+
return { entry, score: finalScore };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return scored
|
|
128
|
+
.filter(s => s.score > 0)
|
|
129
|
+
.sort((a, b) => b.score - a.score)
|
|
130
|
+
.slice(0, limit)
|
|
131
|
+
.map(s => s.entry);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load session context: retrieve the most relevant memories for the current session.
|
|
136
|
+
* @param {object} context - { techStack, phase, topic, project }
|
|
137
|
+
* @returns {object} Categorized memories for session start display
|
|
138
|
+
*/
|
|
139
|
+
function loadSessionContext(context = {}) {
|
|
140
|
+
const { techStack = [], phase, topic = '', project } = context;
|
|
141
|
+
|
|
142
|
+
const allEntries = Store.readAll(true); // Include global knowledge
|
|
143
|
+
const active = allEntries.filter(e => !e.deprecated && e.confidence >= 0.5);
|
|
144
|
+
|
|
145
|
+
// Build query from context
|
|
146
|
+
const queryText = [
|
|
147
|
+
topic,
|
|
148
|
+
...(techStack || []),
|
|
149
|
+
].join(' ');
|
|
150
|
+
|
|
151
|
+
const { index, docTokenCounts, N } = buildIndex(active);
|
|
152
|
+
const queryTokens = tokenize(queryText);
|
|
153
|
+
|
|
154
|
+
// Score all active entries
|
|
155
|
+
const scored = active.map(e => ({
|
|
156
|
+
entry: e,
|
|
157
|
+
score: queryTokens.length > 0
|
|
158
|
+
? tfidfScore(queryTokens, e.id, index, docTokenCounts, N) * 0.6 + e.confidence * 0.4
|
|
159
|
+
: e.confidence,
|
|
160
|
+
})).sort((a, b) => b.score - a.score);
|
|
161
|
+
|
|
162
|
+
// Bucket by type, top N per bucket
|
|
163
|
+
const preferences = scored.filter(s => s.entry.type === 'team_preference').slice(0, 5).map(s => s.entry);
|
|
164
|
+
const decisions = scored.filter(s => s.entry.type === 'architectural_decision').slice(0, 8).map(s => s.entry);
|
|
165
|
+
const bugPatterns = scored.filter(s => s.entry.type === 'bug_pattern').slice(0, 5).map(s => s.entry);
|
|
166
|
+
const codePatterns = scored.filter(s => s.entry.type === 'code_pattern').slice(0, 5).map(s => s.entry);
|
|
167
|
+
const domain = scored.filter(s => s.entry.type === 'domain_knowledge').slice(0, 3).map(s => s.entry);
|
|
168
|
+
|
|
169
|
+
return { preferences, decisions, bugPatterns, codePatterns, domain };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { search, loadSessionContext, buildIndex, tfidfScore, tokenize };
|