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/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;