mindforge-cc 2.3.5 → 3.0.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/.agent/skills/mindforge-plan-phase/SKILL.md +1 -0
- package/.agent/skills/mindforge-system-architecture/SKILL.md +136 -0
- package/.agent/skills/mindforge-system-architecture/examples.md +120 -0
- package/.agent/skills/mindforge-system-architecture/scaling-checklist.md +76 -0
- package/.agent/skills/mindforge-tdd/SKILL.md +112 -0
- package/.agent/skills/mindforge-tdd/deep-modules.md +21 -0
- package/.agent/skills/mindforge-tdd/interface-design.md +22 -0
- package/.agent/skills/mindforge-tdd/mocking.md +24 -0
- package/.agent/skills/mindforge-tdd/refactoring.md +21 -0
- package/.agent/skills/mindforge-tdd/tests.md +28 -0
- package/.agent/workflows/mindforge-plan-phase.md +30 -1
- package/.agent/workflows/mindforge:architecture.md +40 -0
- package/.agent/workflows/mindforge:executor.md +18 -0
- package/.agent/workflows/mindforge:identity.md +18 -0
- package/.agent/workflows/mindforge:memory.md +18 -0
- package/.agent/workflows/mindforge:planner.md +18 -0
- package/.agent/workflows/mindforge:researcher.md +18 -0
- package/.agent/workflows/mindforge:reviewer.md +18 -0
- package/.agent/workflows/mindforge:tdd.md +41 -0
- package/.agent/workflows/mindforge:tool.md +18 -0
- package/.mindforge/engine/ads-protocol.md +54 -0
- package/.mindforge/engine/compaction-protocol.md +21 -36
- package/.mindforge/engine/context-injector.md +26 -0
- package/.mindforge/engine/knowledge-graph-protocol.md +125 -0
- package/.mindforge/engine/shard-controller.md +53 -0
- package/.mindforge/engine/temporal-protocol.md +40 -0
- package/.mindforge/personas/mf-executor.md +40 -0
- package/.mindforge/personas/mf-memory.md +33 -0
- package/.mindforge/personas/mf-planner.md +45 -0
- package/.mindforge/personas/mf-researcher.md +39 -0
- package/.mindforge/personas/mf-reviewer.md +35 -0
- package/.mindforge/personas/mf-tool.md +33 -0
- package/.planning/AUDIT.jsonl +1 -0
- package/.planning/TEMPORAL-TEST.md +1 -0
- package/.planning/history/36525e1d9da1b674/ARCHITECTURE.md +0 -0
- package/.planning/history/36525e1d9da1b674/HANDOFF.json +8 -0
- package/.planning/history/36525e1d9da1b674/PROJECT.md +33 -0
- package/.planning/history/36525e1d9da1b674/RELEASE-CHECKLIST.md +68 -0
- package/.planning/history/36525e1d9da1b674/REQUIREMENTS.md +0 -0
- package/.planning/history/36525e1d9da1b674/ROADMAP.md +12 -0
- package/.planning/history/36525e1d9da1b674/SNAPSHOT-META.json +18 -0
- package/.planning/history/36525e1d9da1b674/STATE.md +31 -0
- package/.planning/history/36525e1d9da1b674/TEMPORAL-TEST.md +1 -0
- package/.planning/history/36525e1d9da1b674/jira-sync.json +5 -0
- package/.planning/history/36525e1d9da1b674/slack-threads.json +3 -0
- package/.planning/history/test-audit-001/ARCHITECTURE.md +0 -0
- package/.planning/history/test-audit-001/HANDOFF.json +8 -0
- package/.planning/history/test-audit-001/PROJECT.md +33 -0
- package/.planning/history/test-audit-001/RELEASE-CHECKLIST.md +68 -0
- package/.planning/history/test-audit-001/REQUIREMENTS.md +0 -0
- package/.planning/history/test-audit-001/ROADMAP.md +12 -0
- package/.planning/history/test-audit-001/SNAPSHOT-META.json +17 -0
- package/.planning/history/test-audit-001/STATE.md +31 -0
- package/.planning/history/test-audit-001/TEMPORAL-TEST.md +1 -0
- package/.planning/history/test-audit-001/jira-sync.json +5 -0
- package/.planning/history/test-audit-001/slack-threads.json +3 -0
- package/CHANGELOG.md +101 -0
- package/README.md +57 -23
- package/bin/autonomous/auto-runner.js +23 -0
- package/bin/dashboard/server.js +2 -0
- package/bin/dashboard/temporal-api.js +82 -0
- package/bin/engine/temporal-cli.js +52 -0
- package/bin/engine/temporal-hub.js +138 -0
- package/bin/hindsight-injector.js +59 -0
- package/bin/memory/auto-shadow.js +274 -0
- package/bin/memory/embedding-engine.js +326 -0
- package/bin/memory/knowledge-capture.js +122 -5
- package/bin/memory/knowledge-graph.js +572 -0
- package/bin/memory/knowledge-store.js +15 -3
- package/bin/mindforge-cli.js +19 -0
- package/bin/models/model-router.js +1 -0
- package/bin/review/ads-engine.js +126 -0
- package/bin/review/ads-synthesizer.js +117 -0
- package/bin/shard-helper.js +134 -0
- package/bin/spawn-agent.js +61 -0
- package/docs/PERSONAS.md +71 -5
- package/docs/adr/ADR-042-ads-protocol.md +30 -0
- package/docs/architecture/README.md +55 -0
- package/docs/architecture/V3-CORE.md +52 -0
- package/docs/commands-reference.md +3 -2
- package/docs/usp-features.md +33 -15
- package/package.json +1 -1
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindForge v2.4.0 — Knowledge Graph Engine (RAG 2.0)
|
|
3
|
+
* Graph-aware knowledge management with nodes, edges, and traversal.
|
|
4
|
+
*
|
|
5
|
+
* Design:
|
|
6
|
+
* - Nodes = KnowledgeEntry items from knowledge-store.js
|
|
7
|
+
* - Edges = Typed relationships stored in graph-edges.jsonl
|
|
8
|
+
* - Adjacency index rebuilt on load for O(1) neighbor lookups
|
|
9
|
+
* - Traversal via BFS with configurable depth and edge type filters
|
|
10
|
+
* - All edge writes are append-only with SHA-256 integrity checksums
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const Store = require('./knowledge-store');
|
|
18
|
+
const Embedder = require('./embedding-engine');
|
|
19
|
+
|
|
20
|
+
// ── Edge Types ────────────────────────────────────────────────────────────────
|
|
21
|
+
const EDGE_TYPES = Object.freeze({
|
|
22
|
+
RELATED_TO: 'RELATED_TO', // Semantic similarity (auto-inferred)
|
|
23
|
+
CAUSED_BY: 'CAUSED_BY', // Bug → Root cause
|
|
24
|
+
SUPERSEDES: 'SUPERSEDES', // New decision → Old decision
|
|
25
|
+
DEPENDS_ON: 'DEPENDS_ON', // Pattern → Required pattern
|
|
26
|
+
INFORMS: 'INFORMS', // Review finding → Decision
|
|
27
|
+
CONTRADICTS: 'CONTRADICTS', // Conflicting knowledge
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const EDGE_SCHEMA_VERSION = '1.0.0';
|
|
31
|
+
const DEFAULT_EDGE_WEIGHT = 1.0;
|
|
32
|
+
const DECAY_RATE = 0.10; // 10% weight decay per cycle
|
|
33
|
+
const DECAY_THRESHOLD_DAYS = 30; // Decay edges not traversed in 30 days
|
|
34
|
+
|
|
35
|
+
// ── Path Configuration ────────────────────────────────────────────────────────
|
|
36
|
+
let baseDir = process.cwd();
|
|
37
|
+
let testMemoryDir = null;
|
|
38
|
+
|
|
39
|
+
function setBaseDir(dir) { baseDir = dir; }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set a flat memory directory for testing (bypasses .mindforge/memory/ nesting).
|
|
43
|
+
* @param {string|null} dir - Flat directory path, or null to reset
|
|
44
|
+
*/
|
|
45
|
+
function setTestMode(dir) { testMemoryDir = dir; }
|
|
46
|
+
|
|
47
|
+
function getPaths() {
|
|
48
|
+
const memoryDir = testMemoryDir || path.join(baseDir, '.mindforge', 'memory');
|
|
49
|
+
return {
|
|
50
|
+
MEMORY_DIR: memoryDir,
|
|
51
|
+
EDGES_PATH: path.join(memoryDir, 'graph-edges.jsonl'),
|
|
52
|
+
CACHE_PATH: path.join(memoryDir, 'embeddings.json'),
|
|
53
|
+
GRAPH_STATS: path.join(memoryDir, 'graph-stats.json'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureDir(dir) {
|
|
58
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Edge CRUD ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add a directed edge between two knowledge nodes.
|
|
65
|
+
* @param {object} edge
|
|
66
|
+
* @param {string} edge.sourceId - Source node ID
|
|
67
|
+
* @param {string} edge.targetId - Target node ID
|
|
68
|
+
* @param {string} edge.type - Edge type (from EDGE_TYPES)
|
|
69
|
+
* @param {number} [edge.weight] - Edge weight (default: 1.0)
|
|
70
|
+
* @param {string} [edge.reason] - Why this edge exists
|
|
71
|
+
* @param {object} [edge.metadata] - Additional metadata
|
|
72
|
+
* @returns {string} Edge ID
|
|
73
|
+
*/
|
|
74
|
+
function addEdge(edge) {
|
|
75
|
+
const paths = getPaths();
|
|
76
|
+
ensureDir(paths.MEMORY_DIR);
|
|
77
|
+
|
|
78
|
+
if (!edge.sourceId) throw new Error('Edge requires sourceId');
|
|
79
|
+
if (!edge.targetId) throw new Error('Edge requires targetId');
|
|
80
|
+
if (!edge.type || !EDGE_TYPES[edge.type]) {
|
|
81
|
+
throw new Error(`Invalid edge type: ${edge.type}. Must be one of: ${Object.keys(EDGE_TYPES).join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
if (edge.sourceId === edge.targetId) {
|
|
84
|
+
throw new Error('Self-referencing edges are not allowed');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const id = crypto.randomUUID();
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
|
|
90
|
+
const record = {
|
|
91
|
+
id,
|
|
92
|
+
schema_version: EDGE_SCHEMA_VERSION,
|
|
93
|
+
sourceId: edge.sourceId,
|
|
94
|
+
targetId: edge.targetId,
|
|
95
|
+
type: edge.type,
|
|
96
|
+
weight: Math.min(2.0, Math.max(0.0, edge.weight ?? DEFAULT_EDGE_WEIGHT)),
|
|
97
|
+
reason: edge.reason || '',
|
|
98
|
+
metadata: edge.metadata || {},
|
|
99
|
+
created_at: now,
|
|
100
|
+
last_traversed: null,
|
|
101
|
+
traversal_count: 0,
|
|
102
|
+
deprecated: false,
|
|
103
|
+
checksum: '',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Compute integrity checksum
|
|
107
|
+
const payload = JSON.stringify({ ...record, checksum: '' });
|
|
108
|
+
record.checksum = crypto.createHash('sha256').update(payload).digest('hex');
|
|
109
|
+
|
|
110
|
+
fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(record) + '\n');
|
|
111
|
+
return id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read all edges from the graph-edges.jsonl file.
|
|
116
|
+
* Later entries with the same ID supersede earlier ones (append-only pattern).
|
|
117
|
+
* @returns {object[]} Edges
|
|
118
|
+
*/
|
|
119
|
+
function readAllEdges() {
|
|
120
|
+
const paths = getPaths();
|
|
121
|
+
if (!fs.existsSync(paths.EDGES_PATH)) return [];
|
|
122
|
+
|
|
123
|
+
const lines = fs.readFileSync(paths.EDGES_PATH, 'utf8').split('\n').filter(Boolean);
|
|
124
|
+
const byId = new Map();
|
|
125
|
+
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
try {
|
|
128
|
+
const edge = JSON.parse(line);
|
|
129
|
+
byId.set(edge.id, edge);
|
|
130
|
+
} catch {
|
|
131
|
+
// Skip malformed lines
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return [...byId.values()].filter(e => !e.deprecated);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Deprecate an edge.
|
|
140
|
+
* @param {string} edgeId - Edge to deprecate
|
|
141
|
+
* @param {string} reason - Why
|
|
142
|
+
*/
|
|
143
|
+
function deprecateEdge(edgeId, reason) {
|
|
144
|
+
const paths = getPaths();
|
|
145
|
+
const edges = readAllEdges();
|
|
146
|
+
const edge = edges.find(e => e.id === edgeId);
|
|
147
|
+
if (!edge) return;
|
|
148
|
+
|
|
149
|
+
const deprecated = {
|
|
150
|
+
...edge,
|
|
151
|
+
deprecated: true,
|
|
152
|
+
deprecated_reason: reason,
|
|
153
|
+
deprecated_at: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(deprecated) + '\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Reinforce an edge (increase weight, update traversal stats).
|
|
161
|
+
* @param {string} edgeId
|
|
162
|
+
*/
|
|
163
|
+
function reinforceEdge(edgeId) {
|
|
164
|
+
const paths = getPaths();
|
|
165
|
+
const edges = readAllEdges();
|
|
166
|
+
const edge = edges.find(e => e.id === edgeId);
|
|
167
|
+
if (!edge) return;
|
|
168
|
+
|
|
169
|
+
const reinforced = {
|
|
170
|
+
...edge,
|
|
171
|
+
weight: Math.min(2.0, edge.weight + 0.1),
|
|
172
|
+
last_traversed: new Date().toISOString(),
|
|
173
|
+
traversal_count: (edge.traversal_count || 0) + 1,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Recompute checksum
|
|
177
|
+
const payload = JSON.stringify({ ...reinforced, checksum: '' });
|
|
178
|
+
reinforced.checksum = crypto.createHash('sha256').update(payload).digest('hex');
|
|
179
|
+
|
|
180
|
+
fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(reinforced) + '\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Adjacency Index ───────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build an in-memory adjacency index for O(1) neighbor lookups.
|
|
187
|
+
* @param {object[]} edges - All active edges
|
|
188
|
+
* @returns {Map<string, object[]>} nodeId → [{ edge, neighborId }]
|
|
189
|
+
*/
|
|
190
|
+
function buildAdjacencyIndex(edges) {
|
|
191
|
+
const index = new Map();
|
|
192
|
+
|
|
193
|
+
for (const edge of edges) {
|
|
194
|
+
// Forward direction
|
|
195
|
+
if (!index.has(edge.sourceId)) index.set(edge.sourceId, []);
|
|
196
|
+
index.get(edge.sourceId).push({
|
|
197
|
+
edge,
|
|
198
|
+
neighborId: edge.targetId,
|
|
199
|
+
direction: 'outgoing',
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Reverse direction (for bidirectional traversal)
|
|
203
|
+
if (!index.has(edge.targetId)) index.set(edge.targetId, []);
|
|
204
|
+
index.get(edge.targetId).push({
|
|
205
|
+
edge,
|
|
206
|
+
neighborId: edge.sourceId,
|
|
207
|
+
direction: 'incoming',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return index;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Graph Traversal ───────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* BFS traversal from a node, returning all reachable nodes within depth.
|
|
218
|
+
* @param {string} startId - Starting node ID
|
|
219
|
+
* @param {number} maxDepth - Maximum hop count (default: 2)
|
|
220
|
+
* @param {object} opts
|
|
221
|
+
* @param {string[]} [opts.edgeTypes] - Filter by edge types
|
|
222
|
+
* @param {number} [opts.minWeight] - Minimum edge weight
|
|
223
|
+
* @returns {Array<{id: string, depth: number, path: string[]}>}
|
|
224
|
+
*/
|
|
225
|
+
function traverse(startId, maxDepth = 2, opts = {}) {
|
|
226
|
+
const { edgeTypes, minWeight = 0 } = opts;
|
|
227
|
+
const edges = readAllEdges();
|
|
228
|
+
const adjacency = buildAdjacencyIndex(edges);
|
|
229
|
+
|
|
230
|
+
const visited = new Set();
|
|
231
|
+
const results = [];
|
|
232
|
+
const queue = [{ id: startId, depth: 0, path: [startId] }];
|
|
233
|
+
|
|
234
|
+
while (queue.length > 0) {
|
|
235
|
+
const { id, depth, path: currentPath } = queue.shift();
|
|
236
|
+
|
|
237
|
+
if (visited.has(id)) continue;
|
|
238
|
+
visited.add(id);
|
|
239
|
+
|
|
240
|
+
if (id !== startId) {
|
|
241
|
+
results.push({ id, depth, path: currentPath });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (depth >= maxDepth) continue;
|
|
245
|
+
|
|
246
|
+
const neighbors = adjacency.get(id) || [];
|
|
247
|
+
for (const { edge, neighborId } of neighbors) {
|
|
248
|
+
if (visited.has(neighborId)) continue;
|
|
249
|
+
if (edgeTypes && !edgeTypes.includes(edge.type)) continue;
|
|
250
|
+
if (edge.weight < minWeight) continue;
|
|
251
|
+
|
|
252
|
+
queue.push({
|
|
253
|
+
id: neighborId,
|
|
254
|
+
depth: depth + 1,
|
|
255
|
+
path: [...currentPath, neighborId],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return results;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Find related nodes via both graph traversal AND embedding similarity.
|
|
265
|
+
* Returns a combined, deduplicated, scored result set.
|
|
266
|
+
* @param {string} queryText - Natural language query
|
|
267
|
+
* @param {Map<string, object>} vectors - Precomputed embeddings
|
|
268
|
+
* @param {Map<string, number>} df - Document frequency
|
|
269
|
+
* @param {number} N - Corpus size
|
|
270
|
+
* @param {object} opts
|
|
271
|
+
* @param {number} [opts.maxHops] - Graph traversal depth
|
|
272
|
+
* @param {number} [opts.topK] - Max results
|
|
273
|
+
* @returns {Array<{id: string, score: number, source: string}>}
|
|
274
|
+
*/
|
|
275
|
+
function findRelated(queryText, vectors, df, N, opts = {}) {
|
|
276
|
+
const { maxHops = 2, topK = 10 } = opts;
|
|
277
|
+
|
|
278
|
+
// 1. Embedding-based similarity
|
|
279
|
+
const embeddingResults = Embedder.findSimilar(queryText, vectors, df, N, topK * 2);
|
|
280
|
+
|
|
281
|
+
// 2. Graph-based traversal from top embedding matches
|
|
282
|
+
const graphResults = new Map();
|
|
283
|
+
for (const { id } of embeddingResults.slice(0, 3)) {
|
|
284
|
+
const neighbors = traverse(id, maxHops);
|
|
285
|
+
for (const n of neighbors) {
|
|
286
|
+
if (!graphResults.has(n.id) || graphResults.get(n.id).depth > n.depth) {
|
|
287
|
+
graphResults.set(n.id, n);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 3. Merge and score
|
|
293
|
+
const combined = new Map();
|
|
294
|
+
|
|
295
|
+
// Embedding scores (0.0 - 1.0)
|
|
296
|
+
for (const r of embeddingResults) {
|
|
297
|
+
combined.set(r.id, {
|
|
298
|
+
id: r.id,
|
|
299
|
+
embeddingScore: r.similarity,
|
|
300
|
+
graphScore: 0,
|
|
301
|
+
source: 'embedding',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Graph scores (inversely proportional to depth)
|
|
306
|
+
for (const [id, result] of graphResults) {
|
|
307
|
+
const graphScore = 1.0 / (result.depth + 1);
|
|
308
|
+
if (combined.has(id)) {
|
|
309
|
+
const existing = combined.get(id);
|
|
310
|
+
existing.graphScore = graphScore;
|
|
311
|
+
existing.source = 'hybrid';
|
|
312
|
+
} else {
|
|
313
|
+
combined.set(id, {
|
|
314
|
+
id,
|
|
315
|
+
embeddingScore: 0,
|
|
316
|
+
graphScore,
|
|
317
|
+
source: 'graph',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Final score: weighted combination
|
|
323
|
+
const scored = [...combined.values()].map(r => ({
|
|
324
|
+
id: r.id,
|
|
325
|
+
score: r.embeddingScore * 0.6 + r.graphScore * 0.4,
|
|
326
|
+
source: r.source,
|
|
327
|
+
}));
|
|
328
|
+
|
|
329
|
+
return scored
|
|
330
|
+
.filter(r => r.score > 0)
|
|
331
|
+
.sort((a, b) => b.score - a.score)
|
|
332
|
+
.slice(0, topK);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get all edges for a specific node.
|
|
337
|
+
* @param {string} nodeId
|
|
338
|
+
* @param {object} opts
|
|
339
|
+
* @param {string} [opts.direction] - 'outgoing', 'incoming', or 'both' (default)
|
|
340
|
+
* @param {string[]} [opts.edgeTypes] - Filter by types
|
|
341
|
+
* @returns {object[]} Edges
|
|
342
|
+
*/
|
|
343
|
+
function getNodeEdges(nodeId, opts = {}) {
|
|
344
|
+
const { direction = 'both', edgeTypes } = opts;
|
|
345
|
+
const edges = readAllEdges();
|
|
346
|
+
|
|
347
|
+
return edges.filter(e => {
|
|
348
|
+
const matchesNode =
|
|
349
|
+
direction === 'outgoing' ? e.sourceId === nodeId :
|
|
350
|
+
direction === 'incoming' ? e.targetId === nodeId :
|
|
351
|
+
e.sourceId === nodeId || e.targetId === nodeId;
|
|
352
|
+
|
|
353
|
+
const matchesType = !edgeTypes || edgeTypes.includes(e.type);
|
|
354
|
+
|
|
355
|
+
return matchesNode && matchesType;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Auto-Edge Creation ────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* When a new entry is added, auto-create RELATED_TO edges
|
|
363
|
+
* for entries with cosine similarity above the threshold.
|
|
364
|
+
* @param {string} entryId - Newly added entry's ID
|
|
365
|
+
* @param {Map<string, object>} vectors - Precomputed entry vectors
|
|
366
|
+
* @returns {string[]} Created edge IDs
|
|
367
|
+
*/
|
|
368
|
+
function autoCreateEdges(entryId, vectors) {
|
|
369
|
+
const candidates = Embedder.inferEdges(entryId, vectors);
|
|
370
|
+
const created = [];
|
|
371
|
+
|
|
372
|
+
// Check for existing edges to avoid duplicates
|
|
373
|
+
const existingEdges = getNodeEdges(entryId);
|
|
374
|
+
const existingTargets = new Set(existingEdges.map(e =>
|
|
375
|
+
e.sourceId === entryId ? e.targetId : e.sourceId
|
|
376
|
+
));
|
|
377
|
+
|
|
378
|
+
for (const { targetId, similarity } of candidates.slice(0, 5)) {
|
|
379
|
+
if (existingTargets.has(targetId)) continue;
|
|
380
|
+
|
|
381
|
+
const edgeId = addEdge({
|
|
382
|
+
sourceId: entryId,
|
|
383
|
+
targetId,
|
|
384
|
+
type: EDGE_TYPES.RELATED_TO,
|
|
385
|
+
weight: similarity,
|
|
386
|
+
reason: `Auto-inferred: cosine similarity ${similarity.toFixed(3)}`,
|
|
387
|
+
metadata: { auto_inferred: true, similarity },
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
created.push(edgeId);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return created;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Edge Weight Decay ─────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Apply weight decay to edges not traversed in DECAY_THRESHOLD_DAYS.
|
|
400
|
+
* Edges that decay to weight ≤ 0.1 are deprecated.
|
|
401
|
+
* @returns {{ decayed: number, pruned: number }}
|
|
402
|
+
*/
|
|
403
|
+
function applyDecay() {
|
|
404
|
+
const edges = readAllEdges();
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
let decayed = 0;
|
|
407
|
+
let pruned = 0;
|
|
408
|
+
|
|
409
|
+
for (const edge of edges) {
|
|
410
|
+
const lastUsed = edge.last_traversed
|
|
411
|
+
? new Date(edge.last_traversed).getTime()
|
|
412
|
+
: new Date(edge.created_at).getTime();
|
|
413
|
+
|
|
414
|
+
const daysSince = (now - lastUsed) / 86_400_000;
|
|
415
|
+
if (daysSince < DECAY_THRESHOLD_DAYS) continue;
|
|
416
|
+
|
|
417
|
+
const cycles = Math.floor(daysSince / DECAY_THRESHOLD_DAYS);
|
|
418
|
+
const newWeight = edge.weight * Math.pow(1 - DECAY_RATE, cycles);
|
|
419
|
+
|
|
420
|
+
if (newWeight <= 0.1) {
|
|
421
|
+
deprecateEdge(edge.id, `Pruned: weight decayed to ${newWeight.toFixed(3)} after ${cycles} cycles`);
|
|
422
|
+
pruned++;
|
|
423
|
+
} else if (newWeight < edge.weight) {
|
|
424
|
+
// Append updated weight
|
|
425
|
+
const paths = getPaths();
|
|
426
|
+
const updated = {
|
|
427
|
+
...edge,
|
|
428
|
+
weight: parseFloat(newWeight.toFixed(4)),
|
|
429
|
+
};
|
|
430
|
+
const payload = JSON.stringify({ ...updated, checksum: '' });
|
|
431
|
+
updated.checksum = crypto.createHash('sha256').update(payload).digest('hex');
|
|
432
|
+
fs.appendFileSync(paths.EDGES_PATH, JSON.stringify(updated) + '\n');
|
|
433
|
+
decayed++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { decayed, pruned };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Cycle Detection ───────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Detect cycles in directed edge types (CAUSED_BY, SUPERSEDES).
|
|
444
|
+
* Uses DFS to find back-edges.
|
|
445
|
+
* @returns {Array<string[]>} List of cycles (as node ID arrays)
|
|
446
|
+
*/
|
|
447
|
+
function detectCycles() {
|
|
448
|
+
const edges = readAllEdges().filter(e =>
|
|
449
|
+
e.type === EDGE_TYPES.CAUSED_BY || e.type === EDGE_TYPES.SUPERSEDES
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const adj = new Map();
|
|
453
|
+
for (const e of edges) {
|
|
454
|
+
if (!adj.has(e.sourceId)) adj.set(e.sourceId, []);
|
|
455
|
+
adj.get(e.sourceId).push(e.targetId);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const visited = new Set();
|
|
459
|
+
const inStack = new Set();
|
|
460
|
+
const cycles = [];
|
|
461
|
+
|
|
462
|
+
function dfs(node, path) {
|
|
463
|
+
if (inStack.has(node)) {
|
|
464
|
+
const cycleStart = path.indexOf(node);
|
|
465
|
+
cycles.push(path.slice(cycleStart));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (visited.has(node)) return;
|
|
469
|
+
|
|
470
|
+
visited.add(node);
|
|
471
|
+
inStack.add(node);
|
|
472
|
+
|
|
473
|
+
for (const neighbor of (adj.get(node) || [])) {
|
|
474
|
+
dfs(neighbor, [...path, neighbor]);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
inStack.delete(node);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
for (const nodeId of adj.keys()) {
|
|
481
|
+
if (!visited.has(nodeId)) {
|
|
482
|
+
dfs(nodeId, [nodeId]);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return cycles;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── Graph Statistics ──────────────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Compute and return graph statistics.
|
|
493
|
+
* @returns {object} Graph stats
|
|
494
|
+
*/
|
|
495
|
+
function graphStats() {
|
|
496
|
+
const edges = readAllEdges();
|
|
497
|
+
const entries = Store.readAll();
|
|
498
|
+
const activeEntries = entries.filter(e => !e.deprecated);
|
|
499
|
+
|
|
500
|
+
const byType = {};
|
|
501
|
+
for (const e of edges) {
|
|
502
|
+
byType[e.type] = (byType[e.type] || 0) + 1;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Find orphan nodes (no edges)
|
|
506
|
+
const connectedNodes = new Set();
|
|
507
|
+
for (const e of edges) {
|
|
508
|
+
connectedNodes.add(e.sourceId);
|
|
509
|
+
connectedNodes.add(e.targetId);
|
|
510
|
+
}
|
|
511
|
+
const orphans = activeEntries.filter(e => !connectedNodes.has(e.id));
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
total_nodes: activeEntries.length,
|
|
515
|
+
total_edges: edges.length,
|
|
516
|
+
edges_by_type: byType,
|
|
517
|
+
orphan_nodes: orphans.length,
|
|
518
|
+
avg_weight: edges.length
|
|
519
|
+
? edges.reduce((s, e) => s + e.weight, 0) / edges.length
|
|
520
|
+
: 0,
|
|
521
|
+
connected_ratio: activeEntries.length
|
|
522
|
+
? connectedNodes.size / activeEntries.length
|
|
523
|
+
: 0,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ── Edge Integrity Verification ───────────────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Verify SHA-256 checksums on all edges.
|
|
531
|
+
* @returns {{ valid: number, corrupted: string[] }}
|
|
532
|
+
*/
|
|
533
|
+
function verifyEdgeIntegrity() {
|
|
534
|
+
const edges = readAllEdges();
|
|
535
|
+
let valid = 0;
|
|
536
|
+
const corrupted = [];
|
|
537
|
+
|
|
538
|
+
for (const edge of edges) {
|
|
539
|
+
const storedChecksum = edge.checksum;
|
|
540
|
+
const payload = JSON.stringify({ ...edge, checksum: '' });
|
|
541
|
+
const computed = crypto.createHash('sha256').update(payload).digest('hex');
|
|
542
|
+
|
|
543
|
+
if (storedChecksum === computed) {
|
|
544
|
+
valid++;
|
|
545
|
+
} else {
|
|
546
|
+
corrupted.push(edge.id);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return { valid, corrupted };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
554
|
+
module.exports = {
|
|
555
|
+
EDGE_TYPES,
|
|
556
|
+
setBaseDir,
|
|
557
|
+
setTestMode,
|
|
558
|
+
getPaths,
|
|
559
|
+
addEdge,
|
|
560
|
+
readAllEdges,
|
|
561
|
+
deprecateEdge,
|
|
562
|
+
reinforceEdge,
|
|
563
|
+
buildAdjacencyIndex,
|
|
564
|
+
traverse,
|
|
565
|
+
findRelated,
|
|
566
|
+
getNodeEdges,
|
|
567
|
+
autoCreateEdges,
|
|
568
|
+
applyDecay,
|
|
569
|
+
detectCycles,
|
|
570
|
+
graphStats,
|
|
571
|
+
verifyEdgeIntegrity,
|
|
572
|
+
};
|
|
@@ -33,9 +33,21 @@ function setGlobalDir(dir) {
|
|
|
33
33
|
globalBaseDir = dir;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Test-mode override: flat memory dir without .mindforge/ nesting
|
|
37
|
+
let testMemoryDir = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set a flat memory directory for testing (bypasses .mindforge/memory/ nesting).
|
|
41
|
+
* This avoids macOS App Sandbox EPERM on dot-prefixed directories.
|
|
42
|
+
* @param {string|null} dir - Flat directory path, or null to reset
|
|
43
|
+
*/
|
|
44
|
+
function setTestMode(dir) {
|
|
45
|
+
testMemoryDir = dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
function getPaths() {
|
|
37
|
-
const memoryDir = path.join(baseDir, '.mindforge', 'memory');
|
|
38
|
-
const globalDir = path.join(globalBaseDir, '.mindforge');
|
|
49
|
+
const memoryDir = testMemoryDir || path.join(baseDir, '.mindforge', 'memory');
|
|
50
|
+
const globalDir = testMemoryDir || path.join(globalBaseDir, '.mindforge');
|
|
39
51
|
return {
|
|
40
52
|
MEMORY_DIR: memoryDir,
|
|
41
53
|
GLOBAL_DIR: globalDir,
|
|
@@ -315,5 +327,5 @@ function stats() {
|
|
|
315
327
|
module.exports = {
|
|
316
328
|
add, deprecate, reinforce,
|
|
317
329
|
readAll, readByType, readFile, query, stats,
|
|
318
|
-
setBaseDir, setGlobalDir, getPaths,
|
|
330
|
+
setBaseDir, setGlobalDir, setTestMode, getPaths,
|
|
319
331
|
};
|
package/bin/mindforge-cli.js
CHANGED
|
@@ -80,6 +80,25 @@ const COMMANDS = {
|
|
|
80
80
|
'marketplace': {
|
|
81
81
|
script: 'bin/skills-builder/marketplace-cli.js',
|
|
82
82
|
description: 'Search and install community skills from the marketplace'
|
|
83
|
+
},
|
|
84
|
+
'spawn': {
|
|
85
|
+
script: 'bin/spawn-agent.js',
|
|
86
|
+
description: 'Spawn a persona essence (e.g., mf-planner)',
|
|
87
|
+
defaultArgs: ['spawn']
|
|
88
|
+
},
|
|
89
|
+
'identity': {
|
|
90
|
+
script: 'bin/spawn-agent.js',
|
|
91
|
+
description: 'Invoke a specialized identity from /agents/',
|
|
92
|
+
defaultArgs: ['identity']
|
|
93
|
+
},
|
|
94
|
+
'temporal': {
|
|
95
|
+
script: 'bin/engine/temporal-cli.js',
|
|
96
|
+
description: 'Manage time-travel debugging and state history'
|
|
97
|
+
},
|
|
98
|
+
'hindsight': {
|
|
99
|
+
script: 'bin/engine/temporal-cli.js',
|
|
100
|
+
description: 'Inject a fix into a past point and regenerate state',
|
|
101
|
+
defaultArgs: ['inject']
|
|
83
102
|
}
|
|
84
103
|
};
|
|
85
104
|
|