mindforge-sdk 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/dist/client.d.ts +28 -0
- package/dist/client.js +232 -0
- package/dist/commands.d.ts +39 -0
- package/dist/commands.js +58 -0
- package/dist/events.d.ts +27 -0
- package/dist/events.js +186 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +16 -0
- package/dist/memory.d.ts +183 -0
- package/dist/memory.js +628 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +5 -0
- package/package.json +48 -0
package/dist/memory.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MindForge v2.4.0 SDK — Memory API (RAG 2.0)
|
|
4
|
+
* Self-contained TypeScript interface to the MindForge Knowledge Graph.
|
|
5
|
+
*
|
|
6
|
+
* This module is fully standalone — it reads/writes JSONL files directly
|
|
7
|
+
* without depending on bin/ internal modules.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.MindForgeMemory = void 0;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const crypto = __importStar(require("crypto"));
|
|
48
|
+
// ── Internal Helpers ──────────────────────────────────────────────────────────
|
|
49
|
+
function ensureDir(dir) {
|
|
50
|
+
if (!fs.existsSync(dir)) {
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function readJsonlFile(filePath) {
|
|
55
|
+
if (!fs.existsSync(filePath))
|
|
56
|
+
return [];
|
|
57
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
58
|
+
const byId = new Map();
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
try {
|
|
61
|
+
const entry = JSON.parse(line);
|
|
62
|
+
byId.set(entry.id, entry); // Last write wins (append-only pattern)
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Skip malformed lines — never crash on corrupt JSONL
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [...byId.values()];
|
|
69
|
+
}
|
|
70
|
+
function appendJsonl(filePath, data) {
|
|
71
|
+
ensureDir(path.dirname(filePath));
|
|
72
|
+
fs.appendFileSync(filePath, JSON.stringify(data) + '\n');
|
|
73
|
+
}
|
|
74
|
+
function generateId() {
|
|
75
|
+
return crypto.randomUUID();
|
|
76
|
+
}
|
|
77
|
+
function computeChecksum(data) {
|
|
78
|
+
const payload = JSON.stringify(data, Object.keys(data).sort());
|
|
79
|
+
return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 16);
|
|
80
|
+
}
|
|
81
|
+
function scoreRelevance(entry, tags, topic) {
|
|
82
|
+
let score = entry.confidence;
|
|
83
|
+
// Tag overlap
|
|
84
|
+
const entryTags = entry.tags || [];
|
|
85
|
+
const tagOverlap = tags.filter(t => entryTags.some(et => et.toLowerCase() === t.toLowerCase())).length;
|
|
86
|
+
score += tagOverlap * 0.2;
|
|
87
|
+
// Topic text match
|
|
88
|
+
if (topic) {
|
|
89
|
+
const topicWords = topic.toLowerCase().split(/\s+/);
|
|
90
|
+
const entryText = `${entry.topic} ${entry.content}`.toLowerCase();
|
|
91
|
+
const wordMatches = topicWords.filter(w => w.length > 3 && entryText.includes(w)).length;
|
|
92
|
+
score += (wordMatches / Math.max(topicWords.length, 1)) * 0.3;
|
|
93
|
+
}
|
|
94
|
+
// Recency boost
|
|
95
|
+
if (entry.last_referenced) {
|
|
96
|
+
const daysSince = (Date.now() - new Date(entry.last_referenced).getTime()) / 86400000;
|
|
97
|
+
if (daysSince < 30)
|
|
98
|
+
score += 0.1 * (1 - daysSince / 30);
|
|
99
|
+
}
|
|
100
|
+
// Penalty for zero references
|
|
101
|
+
if (entry.times_referenced === 0)
|
|
102
|
+
score *= 0.9;
|
|
103
|
+
return score;
|
|
104
|
+
}
|
|
105
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
106
|
+
const MAX_SHADOW_CHARS = 8000;
|
|
107
|
+
const MAX_SHADOW_ITEMS = 5;
|
|
108
|
+
const MIN_SHADOW_SCORE = 0.35;
|
|
109
|
+
const DECAY_RATE = 0.10;
|
|
110
|
+
const DECAY_THRESHOLD_DAYS = 30;
|
|
111
|
+
// ── Main Class ────────────────────────────────────────────────────────────────
|
|
112
|
+
/**
|
|
113
|
+
* MindForge Knowledge Graph client (RAG 2.0).
|
|
114
|
+
*/
|
|
115
|
+
class MindForgeMemory {
|
|
116
|
+
constructor(projectRoot = process.cwd()) {
|
|
117
|
+
this.projectRoot = projectRoot;
|
|
118
|
+
this.memoryDir = path.join(projectRoot, '.mindforge', 'memory');
|
|
119
|
+
this.kbPath = path.join(this.memoryDir, 'knowledge-base.jsonl');
|
|
120
|
+
this.globalPath = path.join(os.homedir(), '.mindforge', 'global-knowledge-base.jsonl');
|
|
121
|
+
this.edgesPath = path.join(this.memoryDir, 'graph-edges.jsonl');
|
|
122
|
+
}
|
|
123
|
+
// ── Knowledge Store Operations ───────────────────────────────────────────
|
|
124
|
+
/** Add a new knowledge entry. */
|
|
125
|
+
async remember(entry) {
|
|
126
|
+
if (!entry.type)
|
|
127
|
+
throw new Error('Knowledge entry requires a "type" field');
|
|
128
|
+
if (!entry.topic)
|
|
129
|
+
throw new Error('Knowledge entry requires a "topic" field');
|
|
130
|
+
if (!entry.content)
|
|
131
|
+
throw new Error('Knowledge entry requires a "content" field');
|
|
132
|
+
const id = entry.id || generateId();
|
|
133
|
+
const now = new Date().toISOString();
|
|
134
|
+
const full = {
|
|
135
|
+
id,
|
|
136
|
+
timestamp: now,
|
|
137
|
+
type: entry.type,
|
|
138
|
+
topic: (entry.topic || '').slice(0, 80),
|
|
139
|
+
content: entry.content,
|
|
140
|
+
source: entry.source || 'sdk',
|
|
141
|
+
project: entry.project || path.basename(this.projectRoot),
|
|
142
|
+
confidence: Math.min(1.0, Math.max(0.0, entry.confidence ?? 0.7)),
|
|
143
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
144
|
+
linked_adrs: Array.isArray(entry.linked_adrs) ? entry.linked_adrs : [],
|
|
145
|
+
times_referenced: entry.times_referenced || 0,
|
|
146
|
+
last_referenced: entry.last_referenced || null,
|
|
147
|
+
deprecated: false,
|
|
148
|
+
deprecated_by: null,
|
|
149
|
+
...(entry.decision && { decision: entry.decision }),
|
|
150
|
+
...(entry.rationale && { rationale: entry.rationale }),
|
|
151
|
+
...(entry.root_cause && { root_cause: entry.root_cause }),
|
|
152
|
+
...(entry.fix && { fix: entry.fix }),
|
|
153
|
+
...(entry.preference && { preference: entry.preference }),
|
|
154
|
+
...(entry.strength && { strength: entry.strength }),
|
|
155
|
+
...(entry.bug_category && { bug_category: entry.bug_category }),
|
|
156
|
+
...(entry.domain && { domain: entry.domain }),
|
|
157
|
+
...(entry.tech_stack && { tech_stack: entry.tech_stack }),
|
|
158
|
+
};
|
|
159
|
+
appendJsonl(this.kbPath, full);
|
|
160
|
+
return id;
|
|
161
|
+
}
|
|
162
|
+
/** Query the knowledge base. */
|
|
163
|
+
async query(params = {}) {
|
|
164
|
+
const { tags = [], topic = '', type, minConfidence = 0.3, limit = 20, includeGlobal = false, includeDeprecated = false, project, } = params;
|
|
165
|
+
let entries = this.readAllEntries(includeGlobal);
|
|
166
|
+
if (!includeDeprecated)
|
|
167
|
+
entries = entries.filter(e => !e.deprecated);
|
|
168
|
+
if (type)
|
|
169
|
+
entries = entries.filter(e => e.type === type);
|
|
170
|
+
if (project)
|
|
171
|
+
entries = entries.filter(e => !e.project || e.project === project);
|
|
172
|
+
entries = entries.filter(e => e.confidence >= minConfidence);
|
|
173
|
+
const scored = entries.map(e => ({
|
|
174
|
+
entry: e,
|
|
175
|
+
score: scoreRelevance(e, tags, topic),
|
|
176
|
+
}));
|
|
177
|
+
return scored
|
|
178
|
+
.filter(s => s.score > 0)
|
|
179
|
+
.sort((a, b) => b.score - a.score)
|
|
180
|
+
.slice(0, limit)
|
|
181
|
+
.map(s => s.entry);
|
|
182
|
+
}
|
|
183
|
+
/** Reinforce an entry (increase confidence). */
|
|
184
|
+
async reinforce(id) {
|
|
185
|
+
const entries = this.readAllEntries(false);
|
|
186
|
+
const entry = entries.find(e => e.id === id && !e.deprecated);
|
|
187
|
+
if (!entry)
|
|
188
|
+
return;
|
|
189
|
+
const reinforced = {
|
|
190
|
+
...entry,
|
|
191
|
+
confidence: Math.min(1.0, entry.confidence + 0.05),
|
|
192
|
+
times_referenced: entry.times_referenced + 1,
|
|
193
|
+
last_referenced: new Date().toISOString(),
|
|
194
|
+
};
|
|
195
|
+
appendJsonl(this.kbPath, reinforced);
|
|
196
|
+
}
|
|
197
|
+
/** Deprecate an entry. */
|
|
198
|
+
async deprecate(id, reason, supersededBy) {
|
|
199
|
+
const entries = this.readAllEntries(false);
|
|
200
|
+
const entry = entries.find(e => e.id === id);
|
|
201
|
+
if (!entry)
|
|
202
|
+
throw new Error(`Knowledge entry not found: ${id}`);
|
|
203
|
+
const deprecated = {
|
|
204
|
+
...entry,
|
|
205
|
+
deprecated: true,
|
|
206
|
+
deprecated_by: supersededBy || null,
|
|
207
|
+
};
|
|
208
|
+
appendJsonl(this.kbPath, deprecated);
|
|
209
|
+
}
|
|
210
|
+
/** Get memory statistics. */
|
|
211
|
+
async getStats() {
|
|
212
|
+
const all = this.readAllEntries(false);
|
|
213
|
+
const active = all.filter(e => !e.deprecated);
|
|
214
|
+
const byType = {};
|
|
215
|
+
for (const e of active) {
|
|
216
|
+
byType[e.type] = (byType[e.type] || 0) + 1;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
total_entries: all.length,
|
|
220
|
+
active_entries: active.length,
|
|
221
|
+
deprecated_entries: all.length - active.length,
|
|
222
|
+
by_type: byType,
|
|
223
|
+
avg_confidence: active.length
|
|
224
|
+
? active.reduce((s, e) => s + e.confidence, 0) / active.length
|
|
225
|
+
: 0,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** Load context for a session. */
|
|
229
|
+
async loadContext(opts) {
|
|
230
|
+
const { techStack = [], phase = '', topic = '' } = opts;
|
|
231
|
+
const entries = this.readAllEntries(true).filter(e => !e.deprecated && e.confidence >= 0.3);
|
|
232
|
+
const preferences = entries.filter(e => e.type === 'team_preference');
|
|
233
|
+
const decisions = entries.filter(e => e.type === 'architectural_decision');
|
|
234
|
+
const bugPatterns = entries.filter(e => e.type === 'bug_pattern');
|
|
235
|
+
const codePatterns = entries.filter(e => e.type === 'code_pattern');
|
|
236
|
+
const domain = entries.filter(e => e.type === 'domain_knowledge');
|
|
237
|
+
// Apply relevance filtering if topic or techStack given
|
|
238
|
+
const filterByRelevance = (list) => {
|
|
239
|
+
if (!topic && techStack.length === 0)
|
|
240
|
+
return list.slice(0, 5);
|
|
241
|
+
return list
|
|
242
|
+
.map(e => ({ entry: e, score: scoreRelevance(e, techStack, topic || phase) }))
|
|
243
|
+
.sort((a, b) => b.score - a.score)
|
|
244
|
+
.slice(0, 5)
|
|
245
|
+
.map(s => s.entry);
|
|
246
|
+
};
|
|
247
|
+
const result = {
|
|
248
|
+
preferences: filterByRelevance(preferences),
|
|
249
|
+
decisions: filterByRelevance(decisions),
|
|
250
|
+
bugPatterns: filterByRelevance(bugPatterns),
|
|
251
|
+
codePatterns: filterByRelevance(codePatterns),
|
|
252
|
+
domain: filterByRelevance(domain),
|
|
253
|
+
};
|
|
254
|
+
const allLoaded = [
|
|
255
|
+
...result.preferences,
|
|
256
|
+
...result.decisions,
|
|
257
|
+
...result.bugPatterns,
|
|
258
|
+
...result.codePatterns,
|
|
259
|
+
...result.domain,
|
|
260
|
+
];
|
|
261
|
+
const formatted = this.formatSessionContext(result);
|
|
262
|
+
return {
|
|
263
|
+
...result,
|
|
264
|
+
count: allLoaded.length,
|
|
265
|
+
formatted,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// ── Knowledge Graph Operations (RAG 2.0) ──────────────────────────────
|
|
269
|
+
/** Add a typed edge between two knowledge nodes. */
|
|
270
|
+
async addEdge(edge) {
|
|
271
|
+
const id = generateId();
|
|
272
|
+
const now = new Date().toISOString();
|
|
273
|
+
const edgeData = {
|
|
274
|
+
id,
|
|
275
|
+
sourceId: edge.sourceId,
|
|
276
|
+
targetId: edge.targetId,
|
|
277
|
+
type: edge.type,
|
|
278
|
+
weight: edge.weight ?? 1.0,
|
|
279
|
+
reason: edge.reason || '',
|
|
280
|
+
metadata: {},
|
|
281
|
+
created_at: now,
|
|
282
|
+
last_traversed: null,
|
|
283
|
+
traversal_count: 0,
|
|
284
|
+
deprecated: false,
|
|
285
|
+
};
|
|
286
|
+
const checksum = computeChecksum(edgeData);
|
|
287
|
+
const fullEdge = { ...edgeData, checksum };
|
|
288
|
+
appendJsonl(this.edgesPath, fullEdge);
|
|
289
|
+
return id;
|
|
290
|
+
}
|
|
291
|
+
/** Get all edges for a specific node. */
|
|
292
|
+
async getEdges(nodeId, opts) {
|
|
293
|
+
const edges = this.readAllEdges();
|
|
294
|
+
const direction = opts?.direction || 'both';
|
|
295
|
+
const edgeTypes = opts?.edgeTypes;
|
|
296
|
+
let filtered = edges.filter(e => !e.deprecated);
|
|
297
|
+
if (direction === 'outgoing') {
|
|
298
|
+
filtered = filtered.filter(e => e.sourceId === nodeId);
|
|
299
|
+
}
|
|
300
|
+
else if (direction === 'incoming') {
|
|
301
|
+
filtered = filtered.filter(e => e.targetId === nodeId);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
filtered = filtered.filter(e => e.sourceId === nodeId || e.targetId === nodeId);
|
|
305
|
+
}
|
|
306
|
+
if (edgeTypes && edgeTypes.length > 0) {
|
|
307
|
+
filtered = filtered.filter(e => edgeTypes.includes(e.type));
|
|
308
|
+
}
|
|
309
|
+
return filtered;
|
|
310
|
+
}
|
|
311
|
+
/** BFS traversal from a starting node. */
|
|
312
|
+
async traverse(startId, maxDepth, opts) {
|
|
313
|
+
const edges = this.readAllEdges().filter(e => !e.deprecated);
|
|
314
|
+
const depth = maxDepth ?? 3;
|
|
315
|
+
const edgeTypes = opts?.edgeTypes;
|
|
316
|
+
const minWeight = opts?.minWeight ?? 0;
|
|
317
|
+
// Build adjacency map
|
|
318
|
+
const adjacency = new Map();
|
|
319
|
+
for (const edge of edges) {
|
|
320
|
+
if (edgeTypes && !edgeTypes.includes(edge.type))
|
|
321
|
+
continue;
|
|
322
|
+
if (edge.weight < minWeight)
|
|
323
|
+
continue;
|
|
324
|
+
if (!adjacency.has(edge.sourceId))
|
|
325
|
+
adjacency.set(edge.sourceId, []);
|
|
326
|
+
adjacency.get(edge.sourceId).push({ target: edge.targetId, edge });
|
|
327
|
+
// For undirected traversal of RELATED_TO edges
|
|
328
|
+
if (edge.type === 'RELATED_TO') {
|
|
329
|
+
if (!adjacency.has(edge.targetId))
|
|
330
|
+
adjacency.set(edge.targetId, []);
|
|
331
|
+
adjacency.get(edge.targetId).push({ target: edge.sourceId, edge });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// BFS
|
|
335
|
+
const results = [];
|
|
336
|
+
const visited = new Set([startId]);
|
|
337
|
+
const queue = [
|
|
338
|
+
{ id: startId, depth: 0, path: [startId] },
|
|
339
|
+
];
|
|
340
|
+
while (queue.length > 0) {
|
|
341
|
+
const current = queue.shift();
|
|
342
|
+
if (current.depth > 0) {
|
|
343
|
+
results.push({
|
|
344
|
+
id: current.id,
|
|
345
|
+
depth: current.depth,
|
|
346
|
+
path: current.path,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (current.depth >= depth)
|
|
350
|
+
continue;
|
|
351
|
+
const neighbors = adjacency.get(current.id) || [];
|
|
352
|
+
for (const { target } of neighbors) {
|
|
353
|
+
if (!visited.has(target)) {
|
|
354
|
+
visited.add(target);
|
|
355
|
+
queue.push({
|
|
356
|
+
id: target,
|
|
357
|
+
depth: current.depth + 1,
|
|
358
|
+
path: [...current.path, target],
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return results;
|
|
364
|
+
}
|
|
365
|
+
/** Find related knowledge via keyword-based scoring + graph traversal. */
|
|
366
|
+
async findRelated(queryText, opts) {
|
|
367
|
+
const maxHops = opts?.maxHops ?? 2;
|
|
368
|
+
const topK = opts?.topK ?? 10;
|
|
369
|
+
const entries = this.readAllEntries(true).filter(e => !e.deprecated && e.confidence >= 0.3);
|
|
370
|
+
// Keyword-based scoring (TF-IDF approximation)
|
|
371
|
+
const queryWords = queryText
|
|
372
|
+
.toLowerCase()
|
|
373
|
+
.split(/\s+/)
|
|
374
|
+
.filter(w => w.length > 3);
|
|
375
|
+
const scored = entries.map(entry => {
|
|
376
|
+
const text = `${entry.topic} ${entry.content} ${(entry.tags || []).join(' ')}`.toLowerCase();
|
|
377
|
+
const matches = queryWords.filter(w => text.includes(w)).length;
|
|
378
|
+
const score = matches / Math.max(queryWords.length, 1);
|
|
379
|
+
return { id: entry.id, score, source: entry.source };
|
|
380
|
+
});
|
|
381
|
+
// Get top seed results
|
|
382
|
+
const seeds = scored
|
|
383
|
+
.filter(s => s.score > 0)
|
|
384
|
+
.sort((a, b) => b.score - a.score)
|
|
385
|
+
.slice(0, 5);
|
|
386
|
+
// Expand via graph traversal
|
|
387
|
+
const expanded = new Map();
|
|
388
|
+
for (const seed of seeds) {
|
|
389
|
+
expanded.set(seed.id, { score: seed.score, source: seed.source });
|
|
390
|
+
}
|
|
391
|
+
if (maxHops > 0 && seeds.length > 0) {
|
|
392
|
+
for (const seed of seeds) {
|
|
393
|
+
const traversed = await this.traverse(seed.id, maxHops);
|
|
394
|
+
for (const node of traversed) {
|
|
395
|
+
const existing = expanded.get(node.id);
|
|
396
|
+
const decayedScore = seed.score * Math.pow(0.5, node.depth);
|
|
397
|
+
if (!existing || existing.score < decayedScore) {
|
|
398
|
+
const entry = entries.find(e => e.id === node.id);
|
|
399
|
+
expanded.set(node.id, {
|
|
400
|
+
score: decayedScore,
|
|
401
|
+
source: entry?.source || 'graph',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return [...expanded.entries()]
|
|
408
|
+
.map(([id, data]) => ({ id, ...data }))
|
|
409
|
+
.sort((a, b) => b.score - a.score)
|
|
410
|
+
.slice(0, topK);
|
|
411
|
+
}
|
|
412
|
+
/** Generate auto-shadow context for a task. */
|
|
413
|
+
async autoShadow(opts) {
|
|
414
|
+
const { taskDescription, excludeIds = [], techStack = [] } = opts;
|
|
415
|
+
if (!taskDescription || taskDescription.length < 10) {
|
|
416
|
+
return { formatted: '', items: [], count: 0, budgetUsed: 0 };
|
|
417
|
+
}
|
|
418
|
+
const entries = this.readAllEntries(true).filter(e => !e.deprecated && e.confidence >= 0.3 && !excludeIds.includes(e.id));
|
|
419
|
+
// Score entries against task description
|
|
420
|
+
const queryWords = taskDescription
|
|
421
|
+
.toLowerCase()
|
|
422
|
+
.split(/\s+/)
|
|
423
|
+
.filter(w => w.length > 3);
|
|
424
|
+
const scored = entries
|
|
425
|
+
.map(entry => {
|
|
426
|
+
const text = `${entry.topic} ${entry.content} ${(entry.tags || []).join(' ')}`.toLowerCase();
|
|
427
|
+
let score = 0;
|
|
428
|
+
// Keyword matching
|
|
429
|
+
const matches = queryWords.filter(w => text.includes(w)).length;
|
|
430
|
+
score += (matches / Math.max(queryWords.length, 1)) * 0.6;
|
|
431
|
+
// Tech stack boost
|
|
432
|
+
if (techStack.length > 0 && entry.tech_stack) {
|
|
433
|
+
const stackOverlap = techStack.filter(t => entry.tech_stack.some(et => et.toLowerCase() === t.toLowerCase())).length;
|
|
434
|
+
score += (stackOverlap / techStack.length) * 0.2;
|
|
435
|
+
}
|
|
436
|
+
// Confidence factor
|
|
437
|
+
score += entry.confidence * 0.2;
|
|
438
|
+
return {
|
|
439
|
+
id: entry.id,
|
|
440
|
+
type: entry.type,
|
|
441
|
+
topic: entry.topic,
|
|
442
|
+
content: entry.content,
|
|
443
|
+
confidence: entry.confidence,
|
|
444
|
+
score,
|
|
445
|
+
source: entry.source,
|
|
446
|
+
tags: entry.tags,
|
|
447
|
+
};
|
|
448
|
+
})
|
|
449
|
+
.filter(item => item.score >= MIN_SHADOW_SCORE)
|
|
450
|
+
.sort((a, b) => b.score - a.score)
|
|
451
|
+
.slice(0, MAX_SHADOW_ITEMS);
|
|
452
|
+
// Format within budget
|
|
453
|
+
let budgetUsed = 0;
|
|
454
|
+
const items = [];
|
|
455
|
+
const lines = ['### Auto-Shadow Context'];
|
|
456
|
+
for (const item of scored) {
|
|
457
|
+
const line = `- [${item.type}] ${item.topic}: ${item.content.slice(0, 150)}`;
|
|
458
|
+
if (budgetUsed + line.length > MAX_SHADOW_CHARS)
|
|
459
|
+
break;
|
|
460
|
+
lines.push(line);
|
|
461
|
+
items.push(item);
|
|
462
|
+
budgetUsed += line.length;
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
formatted: items.length > 0 ? lines.join('\n') : '',
|
|
466
|
+
items,
|
|
467
|
+
count: items.length,
|
|
468
|
+
budgetUsed,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/** Get graph statistics. */
|
|
472
|
+
async getGraphStats() {
|
|
473
|
+
const edges = this.readAllEdges().filter(e => !e.deprecated);
|
|
474
|
+
const entries = this.readAllEntries(false);
|
|
475
|
+
const nodeIds = new Set(entries.map(e => e.id));
|
|
476
|
+
const connectedNodes = new Set();
|
|
477
|
+
const edgesByType = {};
|
|
478
|
+
let totalWeight = 0;
|
|
479
|
+
for (const edge of edges) {
|
|
480
|
+
connectedNodes.add(edge.sourceId);
|
|
481
|
+
connectedNodes.add(edge.targetId);
|
|
482
|
+
edgesByType[edge.type] = (edgesByType[edge.type] || 0) + 1;
|
|
483
|
+
totalWeight += edge.weight;
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
total_nodes: nodeIds.size,
|
|
487
|
+
total_edges: edges.length,
|
|
488
|
+
edges_by_type: edgesByType,
|
|
489
|
+
orphan_nodes: nodeIds.size - connectedNodes.size,
|
|
490
|
+
avg_weight: edges.length > 0 ? totalWeight / edges.length : 0,
|
|
491
|
+
connected_ratio: nodeIds.size > 0 ? connectedNodes.size / nodeIds.size : 0,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** Apply weight decay to stale edges. */
|
|
495
|
+
async decayEdges() {
|
|
496
|
+
const edges = this.readAllEdges();
|
|
497
|
+
const now = Date.now();
|
|
498
|
+
let decayed = 0;
|
|
499
|
+
let pruned = 0;
|
|
500
|
+
for (const edge of edges) {
|
|
501
|
+
if (edge.deprecated)
|
|
502
|
+
continue;
|
|
503
|
+
const lastActivity = edge.last_traversed || edge.created_at;
|
|
504
|
+
const daysSince = (now - new Date(lastActivity).getTime()) / 86400000;
|
|
505
|
+
if (daysSince > DECAY_THRESHOLD_DAYS) {
|
|
506
|
+
const newWeight = edge.weight * (1 - DECAY_RATE);
|
|
507
|
+
if (newWeight < 0.1) {
|
|
508
|
+
// Prune by deprecating
|
|
509
|
+
const prunedEdge = { ...edge, deprecated: true };
|
|
510
|
+
appendJsonl(this.edgesPath, prunedEdge);
|
|
511
|
+
pruned++;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
const decayedEdge = { ...edge, weight: newWeight };
|
|
515
|
+
appendJsonl(this.edgesPath, decayedEdge);
|
|
516
|
+
decayed++;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return { decayed, pruned };
|
|
521
|
+
}
|
|
522
|
+
/** Detect cycles in directed edge types. */
|
|
523
|
+
async detectCycles() {
|
|
524
|
+
const edges = this.readAllEdges().filter(e => !e.deprecated && e.type !== 'RELATED_TO');
|
|
525
|
+
// Build directed adjacency
|
|
526
|
+
const adjacency = new Map();
|
|
527
|
+
const allNodes = new Set();
|
|
528
|
+
for (const edge of edges) {
|
|
529
|
+
allNodes.add(edge.sourceId);
|
|
530
|
+
allNodes.add(edge.targetId);
|
|
531
|
+
if (!adjacency.has(edge.sourceId))
|
|
532
|
+
adjacency.set(edge.sourceId, []);
|
|
533
|
+
adjacency.get(edge.sourceId).push(edge.targetId);
|
|
534
|
+
}
|
|
535
|
+
// DFS-based cycle detection
|
|
536
|
+
const cycles = [];
|
|
537
|
+
const visited = new Set();
|
|
538
|
+
const inStack = new Set();
|
|
539
|
+
const dfs = (node, pathArr) => {
|
|
540
|
+
visited.add(node);
|
|
541
|
+
inStack.add(node);
|
|
542
|
+
const neighbors = adjacency.get(node) || [];
|
|
543
|
+
for (const neighbor of neighbors) {
|
|
544
|
+
if (inStack.has(neighbor)) {
|
|
545
|
+
// Found a cycle
|
|
546
|
+
const cycleStart = pathArr.indexOf(neighbor);
|
|
547
|
+
if (cycleStart >= 0) {
|
|
548
|
+
cycles.push([...pathArr.slice(cycleStart), neighbor]);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else if (!visited.has(neighbor)) {
|
|
552
|
+
dfs(neighbor, [...pathArr, neighbor]);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
inStack.delete(node);
|
|
556
|
+
};
|
|
557
|
+
for (const node of allNodes) {
|
|
558
|
+
if (!visited.has(node)) {
|
|
559
|
+
dfs(node, [node]);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return cycles;
|
|
563
|
+
}
|
|
564
|
+
/** Verify SHA-256 integrity of all edges. */
|
|
565
|
+
async verifyIntegrity() {
|
|
566
|
+
const edges = this.readAllEdges();
|
|
567
|
+
let valid = 0;
|
|
568
|
+
const corrupted = [];
|
|
569
|
+
for (const edge of edges) {
|
|
570
|
+
const { checksum, ...rest } = edge;
|
|
571
|
+
const expected = computeChecksum(rest);
|
|
572
|
+
if (expected === checksum) {
|
|
573
|
+
valid++;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
corrupted.push(edge.id);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { valid, corrupted };
|
|
580
|
+
}
|
|
581
|
+
// ── Private Helpers ─────────────────────────────────────────────────────
|
|
582
|
+
readAllEntries(includeGlobal) {
|
|
583
|
+
let entries = readJsonlFile(this.kbPath);
|
|
584
|
+
if (includeGlobal && fs.existsSync(this.globalPath)) {
|
|
585
|
+
const globalEntries = readJsonlFile(this.globalPath).map(e => ({ ...e, global: true }));
|
|
586
|
+
entries = [...entries, ...globalEntries];
|
|
587
|
+
}
|
|
588
|
+
return entries;
|
|
589
|
+
}
|
|
590
|
+
readAllEdges() {
|
|
591
|
+
return readJsonlFile(this.edgesPath);
|
|
592
|
+
}
|
|
593
|
+
formatSessionContext(ctx) {
|
|
594
|
+
const sections = [];
|
|
595
|
+
if (ctx.preferences.length > 0) {
|
|
596
|
+
sections.push('### Team Preferences');
|
|
597
|
+
for (const e of ctx.preferences) {
|
|
598
|
+
sections.push(`- [${(e.confidence * 100).toFixed(0)}% confidence] ${e.topic}: ${e.content.slice(0, 200)}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (ctx.decisions.length > 0) {
|
|
602
|
+
sections.push('\n### Architectural Decisions');
|
|
603
|
+
for (const e of ctx.decisions) {
|
|
604
|
+
sections.push(`- ${e.topic}: ${e.content.slice(0, 200)}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (ctx.bugPatterns.length > 0) {
|
|
608
|
+
sections.push('\n### Known Bug Patterns');
|
|
609
|
+
for (const e of ctx.bugPatterns) {
|
|
610
|
+
sections.push(`- ${e.topic}: ${e.content.slice(0, 200)}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (ctx.codePatterns.length > 0) {
|
|
614
|
+
sections.push('\n### Code Patterns');
|
|
615
|
+
for (const e of ctx.codePatterns) {
|
|
616
|
+
sections.push(`- ${e.topic}: ${e.content.slice(0, 200)}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (ctx.domain.length > 0) {
|
|
620
|
+
sections.push('\n### Domain Knowledge');
|
|
621
|
+
for (const e of ctx.domain) {
|
|
622
|
+
sections.push(`- ${e.topic}: ${e.content.slice(0, 200)}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return sections.join('\n');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
exports.MindForgeMemory = MindForgeMemory;
|