persyst-mcp 1.0.0 → 1.1.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/LICENSE +21 -0
- package/README.md +85 -62
- package/index.js +4 -1
- package/package.json +16 -2
- package/src/attestation.js +207 -0
- package/src/database.js +322 -28
- package/src/git.js +72 -11
- package/src/search.js +269 -49
- package/src/server.js +19 -5
- package/src/tools.js +362 -96
package/src/database.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - Opens SQLite connection at ~/.persyst/persyst.db
|
|
6
6
|
* - Loads the sqlite-vec extension for vector search
|
|
7
7
|
* - Creates all tables (memories, FTS5 index, vector index)
|
|
8
|
+
* - Runs schema migrations for production-grade bi-temporal model
|
|
8
9
|
* - Exports simple CRUD functions for other modules to use
|
|
9
10
|
*
|
|
10
11
|
* IMPORTANT: better-sqlite3 is SYNCHRONOUS. No async/await here.
|
|
@@ -18,7 +19,7 @@ import { mkdirSync } from 'fs';
|
|
|
18
19
|
|
|
19
20
|
// ============================================================
|
|
20
21
|
// DATABASE LOCATION
|
|
21
|
-
// Store in ~/.persyst/
|
|
22
|
+
// Store in ~/.persyst/ per default to persist across sessions
|
|
22
23
|
// ============================================================
|
|
23
24
|
|
|
24
25
|
const DB_DIR = join(homedir(), '.persyst');
|
|
@@ -39,7 +40,7 @@ sqliteVec.load(db);
|
|
|
39
40
|
console.error(`[persyst] Database: ${DB_PATH}`);
|
|
40
41
|
|
|
41
42
|
// ============================================================
|
|
42
|
-
// CREATE TABLES
|
|
43
|
+
// CREATE TABLES & SCHEMA MIGRATIONS
|
|
43
44
|
// ============================================================
|
|
44
45
|
|
|
45
46
|
// --- Main memories table ---
|
|
@@ -50,7 +51,74 @@ db.exec(`
|
|
|
50
51
|
importance_score REAL DEFAULT 1.0,
|
|
51
52
|
created_at INTEGER DEFAULT (unixepoch()),
|
|
52
53
|
last_accessed INTEGER DEFAULT (unixepoch()),
|
|
53
|
-
access_count INTEGER DEFAULT 0
|
|
54
|
+
access_count INTEGER DEFAULT 0,
|
|
55
|
+
valid_from INTEGER DEFAULT (unixepoch()),
|
|
56
|
+
valid_until INTEGER DEFAULT NULL,
|
|
57
|
+
assertion_time INTEGER DEFAULT (unixepoch())
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
// --- Migrations for bi-temporal validity on existing tables ---
|
|
62
|
+
try {
|
|
63
|
+
db.exec('ALTER TABLE memories ADD COLUMN valid_from INTEGER DEFAULT (unixepoch())');
|
|
64
|
+
} catch (e) { /* Column already exists */ }
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
db.exec('ALTER TABLE memories ADD COLUMN valid_until INTEGER DEFAULT NULL');
|
|
68
|
+
} catch (e) { /* Column already exists */ }
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
db.exec('ALTER TABLE memories ADD COLUMN assertion_time INTEGER DEFAULT (unixepoch())');
|
|
72
|
+
} catch (e) { /* Column already exists */ }
|
|
73
|
+
|
|
74
|
+
// --- Contradictions table ---
|
|
75
|
+
db.exec(`
|
|
76
|
+
CREATE TABLE IF NOT EXISTS contradictions (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
old_memory_id INTEGER NOT NULL,
|
|
79
|
+
new_memory_id INTEGER NOT NULL,
|
|
80
|
+
resolved_at INTEGER DEFAULT (unixepoch()),
|
|
81
|
+
resolution_reason TEXT
|
|
82
|
+
)
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
// --- Provenance table ---
|
|
86
|
+
db.exec(`
|
|
87
|
+
CREATE TABLE IF NOT EXISTS provenance (
|
|
88
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
89
|
+
memory_id INTEGER NOT NULL,
|
|
90
|
+
source_type TEXT NOT NULL, -- agent | git | manual | api
|
|
91
|
+
source_id TEXT, -- agent name or git hash
|
|
92
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
93
|
+
confidence REAL NOT NULL
|
|
94
|
+
)
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
// --- Agent Stats table ---
|
|
98
|
+
db.exec(`
|
|
99
|
+
CREATE TABLE IF NOT EXISTS agent_stats (
|
|
100
|
+
agent_id TEXT PRIMARY KEY,
|
|
101
|
+
memories_created INTEGER DEFAULT 0,
|
|
102
|
+
memories_confirmed INTEGER DEFAULT 0,
|
|
103
|
+
memories_contradicted INTEGER DEFAULT 0,
|
|
104
|
+
reputation_score REAL DEFAULT 1.0,
|
|
105
|
+
last_active INTEGER DEFAULT (unixepoch())
|
|
106
|
+
)
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
// --- Attestations table ---
|
|
110
|
+
db.exec(`
|
|
111
|
+
CREATE TABLE IF NOT EXISTS attestations (
|
|
112
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
113
|
+
attestation_id TEXT NOT NULL UNIQUE,
|
|
114
|
+
query TEXT NOT NULL,
|
|
115
|
+
timestamp TEXT NOT NULL,
|
|
116
|
+
memories_retrieved TEXT NOT NULL,
|
|
117
|
+
agent_id TEXT,
|
|
118
|
+
session_id TEXT,
|
|
119
|
+
signature TEXT NOT NULL,
|
|
120
|
+
previous_hash TEXT,
|
|
121
|
+
hash TEXT NOT NULL
|
|
54
122
|
)
|
|
55
123
|
`);
|
|
56
124
|
|
|
@@ -64,9 +132,6 @@ db.exec(`
|
|
|
64
132
|
`);
|
|
65
133
|
|
|
66
134
|
// --- FTS5 auto-sync triggers ---
|
|
67
|
-
// These keep the FTS index in sync when memories are added/updated/deleted.
|
|
68
|
-
// Using try/catch because "CREATE TRIGGER IF NOT EXISTS" isn't supported.
|
|
69
|
-
|
|
70
135
|
try {
|
|
71
136
|
db.exec(`
|
|
72
137
|
CREATE TRIGGER memories_fts_insert AFTER INSERT ON memories
|
|
@@ -106,7 +171,6 @@ db.exec(`
|
|
|
106
171
|
`);
|
|
107
172
|
|
|
108
173
|
// --- Knowledge Graph: entities + edges ---
|
|
109
|
-
// Entities are the "nouns" — people, files, tech, concepts
|
|
110
174
|
db.exec(`
|
|
111
175
|
CREATE TABLE IF NOT EXISTS entities (
|
|
112
176
|
id INTEGER PRIMARY KEY,
|
|
@@ -116,7 +180,6 @@ db.exec(`
|
|
|
116
180
|
)
|
|
117
181
|
`);
|
|
118
182
|
|
|
119
|
-
// Edges connect entities to memories (or entities to entities)
|
|
120
183
|
db.exec(`
|
|
121
184
|
CREATE TABLE IF NOT EXISTS edges (
|
|
122
185
|
id INTEGER PRIMARY KEY,
|
|
@@ -144,22 +207,71 @@ const stmts = {
|
|
|
144
207
|
insertVec: db.prepare(
|
|
145
208
|
'INSERT INTO memories_vec (rowid, embedding) VALUES (?, ?)'
|
|
146
209
|
),
|
|
210
|
+
insertProvenance: db.prepare(
|
|
211
|
+
'INSERT INTO provenance (memory_id, source_type, source_id, confidence) VALUES (?, ?, ?, ?)'
|
|
212
|
+
),
|
|
213
|
+
insertContradiction: db.prepare(
|
|
214
|
+
'INSERT INTO contradictions (old_memory_id, new_memory_id, resolution_reason) VALUES (?, ?, ?)'
|
|
215
|
+
),
|
|
216
|
+
upsertAgent: db.prepare(`
|
|
217
|
+
INSERT INTO agent_stats (agent_id) VALUES (?)
|
|
218
|
+
ON CONFLICT(agent_id) DO UPDATE SET last_active = unixepoch()
|
|
219
|
+
`),
|
|
220
|
+
incrementCreated: db.prepare(
|
|
221
|
+
'UPDATE agent_stats SET memories_created = memories_created + 1 WHERE agent_id = ?'
|
|
222
|
+
),
|
|
223
|
+
incrementConfirmed: db.prepare(
|
|
224
|
+
'UPDATE agent_stats SET memories_confirmed = memories_confirmed + 1 WHERE agent_id = ?'
|
|
225
|
+
),
|
|
226
|
+
incrementContradicted: db.prepare(
|
|
227
|
+
'UPDATE agent_stats SET memories_contradicted = memories_contradicted + 1 WHERE agent_id = ?'
|
|
228
|
+
),
|
|
229
|
+
recalculateReputation: db.prepare(
|
|
230
|
+
'UPDATE agent_stats SET reputation_score = (memories_confirmed + 1.0) / (memories_contradicted + 1.0) WHERE agent_id = ?'
|
|
231
|
+
),
|
|
232
|
+
insertAttestation: db.prepare(`
|
|
233
|
+
INSERT INTO attestations (
|
|
234
|
+
attestation_id, query, timestamp, memories_retrieved,
|
|
235
|
+
agent_id, session_id, signature, previous_hash, hash
|
|
236
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
237
|
+
`),
|
|
147
238
|
|
|
148
239
|
// -- Read --
|
|
149
240
|
getById: db.prepare(
|
|
241
|
+
'SELECT * FROM memories WHERE id = ? AND valid_until IS NULL'
|
|
242
|
+
),
|
|
243
|
+
getAnyById: db.prepare(
|
|
150
244
|
'SELECT * FROM memories WHERE id = ?'
|
|
151
245
|
),
|
|
152
246
|
getRecent: db.prepare(
|
|
153
|
-
'SELECT * FROM memories ORDER BY created_at DESC LIMIT ?'
|
|
247
|
+
'SELECT * FROM memories WHERE valid_until IS NULL ORDER BY created_at DESC LIMIT ?'
|
|
154
248
|
),
|
|
155
249
|
getImportant: db.prepare(
|
|
156
|
-
'SELECT * FROM memories ORDER BY importance_score DESC LIMIT ?'
|
|
250
|
+
'SELECT * FROM memories WHERE valid_until IS NULL ORDER BY importance_score DESC LIMIT ?'
|
|
251
|
+
),
|
|
252
|
+
getProvenance: db.prepare(
|
|
253
|
+
'SELECT * FROM provenance WHERE memory_id = ?'
|
|
254
|
+
),
|
|
255
|
+
getAllAgentStats: db.prepare(
|
|
256
|
+
'SELECT * FROM agent_stats ORDER BY reputation_score DESC'
|
|
257
|
+
),
|
|
258
|
+
getAttestation: db.prepare(
|
|
259
|
+
'SELECT * FROM attestations WHERE attestation_id = ?'
|
|
260
|
+
),
|
|
261
|
+
getLastAttestation: db.prepare(
|
|
262
|
+
'SELECT * FROM attestations ORDER BY id DESC LIMIT 1'
|
|
263
|
+
),
|
|
264
|
+
getAttestationsByDate: db.prepare(
|
|
265
|
+
'SELECT * FROM attestations WHERE timestamp >= ? AND timestamp <= ? ORDER BY id ASC'
|
|
157
266
|
),
|
|
158
267
|
|
|
159
268
|
// -- Update --
|
|
160
269
|
updateContent: db.prepare(
|
|
161
270
|
'UPDATE memories SET content = ? WHERE id = ?'
|
|
162
271
|
),
|
|
272
|
+
archiveMemory: db.prepare(
|
|
273
|
+
'UPDATE memories SET valid_until = unixepoch() WHERE id = ?'
|
|
274
|
+
),
|
|
163
275
|
|
|
164
276
|
// -- Delete --
|
|
165
277
|
deleteMemory: db.prepare(
|
|
@@ -233,7 +345,7 @@ const stmts = {
|
|
|
233
345
|
|
|
234
346
|
// -- Dedup --
|
|
235
347
|
findMemoryByContent: db.prepare(
|
|
236
|
-
'SELECT id FROM memories WHERE content
|
|
348
|
+
'SELECT id FROM memories WHERE content = ? AND valid_until IS NULL LIMIT 1'
|
|
237
349
|
)
|
|
238
350
|
};
|
|
239
351
|
|
|
@@ -243,13 +355,26 @@ const stmts = {
|
|
|
243
355
|
// ============================================================
|
|
244
356
|
|
|
245
357
|
/**
|
|
246
|
-
* Insert a new memory into the memories table.
|
|
247
|
-
* FTS5 index is auto-updated via trigger.
|
|
358
|
+
* Insert a new memory into the memories table and log its provenance.
|
|
248
359
|
* @returns {number} The new memory's ID
|
|
249
360
|
*/
|
|
250
|
-
export function insertMemory(content, importance = 1.0) {
|
|
361
|
+
export function insertMemory(content, importance = 1.0, provenanceInfo = null) {
|
|
251
362
|
const result = stmts.insertMemory.run(content, importance);
|
|
252
|
-
|
|
363
|
+
const id = Number(result.lastInsertRowid);
|
|
364
|
+
|
|
365
|
+
// Provenance Info handling
|
|
366
|
+
const source_type = provenanceInfo?.source_type || 'manual';
|
|
367
|
+
const source_id = provenanceInfo?.source_id || null;
|
|
368
|
+
const confidence = provenanceInfo?.confidence !== undefined ? provenanceInfo.confidence : 1.0;
|
|
369
|
+
|
|
370
|
+
stmts.insertProvenance.run(id, source_type, source_id, confidence);
|
|
371
|
+
|
|
372
|
+
// Agent Stats handling
|
|
373
|
+
if (source_type === 'agent' && source_id) {
|
|
374
|
+
incrementAgentStat(source_id, 'created');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return id;
|
|
253
378
|
}
|
|
254
379
|
|
|
255
380
|
/**
|
|
@@ -258,7 +383,6 @@ export function insertMemory(content, importance = 1.0) {
|
|
|
258
383
|
* @param {Float32Array} embedding - 384-dim embedding vector
|
|
259
384
|
*/
|
|
260
385
|
export function insertVector(id, embedding) {
|
|
261
|
-
// better-sqlite3 needs Buffer, sqlite-vec needs BigInt for rowid
|
|
262
386
|
stmts.insertVec.run(BigInt(id), Buffer.from(embedding.buffer));
|
|
263
387
|
}
|
|
264
388
|
|
|
@@ -268,7 +392,24 @@ export function insertVector(id, embedding) {
|
|
|
268
392
|
*/
|
|
269
393
|
export function getMemory(id) {
|
|
270
394
|
const memory = stmts.getById.get(id);
|
|
271
|
-
if (memory)
|
|
395
|
+
if (memory) {
|
|
396
|
+
boostMemory(id);
|
|
397
|
+
// Fetch and link provenance info
|
|
398
|
+
const prov = getProvenance(id);
|
|
399
|
+
memory.provenance = prov;
|
|
400
|
+
}
|
|
401
|
+
return memory || null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get a memory by ID WITHOUT boosting or checking bi-temporal validity.
|
|
406
|
+
* @returns {object|null} The memory row, or null if not found
|
|
407
|
+
*/
|
|
408
|
+
export function getAnyMemoryById(id) {
|
|
409
|
+
const memory = stmts.getAnyById.get(id);
|
|
410
|
+
if (memory) {
|
|
411
|
+
memory.provenance = getProvenance(id);
|
|
412
|
+
}
|
|
272
413
|
return memory || null;
|
|
273
414
|
}
|
|
274
415
|
|
|
@@ -277,7 +418,11 @@ export function getMemory(id) {
|
|
|
277
418
|
* @returns {object|null} The memory row, or null if not found
|
|
278
419
|
*/
|
|
279
420
|
export function getMemoryById(id) {
|
|
280
|
-
|
|
421
|
+
const memory = stmts.getById.get(id);
|
|
422
|
+
if (memory) {
|
|
423
|
+
memory.provenance = getProvenance(id);
|
|
424
|
+
}
|
|
425
|
+
return memory || null;
|
|
281
426
|
}
|
|
282
427
|
|
|
283
428
|
/**
|
|
@@ -298,11 +443,12 @@ export function deleteVec(id) {
|
|
|
298
443
|
}
|
|
299
444
|
|
|
300
445
|
/**
|
|
301
|
-
* Delete a memory
|
|
446
|
+
* Delete a memory, its vector embedding, and all associated graph edges.
|
|
302
447
|
* FTS5 index auto-updates via trigger.
|
|
303
448
|
* @returns {boolean} true if the memory existed and was deleted
|
|
304
449
|
*/
|
|
305
450
|
export function deleteMemory(id) {
|
|
451
|
+
stmts.deleteEdgesByMemory.run(id, id);
|
|
306
452
|
deleteVec(id); // Remove vector first (no cascades on virtual tables)
|
|
307
453
|
const result = stmts.deleteMemory.run(id);
|
|
308
454
|
return result.changes > 0;
|
|
@@ -312,14 +458,22 @@ export function deleteMemory(id) {
|
|
|
312
458
|
* Get the N most recently created memories.
|
|
313
459
|
*/
|
|
314
460
|
export function getRecentMemories(limit = 10) {
|
|
315
|
-
|
|
461
|
+
const rows = stmts.getRecent.all(limit);
|
|
462
|
+
rows.forEach(r => {
|
|
463
|
+
r.provenance = getProvenance(r.id);
|
|
464
|
+
});
|
|
465
|
+
return rows;
|
|
316
466
|
}
|
|
317
467
|
|
|
318
468
|
/**
|
|
319
469
|
* Get the N most important memories (by importance_score).
|
|
320
470
|
*/
|
|
321
471
|
export function getImportantMemories(limit = 10) {
|
|
322
|
-
|
|
472
|
+
const rows = stmts.getImportant.all(limit);
|
|
473
|
+
rows.forEach(r => {
|
|
474
|
+
r.provenance = getProvenance(r.id);
|
|
475
|
+
});
|
|
476
|
+
return rows;
|
|
323
477
|
}
|
|
324
478
|
|
|
325
479
|
// ============================================================
|
|
@@ -379,7 +533,7 @@ export function searchVector(embedding, limit = 10) {
|
|
|
379
533
|
// ============================================================
|
|
380
534
|
|
|
381
535
|
/**
|
|
382
|
-
* Create a named entity (person, tech,
|
|
536
|
+
* Create a named entity (person, tech, project, concept, file).
|
|
383
537
|
* Silently skips if entity with that name already exists.
|
|
384
538
|
* @returns {number|null} The entity ID, or null if already existed
|
|
385
539
|
*/
|
|
@@ -448,14 +602,154 @@ export function memoryExists(pattern) {
|
|
|
448
602
|
return stmts.findMemoryByContent.get(pattern) !== undefined;
|
|
449
603
|
}
|
|
450
604
|
|
|
605
|
+
// ============================================================
|
|
606
|
+
// DEDUPLICATION BY EXACT CONTENT
|
|
607
|
+
// ============================================================
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Find memory by exact content.
|
|
611
|
+
* @param {string} content
|
|
612
|
+
* @returns {object|null} The memory row, or null if not found
|
|
613
|
+
*/
|
|
614
|
+
export function getMemoryByContent(content) {
|
|
615
|
+
const row = stmts.findMemoryByContent.get(content);
|
|
616
|
+
return row ? getMemoryById(row.id) : null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ============================================================
|
|
620
|
+
// TEMPORAL CONTRADICTIONS & AGENT STATS & ATTESTATIONS CRUD
|
|
621
|
+
// ============================================================
|
|
622
|
+
|
|
451
623
|
/**
|
|
452
|
-
*
|
|
624
|
+
* Archive a memory and log the contradiction.
|
|
453
625
|
*/
|
|
454
|
-
export function
|
|
455
|
-
stmts.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
626
|
+
export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
|
|
627
|
+
stmts.archiveMemory.run(oldMemoryId);
|
|
628
|
+
stmts.insertContradiction.run(oldMemoryId, newMemoryId, reason);
|
|
629
|
+
|
|
630
|
+
// Track that the agent's memory was contradicted
|
|
631
|
+
const oldProvenance = getProvenance(oldMemoryId);
|
|
632
|
+
if (oldProvenance && oldProvenance.source_type === 'agent' && oldProvenance.source_id) {
|
|
633
|
+
incrementAgentStat(oldProvenance.source_id, 'contradicted');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Get provenance for a memory.
|
|
639
|
+
*/
|
|
640
|
+
export function getProvenance(memoryId) {
|
|
641
|
+
return stmts.getProvenance.get(memoryId) || null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Update agent reputation counters.
|
|
646
|
+
*/
|
|
647
|
+
export function incrementAgentStat(agentId, action) {
|
|
648
|
+
stmts.upsertAgent.run(agentId);
|
|
649
|
+
if (action === 'created') {
|
|
650
|
+
stmts.incrementCreated.run(agentId);
|
|
651
|
+
} else if (action === 'confirmed') {
|
|
652
|
+
stmts.incrementConfirmed.run(agentId);
|
|
653
|
+
} else if (action === 'contradicted') {
|
|
654
|
+
stmts.incrementContradicted.run(agentId);
|
|
655
|
+
}
|
|
656
|
+
stmts.recalculateReputation.run(agentId);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Get all agent stats.
|
|
661
|
+
*/
|
|
662
|
+
export function getAllAgentStats() {
|
|
663
|
+
return stmts.getAllAgentStats.all();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Upsert agent signature / record attestation in database.
|
|
668
|
+
*/
|
|
669
|
+
export function insertAttestation(att) {
|
|
670
|
+
stmts.insertAttestation.run(
|
|
671
|
+
att.attestation_id,
|
|
672
|
+
att.query,
|
|
673
|
+
att.timestamp,
|
|
674
|
+
JSON.stringify(att.memories_retrieved),
|
|
675
|
+
att.agent_id || null,
|
|
676
|
+
att.session_id || null,
|
|
677
|
+
att.signature,
|
|
678
|
+
att.previous_hash || null,
|
|
679
|
+
att.hash
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Retrieve a specific attestation by ID.
|
|
685
|
+
*/
|
|
686
|
+
export function getAttestationById(attestationId) {
|
|
687
|
+
return stmts.getAttestation.get(attestationId) || null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Retrieve the last attestation logged for chaining.
|
|
692
|
+
*/
|
|
693
|
+
export function getLastAttestation() {
|
|
694
|
+
return stmts.getLastAttestation.get() || null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Retrieve attestations within a timestamp range.
|
|
699
|
+
*/
|
|
700
|
+
export function getAttestationsByDateRange(startDate, endDate) {
|
|
701
|
+
return stmts.getAttestationsByDate.all(startDate, endDate);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Traverses contradictions to get historical versions of a memory.
|
|
706
|
+
*/
|
|
707
|
+
export function getMemoryHistoryChain(memoryId) {
|
|
708
|
+
const versions = new Set();
|
|
709
|
+
const queue = [memoryId];
|
|
710
|
+
|
|
711
|
+
while (queue.length > 0) {
|
|
712
|
+
const currentId = queue.shift();
|
|
713
|
+
if (versions.has(currentId)) continue;
|
|
714
|
+
versions.add(currentId);
|
|
715
|
+
|
|
716
|
+
// Find ancestors (replaced by current)
|
|
717
|
+
const ancestors = db.prepare('SELECT old_memory_id FROM contradictions WHERE new_memory_id = ?').all(currentId);
|
|
718
|
+
ancestors.forEach(a => {
|
|
719
|
+
if (!versions.has(a.old_memory_id)) queue.push(a.old_memory_id);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Find descendants (replaces current)
|
|
723
|
+
const descendants = db.prepare('SELECT new_memory_id FROM contradictions WHERE old_memory_id = ?').all(currentId);
|
|
724
|
+
descendants.forEach(d => {
|
|
725
|
+
if (!versions.has(d.new_memory_id)) queue.push(d.new_memory_id);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const ids = Array.from(versions);
|
|
730
|
+
if (ids.length === 0) return [];
|
|
731
|
+
|
|
732
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
733
|
+
const rows = db.prepare(`
|
|
734
|
+
SELECT m.*, p.source_type, p.source_id, p.confidence
|
|
735
|
+
FROM memories m
|
|
736
|
+
LEFT JOIN provenance p ON m.id = p.memory_id
|
|
737
|
+
WHERE m.id IN (${placeholders})
|
|
738
|
+
ORDER BY m.created_at ASC
|
|
739
|
+
`).all(...ids);
|
|
740
|
+
|
|
741
|
+
return rows;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Search all memories FTS (including archived memories).
|
|
746
|
+
*/
|
|
747
|
+
export function searchAllMemoriesFts(queryText, limit = 10) {
|
|
748
|
+
try {
|
|
749
|
+
return stmts.searchFts.all(queryText, limit);
|
|
750
|
+
} catch (e) {
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
459
753
|
}
|
|
460
754
|
|
|
461
755
|
// ============================================================
|
package/src/git.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* git.js — Git Commit Ingestion
|
|
2
|
+
* git.js — Git Commit Ingestion & Analysis
|
|
3
3
|
*
|
|
4
4
|
* Reads git log from a repository and converts commits into memories.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Each commit becomes a memory like:
|
|
8
|
-
* "[abc1234] Fix login bug — by John on 2024-01-15"
|
|
9
|
-
*
|
|
10
|
-
* Deduplicates by commit hash so you can ingest safely multiple times.
|
|
5
|
+
* Performs commit categorization, file diff analysis, and imports notes.
|
|
11
6
|
*/
|
|
12
7
|
|
|
13
8
|
import { execSync } from 'child_process';
|
|
@@ -17,7 +12,7 @@ import { execSync } from 'child_process';
|
|
|
17
12
|
*
|
|
18
13
|
* @param {string} repoPath - Absolute path to the git repo
|
|
19
14
|
* @param {number} count - Number of commits to read (default: 20)
|
|
20
|
-
* @returns {Array<{hash: string, message: string, author: string, date: string, fullText: string}>}
|
|
15
|
+
* @returns {Array<{hash: string, message: string, author: string, date: string, fullText: string, files: string[], importance: number}>}
|
|
21
16
|
*/
|
|
22
17
|
export function getRecentCommits(repoPath, count = 20) {
|
|
23
18
|
try {
|
|
@@ -49,17 +44,37 @@ export function getRecentCommits(repoPath, count = 20) {
|
|
|
49
44
|
const subject = lines[3].trim();
|
|
50
45
|
const body = lines.slice(4).join(' ').trim();
|
|
51
46
|
|
|
47
|
+
// Fetch git notes if available (represents PR metadata)
|
|
48
|
+
const notes = getGitNotes(repoPath, hash);
|
|
49
|
+
|
|
52
50
|
// Build a readable memory string
|
|
53
|
-
|
|
51
|
+
let fullText = body
|
|
54
52
|
? `[${hash.slice(0, 7)}] ${subject} — by ${author} on ${date}. ${body}`
|
|
55
53
|
: `[${hash.slice(0, 7)}] ${subject} — by ${author} on ${date}`;
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
if (notes) {
|
|
56
|
+
fullText += ` [PR Notes] ${notes}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fetch files touched
|
|
60
|
+
const files = getCommitFiles(repoPath, hash);
|
|
61
|
+
|
|
62
|
+
// Classify importance based on message
|
|
63
|
+
const classification = classifyCommit(subject);
|
|
64
|
+
|
|
65
|
+
commits.push({
|
|
66
|
+
hash,
|
|
67
|
+
message: subject,
|
|
68
|
+
author,
|
|
69
|
+
date,
|
|
70
|
+
fullText,
|
|
71
|
+
files,
|
|
72
|
+
importance: classification.importance
|
|
73
|
+
});
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
return commits;
|
|
61
77
|
} catch (err) {
|
|
62
|
-
// Not a git repo, or git not installed
|
|
63
78
|
const message = err.message || String(err);
|
|
64
79
|
if (message.includes('not a git repository')) {
|
|
65
80
|
throw new Error(`Not a git repository: ${repoPath}`);
|
|
@@ -95,3 +110,49 @@ export function getCommitFiles(repoPath, hash) {
|
|
|
95
110
|
return [];
|
|
96
111
|
}
|
|
97
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fetch git notes (representing PR metadata or additional annotations).
|
|
116
|
+
*/
|
|
117
|
+
export function getGitNotes(repoPath, hash) {
|
|
118
|
+
try {
|
|
119
|
+
const output = execSync(
|
|
120
|
+
`git notes show ${hash}`,
|
|
121
|
+
{
|
|
122
|
+
cwd: repoPath,
|
|
123
|
+
encoding: 'utf-8',
|
|
124
|
+
timeout: 3000,
|
|
125
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
return output.trim();
|
|
129
|
+
} catch {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Categorize commit and assign importance.
|
|
136
|
+
*/
|
|
137
|
+
export function classifyCommit(subject) {
|
|
138
|
+
const s = subject.toLowerCase().trim();
|
|
139
|
+
if (
|
|
140
|
+
s.startsWith('feat:') ||
|
|
141
|
+
s.startsWith('fix:') ||
|
|
142
|
+
s.startsWith('refactor:') ||
|
|
143
|
+
s.startsWith('breaking:') ||
|
|
144
|
+
s.startsWith('decision:')
|
|
145
|
+
) {
|
|
146
|
+
return { type: 'architectural', importance: 0.9 };
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
s.startsWith('chore:') ||
|
|
150
|
+
s.startsWith('docs:') ||
|
|
151
|
+
s.startsWith('test:') ||
|
|
152
|
+
s.startsWith('style:') ||
|
|
153
|
+
s.startsWith('ci:')
|
|
154
|
+
) {
|
|
155
|
+
return { type: 'chore', importance: 0.4 };
|
|
156
|
+
}
|
|
157
|
+
return { type: 'other', importance: 0.6 };
|
|
158
|
+
}
|