persyst-mcp 1.0.1 → 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/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/ so data persists across sessions
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 LIKE ? LIMIT 1'
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
- return Number(result.lastInsertRowid);
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) boostMemory(id);
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
- return stmts.getById.get(id) || null;
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 and its vector embedding.
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
- return stmts.getRecent.all(limit);
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
- return stmts.getImportant.all(limit);
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, file, concept, etc.).
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
- * Delete a memory and clean up its edges.
624
+ * Archive a memory and log the contradiction.
453
625
  */
454
- export function deleteMemoryFull(id) {
455
- stmts.deleteEdgesByMemory.run(id, id);
456
- deleteVec(id);
457
- const result = stmts.deleteMemory.run(id);
458
- return result.changes > 0;
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
- * Useful for giving coding agents context about a project's history.
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
- const fullText = body
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
- commits.push({ hash, message: subject, author, date, fullText });
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
+ }