ac-framework 1.7.0 → 1.8.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.
Files changed (87) hide show
  1. package/README.md +26 -0
  2. package/bin/postinstall.js +8 -1
  3. package/framework/.agent/skills/acfm-memory/SKILL.md +312 -0
  4. package/framework/.agent/workflows/ac-lite.md +192 -0
  5. package/framework/.agent/workflows/ac.md +40 -0
  6. package/framework/.amazonq/prompts/ac-lite.md +192 -0
  7. package/framework/.amazonq/prompts/ac.md +40 -0
  8. package/framework/.amazonq/skills/acfm-memory/SKILL.md +312 -0
  9. package/framework/.antigravity/skills/acfm-memory/SKILL.md +312 -0
  10. package/framework/.antigravity/workflows/ac-lite.md +192 -0
  11. package/framework/.antigravity/workflows/ac.md +40 -0
  12. package/framework/.augment/commands/ac-lite.md +192 -0
  13. package/framework/.augment/commands/ac.md +40 -0
  14. package/framework/.augment/skills/acfm-memory/SKILL.md +312 -0
  15. package/framework/.claude/commands/opsx/ac-lite.md +192 -0
  16. package/framework/.claude/commands/opsx/ac.md +40 -0
  17. package/framework/.claude/skills/acfm-memory/SKILL.md +312 -0
  18. package/framework/.cline/commands/opsx/ac-lite.md +192 -0
  19. package/framework/.cline/commands/opsx/ac.md +40 -0
  20. package/framework/.cline/skills/acfm-memory/SKILL.md +312 -0
  21. package/framework/.clinerules/skills/acfm-memory/SKILL.md +312 -0
  22. package/framework/.clinerules/workflows/ac-lite.md +192 -0
  23. package/framework/.clinerules/workflows/ac.md +40 -0
  24. package/framework/.codebuddy/commands/opsx/ac-lite.md +192 -0
  25. package/framework/.codebuddy/commands/opsx/ac.md +40 -0
  26. package/framework/.codebuddy/skills/acfm-memory/SKILL.md +312 -0
  27. package/framework/.codex/skills/acfm-memory/SKILL.md +312 -0
  28. package/framework/.continue/prompts/ac-lite.md +192 -0
  29. package/framework/.continue/prompts/ac.md +40 -0
  30. package/framework/.continue/skills/acfm-memory/SKILL.md +312 -0
  31. package/framework/.cospec/openspec/commands/ac-lite.md +192 -0
  32. package/framework/.cospec/openspec/commands/ac.md +40 -0
  33. package/framework/.cospec/skills/acfm-memory/SKILL.md +312 -0
  34. package/framework/.crush/commands/opsx/ac-lite.md +192 -0
  35. package/framework/.crush/commands/opsx/ac.md +40 -0
  36. package/framework/.crush/skills/acfm-memory/SKILL.md +312 -0
  37. package/framework/.cursor/commands/ac-lite.md +192 -0
  38. package/framework/.cursor/commands/ac.md +40 -0
  39. package/framework/.cursor/skills/acfm-memory/SKILL.md +312 -0
  40. package/framework/.factory/commands/ac-lite.md +192 -0
  41. package/framework/.factory/commands/ac.md +40 -0
  42. package/framework/.factory/skills/acfm-memory/SKILL.md +312 -0
  43. package/framework/.gemini/commands/opsx/ac-lite.md +192 -0
  44. package/framework/.gemini/commands/opsx/ac.md +40 -0
  45. package/framework/.gemini/skills/acfm-memory/SKILL.md +312 -0
  46. package/framework/.github/prompts/ac-lite.md +192 -0
  47. package/framework/.github/prompts/ac.md +40 -0
  48. package/framework/.github/skills/acfm-memory/SKILL.md +312 -0
  49. package/framework/.iflow/commands/ac-lite.md +192 -0
  50. package/framework/.iflow/commands/ac.md +40 -0
  51. package/framework/.iflow/skills/acfm-memory/SKILL.md +312 -0
  52. package/framework/.kilocode/skills/acfm-memory/SKILL.md +312 -0
  53. package/framework/.kilocode/workflows/ac-lite.md +192 -0
  54. package/framework/.kilocode/workflows/ac.md +40 -0
  55. package/framework/.kimi/skills/acfm-memory/SKILL.md +312 -0
  56. package/framework/.kimi/workflows/ac-lite.md +192 -0
  57. package/framework/.kimi/workflows/ac.md +40 -0
  58. package/framework/.opencode/command/ac-lite.md +192 -0
  59. package/framework/.opencode/command/ac.md +40 -0
  60. package/framework/.opencode/skills/acfm-memory/SKILL.md +312 -0
  61. package/framework/.qoder/commands/opsx/ac-lite.md +192 -0
  62. package/framework/.qoder/commands/opsx/ac.md +40 -0
  63. package/framework/.qoder/skills/acfm-memory/SKILL.md +312 -0
  64. package/framework/.qwen/commands/ac-lite.md +192 -0
  65. package/framework/.qwen/commands/ac.md +40 -0
  66. package/framework/.qwen/skills/acfm-memory/SKILL.md +312 -0
  67. package/framework/.roo/commands/ac-lite.md +192 -0
  68. package/framework/.roo/commands/ac.md +40 -0
  69. package/framework/.roo/skills/acfm-memory/SKILL.md +312 -0
  70. package/framework/.trae/skills/acfm-memory/SKILL.md +312 -0
  71. package/framework/.windsurf/skills/acfm-memory/SKILL.md +312 -0
  72. package/framework/.windsurf/workflows/ac-lite.md +192 -0
  73. package/framework/.windsurf/workflows/ac.md +40 -0
  74. package/framework/AGENTS.md +39 -0
  75. package/framework/CLAUDE.md +39 -0
  76. package/framework/GEMINI.md +39 -0
  77. package/framework/copilot-instructions.md +39 -0
  78. package/package.json +2 -1
  79. package/src/cli.js +2 -0
  80. package/src/commands/memory.js +772 -0
  81. package/src/index.js +46 -0
  82. package/src/memory/autosave.js +382 -0
  83. package/src/memory/database.js +178 -0
  84. package/src/memory/engine.js +727 -0
  85. package/src/memory/index.js +62 -0
  86. package/src/memory/utils.js +128 -0
  87. package/src/services/spec-engine.js +69 -1
@@ -0,0 +1,727 @@
1
+ /**
2
+ * engine.js — Motor de memoria autónoma
3
+ *
4
+ * CRUD operations, búsqueda FTS5, análisis de patrones,
5
+ * grafo de relaciones, y sistema de confianza.
6
+ */
7
+
8
+ import { getDatabase } from './database.js';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { redactPrivateContent } from './utils.js';
11
+
12
+ // Tipos de memoria válidos
13
+ const VALID_TYPES = [
14
+ 'architectural_decision',
15
+ 'bugfix_pattern',
16
+ 'api_pattern',
17
+ 'performance_insight',
18
+ 'security_fix',
19
+ 'refactor_technique',
20
+ 'dependency_note',
21
+ 'workaround',
22
+ 'convention',
23
+ 'context_boundary',
24
+ 'general_insight',
25
+ 'session_summary'
26
+ ];
27
+
28
+ const VALID_IMPORTANCE = ['critical', 'high', 'medium', 'low'];
29
+
30
+ /**
31
+ * Guarda una nueva memoria (con upsert por topic_key)
32
+ */
33
+ export function saveMemory(data) {
34
+ const db = getDatabase();
35
+
36
+ // Redactar contenido privado
37
+ const content = redactPrivateContent(data.content);
38
+ const codeSnippet = data.codeSnippet ? redactPrivateContent(data.codeSnippet) : null;
39
+ const solution = data.solution ? redactPrivateContent(data.solution) : null;
40
+ const errorMessage = data.errorMessage ? redactPrivateContent(data.errorMessage) : null;
41
+
42
+ // Validar tipo
43
+ const type = VALID_TYPES.includes(data.type) ? data.type : 'general_insight';
44
+
45
+ // Calcular confianza si no se proporciona
46
+ const confidence = data.confidence ?? calculateConfidence(content, data);
47
+
48
+ // Determinar importancia
49
+ const importance = data.importance ?? determineImportance(type, confidence);
50
+
51
+ // Generar topic_key si no existe
52
+ const topicKey = data.topicKey || generateTopicKey(content);
53
+
54
+ // Verificar si existe para upsert
55
+ const existing = db.prepare('SELECT id, revision_count FROM memories WHERE topic_key = ?').get(topicKey);
56
+
57
+ if (existing) {
58
+ // Update (upsert)
59
+ const stmt = db.prepare(`
60
+ UPDATE memories SET
61
+ type = ?,
62
+ content = ?,
63
+ project_path = ?,
64
+ change_name = ?,
65
+ session_id = ?,
66
+ confidence = ?,
67
+ importance = ?,
68
+ tags = ?,
69
+ related_to = ?,
70
+ shareable = ?,
71
+ author = ?,
72
+ code_snippet = ?,
73
+ error_message = ?,
74
+ solution = ?,
75
+ revision_count = ?,
76
+ is_deleted = 0
77
+ WHERE id = ?
78
+ `);
79
+
80
+ stmt.run(
81
+ type,
82
+ content,
83
+ data.projectPath || null,
84
+ data.changeName || null,
85
+ data.sessionId || null,
86
+ confidence,
87
+ importance,
88
+ data.tags ? JSON.stringify(data.tags) : null,
89
+ data.relatedTo ? JSON.stringify(data.relatedTo) : null,
90
+ data.shareable !== false ? 1 : 0,
91
+ data.author || 'unknown',
92
+ codeSnippet,
93
+ errorMessage,
94
+ solution,
95
+ existing.revision_count + 1,
96
+ existing.id
97
+ );
98
+
99
+ return { id: existing.id, operation: 'updated', revisionCount: existing.revision_count + 1 };
100
+ } else {
101
+ // Insert nuevo
102
+ const stmt = db.prepare(`
103
+ INSERT INTO memories (
104
+ topic_key, type, content, project_path, change_name, session_id,
105
+ confidence, importance, tags, related_to, shareable, author,
106
+ code_snippet, error_message, solution
107
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
108
+ `);
109
+
110
+ const result = stmt.run(
111
+ topicKey,
112
+ type,
113
+ content,
114
+ data.projectPath || null,
115
+ data.changeName || null,
116
+ data.sessionId || null,
117
+ confidence,
118
+ importance,
119
+ data.tags ? JSON.stringify(data.tags) : null,
120
+ data.relatedTo ? JSON.stringify(data.relatedTo) : null,
121
+ data.shareable !== false ? 1 : 0,
122
+ data.author || 'unknown',
123
+ codeSnippet,
124
+ errorMessage,
125
+ solution
126
+ );
127
+
128
+ return { id: result.lastInsertRowid, operation: 'created', revisionCount: 0 };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Busca memorias usando FTS5
134
+ */
135
+ export function searchMemories(query, options = {}) {
136
+ const db = getDatabase();
137
+
138
+ const {
139
+ limit = 10,
140
+ offset = 0,
141
+ type = null,
142
+ projectPath = null,
143
+ changeName = null,
144
+ importance = null,
145
+ since = null, // fecha ISO
146
+ tags = null, // array
147
+ minConfidence = 0
148
+ } = options;
149
+
150
+ let sql = `
151
+ SELECT m.* FROM memories m
152
+ JOIN memories_fts fts ON m.id = fts.rowid
153
+ WHERE m.is_deleted = 0
154
+ AND fts.memories_fts MATCH ?
155
+ AND m.confidence >= ?
156
+ `;
157
+
158
+ const params = [query, minConfidence];
159
+
160
+ if (type) {
161
+ sql += ' AND m.type = ?';
162
+ params.push(type);
163
+ }
164
+
165
+ if (projectPath) {
166
+ sql += ' AND m.project_path = ?';
167
+ params.push(projectPath);
168
+ }
169
+
170
+ if (changeName) {
171
+ sql += ' AND m.change_name = ?';
172
+ params.push(changeName);
173
+ }
174
+
175
+ if (importance) {
176
+ sql += ' AND m.importance = ?';
177
+ params.push(importance);
178
+ }
179
+
180
+ if (since) {
181
+ sql += ' AND m.created_at >= ?';
182
+ params.push(since);
183
+ }
184
+
185
+ sql += ' ORDER BY m.confidence DESC, m.created_at DESC LIMIT ? OFFSET ?';
186
+ params.push(limit, offset);
187
+
188
+ const stmt = db.prepare(sql);
189
+ const rows = stmt.all(...params);
190
+
191
+ // Marcar acceso
192
+ rows.forEach(row => {
193
+ recordAccess(row.id, options.sessionId, 'search');
194
+ });
195
+
196
+ return rows.map(formatMemory);
197
+ }
198
+
199
+ /**
200
+ * Obtiene contexto relevante para el proyecto/change actual
201
+ */
202
+ export function getContext(options = {}) {
203
+ const db = getDatabase();
204
+
205
+ const {
206
+ projectPath = null,
207
+ changeName = null,
208
+ currentTask = null, // para búsqueda semántica simple
209
+ limit = 5,
210
+ lookbackDays = 30,
211
+ sessionId = null
212
+ } = options;
213
+
214
+ // Calcular fecha de corte
215
+ const since = new Date();
216
+ since.setDate(since.getDate() - lookbackDays);
217
+ const sinceStr = since.toISOString();
218
+
219
+ let sql = `
220
+ SELECT m.* FROM memories m
221
+ WHERE m.is_deleted = 0
222
+ AND m.created_at >= ?
223
+ AND m.importance IN ('critical', 'high')
224
+ `;
225
+
226
+ const params = [sinceStr];
227
+
228
+ // Priorizar memorias del mismo proyecto
229
+ if (projectPath) {
230
+ sql += ' AND (m.project_path = ? OR m.shareable = 1)';
231
+ params.push(projectPath);
232
+ }
233
+
234
+ // Si hay change específico, incluirlo
235
+ if (changeName) {
236
+ sql += ' AND (m.change_name = ? OR m.change_name IS NULL)';
237
+ params.push(changeName);
238
+ }
239
+
240
+ sql += ` ORDER BY
241
+ CASE WHEN m.project_path = ? THEN 2 ELSE 1 END DESC,
242
+ m.access_count DESC,
243
+ m.created_at DESC
244
+ LIMIT ?`;
245
+
246
+ params.push(projectPath || '', limit);
247
+
248
+ const stmt = db.prepare(sql);
249
+ const rows = stmt.all(...params);
250
+
251
+ rows.forEach(row => {
252
+ recordAccess(row.id, sessionId, 'context');
253
+ });
254
+
255
+ return rows.map(formatMemory);
256
+ }
257
+
258
+ /**
259
+ * Obtiene timeline cronológico alrededor de una memoria
260
+ */
261
+ export function getTimeline(memoryId, options = {}) {
262
+ const db = getDatabase();
263
+
264
+ const { window = 5, projectPath = null } = options;
265
+
266
+ // Obtener la memoria base
267
+ const base = db.prepare('SELECT * FROM memories WHERE id = ?').get(memoryId);
268
+ if (!base) return null;
269
+
270
+ // Memorias anteriores
271
+ const beforeStmt = db.prepare(`
272
+ SELECT * FROM memories
273
+ WHERE is_deleted = 0
274
+ AND created_at < ?
275
+ ${projectPath ? 'AND (project_path = ? OR shareable = 1)' : ''}
276
+ ORDER BY created_at DESC
277
+ LIMIT ?
278
+ `);
279
+
280
+ const beforeParams = projectPath ? [base.created_at, projectPath, window] : [base.created_at, window];
281
+ const before = beforeStmt.all(...beforeParams).reverse();
282
+
283
+ // Memorias posteriores
284
+ const afterStmt = db.prepare(`
285
+ SELECT * FROM memories
286
+ WHERE is_deleted = 0
287
+ AND created_at > ?
288
+ ${projectPath ? 'AND (project_path = ? OR shareable = 1)' : ''}
289
+ ORDER BY created_at ASC
290
+ LIMIT ?
291
+ `);
292
+
293
+ const afterParams = projectPath ? [base.created_at, projectPath, window] : [base.created_at, window];
294
+ const after = afterStmt.all(...afterParams);
295
+
296
+ return {
297
+ base: formatMemory(base),
298
+ before: before.map(formatMemory),
299
+ after: after.map(formatMemory)
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Obtiene una memoria específica por ID
305
+ */
306
+ export function getMemory(id, sessionId = null) {
307
+ const db = getDatabase();
308
+
309
+ const row = db.prepare('SELECT * FROM memories WHERE id = ? AND is_deleted = 0').get(id);
310
+ if (!row) return null;
311
+
312
+ recordAccess(id, sessionId, 'get');
313
+ return formatMemory(row);
314
+ }
315
+
316
+ /**
317
+ * Actualiza una memoria existente
318
+ */
319
+ export function updateMemory(id, updates) {
320
+ const db = getDatabase();
321
+
322
+ const allowedFields = ['type', 'content', 'tags', 'importance', 'shareable', 'relatedTo'];
323
+ const setParts = [];
324
+ const params = [];
325
+
326
+ for (const [key, value] of Object.entries(updates)) {
327
+ if (!allowedFields.includes(key)) continue;
328
+
329
+ const dbKey = key === 'relatedTo' ? 'related_to' : key;
330
+
331
+ if (key === 'content') {
332
+ setParts.push(`${dbKey} = ?`);
333
+ params.push(redactPrivateContent(value));
334
+ } else if (key === 'tags' || key === 'relatedTo') {
335
+ setParts.push(`${dbKey} = ?`);
336
+ params.push(JSON.stringify(value));
337
+ } else {
338
+ setParts.push(`${dbKey} = ?`);
339
+ params.push(value);
340
+ }
341
+ }
342
+
343
+ if (setParts.length === 0) return null;
344
+
345
+ params.push(id);
346
+
347
+ const stmt = db.prepare(`UPDATE memories SET ${setParts.join(', ')} WHERE id = ?`);
348
+ const result = stmt.run(...params);
349
+
350
+ return result.changes > 0 ? getMemory(id) : null;
351
+ }
352
+
353
+ /**
354
+ * Soft-delete de una memoria
355
+ */
356
+ export function deleteMemory(id) {
357
+ const db = getDatabase();
358
+
359
+ const stmt = db.prepare('UPDATE memories SET is_deleted = 1 WHERE id = ?');
360
+ const result = stmt.run(id);
361
+
362
+ return result.changes > 0;
363
+ }
364
+
365
+ /**
366
+ * Gestión de sesiones
367
+ */
368
+ export function startSession(projectPath = null, changeName = null) {
369
+ const db = getDatabase();
370
+ const sessionId = randomUUID();
371
+
372
+ const stmt = db.prepare('INSERT INTO sessions (id, project_path, change_name) VALUES (?, ?, ?)');
373
+ stmt.run(sessionId, projectPath, changeName);
374
+
375
+ return sessionId;
376
+ }
377
+
378
+ export function endSession(sessionId, summary = null) {
379
+ const db = getDatabase();
380
+
381
+ const stmt = db.prepare('UPDATE sessions SET ended_at = CURRENT_TIMESTAMP, summary = ? WHERE id = ?');
382
+ stmt.run(summary, sessionId);
383
+
384
+ // Si hay summary, guardarlo como memoria
385
+ if (summary) {
386
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
387
+ if (session) {
388
+ saveMemory({
389
+ type: 'session_summary',
390
+ content: summary,
391
+ projectPath: session.project_path,
392
+ changeName: session.change_name,
393
+ sessionId,
394
+ author: 'system'
395
+ });
396
+ }
397
+ }
398
+
399
+ return true;
400
+ }
401
+
402
+ /**
403
+ * Estadísticas del sistema de memoria
404
+ */
405
+ export function getStats(options = {}) {
406
+ const db = getDatabase();
407
+ const { projectPath = null, since = null } = options;
408
+
409
+ let whereClause = 'WHERE is_deleted = 0';
410
+ const params = [];
411
+
412
+ if (projectPath) {
413
+ whereClause += ' AND project_path = ?';
414
+ params.push(projectPath);
415
+ }
416
+
417
+ if (since) {
418
+ whereClause += ' AND created_at >= ?';
419
+ params.push(since);
420
+ }
421
+
422
+ // Totales
423
+ const totalStmt = db.prepare(`SELECT COUNT(*) as count FROM memories ${whereClause}`);
424
+ const total = totalStmt.get(...params).count;
425
+
426
+ // Por tipo
427
+ const byTypeStmt = db.prepare(`
428
+ SELECT type, COUNT(*) as count FROM memories
429
+ ${whereClause}
430
+ GROUP BY type ORDER BY count DESC
431
+ `);
432
+ const byType = byTypeStmt.all(...params);
433
+
434
+ // Por importancia
435
+ const byImportanceStmt = db.prepare(`
436
+ SELECT importance, COUNT(*) as count FROM memories
437
+ ${whereClause}
438
+ GROUP BY importance ORDER BY count DESC
439
+ `);
440
+ const byImportance = byImportanceStmt.all(...params);
441
+
442
+ // Más accedidas
443
+ const mostAccessedStmt = db.prepare(`
444
+ SELECT id, content, access_count FROM memories
445
+ ${whereClause}
446
+ ORDER BY access_count DESC LIMIT 5
447
+ `);
448
+ const mostAccessed = mostAccessedStmt.all(...params);
449
+
450
+ // Patrones de error comunes
451
+ const commonErrorsStmt = db.prepare(`
452
+ SELECT error_message, COUNT(*) as count FROM memories
453
+ ${whereClause} AND error_message IS NOT NULL
454
+ GROUP BY error_message ORDER BY count DESC LIMIT 5
455
+ `);
456
+ const commonErrors = commonErrorsStmt.all(...params);
457
+
458
+ return {
459
+ total,
460
+ byType,
461
+ byImportance,
462
+ mostAccessed: mostAccessed.map(m => ({ id: m.id, preview: m.content.slice(0, 100), accessCount: m.access_count })),
463
+ commonErrors: commonErrors.map(e => ({ error: e.error_message, count: e.count }))
464
+ };
465
+ }
466
+
467
+ /**
468
+ * Encuentra patrones y clusters
469
+ */
470
+ export function findPatterns(options = {}) {
471
+ const db = getDatabase();
472
+ const { type = null, minFrequency = 2 } = options;
473
+
474
+ // Buscar topic_keys similares (simple string similarity)
475
+ let sql = `
476
+ SELECT topic_key, type, COUNT(*) as frequency,
477
+ GROUP_CONCAT(id) as memory_ids,
478
+ MAX(created_at) as last_seen
479
+ FROM memories
480
+ WHERE is_deleted = 0
481
+ `;
482
+
483
+ const params = [];
484
+
485
+ if (type) {
486
+ sql += ' AND type = ?';
487
+ params.push(type);
488
+ }
489
+
490
+ sql += ' GROUP BY topic_key HAVING frequency >= ? ORDER BY frequency DESC';
491
+ params.push(minFrequency);
492
+
493
+ const stmt = db.prepare(sql);
494
+ return stmt.all(...params);
495
+ }
496
+
497
+ /**
498
+ * Grafo de relaciones entre memorias
499
+ */
500
+ export function getConnections(memoryId, depth = 1) {
501
+ const db = getDatabase();
502
+
503
+ const visited = new Set();
504
+ const connections = [];
505
+ const queue = [{ id: memoryId, d: 0 }];
506
+
507
+ while (queue.length > 0) {
508
+ const { id, d } = queue.shift();
509
+ if (visited.has(id) || d > depth) continue;
510
+ visited.add(id);
511
+
512
+ const memory = db.prepare('SELECT id, content, related_to FROM memories WHERE id = ? AND is_deleted = 0').get(id);
513
+ if (!memory) continue;
514
+
515
+ const related = memory.related_to ? JSON.parse(memory.related_to) : [];
516
+
517
+ for (const relatedId of related) {
518
+ if (!visited.has(relatedId)) {
519
+ connections.push({ from: id, to: relatedId, depth: d + 1 });
520
+ queue.push({ id: relatedId, d: d + 1 });
521
+ }
522
+ }
523
+ }
524
+
525
+ return connections;
526
+ }
527
+
528
+ /**
529
+ * Sugerencias predictivas
530
+ */
531
+ export function anticipateNeeds(currentTask, projectPath = null) {
532
+ const db = getDatabase();
533
+
534
+ // Extraer palabras clave simples
535
+ const keywords = currentTask.toLowerCase().split(/\s+/).filter(w => w.length > 3);
536
+
537
+ if (keywords.length === 0) return [];
538
+
539
+ // Construir query FTS
540
+ const query = keywords.join(' OR ');
541
+
542
+ return searchMemories(query, {
543
+ projectPath,
544
+ limit: 5,
545
+ importance: 'high',
546
+ minConfidence: 0.7
547
+ });
548
+ }
549
+
550
+ // ==================== HELPERS ====================
551
+
552
+ function formatMemory(row) {
553
+ return {
554
+ id: row.id,
555
+ topicKey: row.topic_key,
556
+ type: row.type,
557
+ content: row.content,
558
+ projectPath: row.project_path,
559
+ changeName: row.change_name,
560
+ sessionId: row.session_id,
561
+ confidence: row.confidence,
562
+ importance: row.importance,
563
+ tags: row.tags ? JSON.parse(row.tags) : [],
564
+ relatedTo: row.related_to ? JSON.parse(row.related_to) : [],
565
+ shareable: row.shareable === 1,
566
+ author: row.author,
567
+ codeSnippet: row.code_snippet,
568
+ errorMessage: row.error_message,
569
+ solution: row.solution,
570
+ createdAt: row.created_at,
571
+ updatedAt: row.updated_at,
572
+ lastAccessed: row.last_accessed,
573
+ accessCount: row.access_count,
574
+ revisionCount: row.revision_count
575
+ };
576
+ }
577
+
578
+ function recordAccess(memoryId, sessionId, accessType) {
579
+ const db = getDatabase();
580
+
581
+ // Log del acceso
582
+ const logStmt = db.prepare('INSERT INTO memory_access_log (memory_id, session_id, access_type) VALUES (?, ?, ?)');
583
+ logStmt.run(memoryId, sessionId, accessType);
584
+
585
+ // Actualizar contador
586
+ const updateStmt = db.prepare('UPDATE memories SET access_count = access_count + 1, last_accessed = CURRENT_TIMESTAMP WHERE id = ?');
587
+ updateStmt.run(memoryId);
588
+ }
589
+
590
+ function calculateConfidence(content, data) {
591
+ let score = 0.5;
592
+
593
+ // Palabras clave de decisión
594
+ if (/decidimos|elegimos|optamos|mejor usar|recomendado|evitar/i.test(content)) score += 0.2;
595
+
596
+ // Contiene solución
597
+ if (data.solution || /solución|fix|resuelve/i.test(content)) score += 0.15;
598
+
599
+ // Contiene error
600
+ if (data.errorMessage || /error|bug|issue|problema/i.test(content)) score += 0.1;
601
+
602
+ // Es arquitectónico
603
+ if (data.type === 'architectural_decision') score += 0.1;
604
+
605
+ // Tiene código de ejemplo
606
+ if (data.codeSnippet) score += 0.05;
607
+
608
+ return Math.min(0.95, score);
609
+ }
610
+
611
+ function determineImportance(type, confidence) {
612
+ if (type === 'architectural_decision' || type === 'security_fix') return 'critical';
613
+ if (confidence > 0.8) return 'high';
614
+ if (confidence > 0.6) return 'medium';
615
+ return 'low';
616
+ }
617
+
618
+ function generateTopicKey(content) {
619
+ // Generar key estable a partir del contenido
620
+ const normalized = content.toLowerCase()
621
+ .replace(/[^\w\s]/g, '')
622
+ .split(/\s+/)
623
+ .filter(w => w.length > 3)
624
+ .slice(0, 5)
625
+ .join('-');
626
+
627
+ return normalized || `mem-${Date.now()}`;
628
+ }
629
+
630
+ /**
631
+ * Exporta memorias para sync
632
+ */
633
+ export function exportMemories(options = {}) {
634
+ const db = getDatabase();
635
+ const { projectPath = null, shareableOnly = true, since = null } = options;
636
+
637
+ let sql = 'SELECT * FROM memories WHERE is_deleted = 0';
638
+ const params = [];
639
+
640
+ if (shareableOnly) {
641
+ sql += ' AND shareable = 1';
642
+ }
643
+
644
+ if (projectPath) {
645
+ sql += ' AND project_path = ?';
646
+ params.push(projectPath);
647
+ }
648
+
649
+ if (since) {
650
+ sql += ' AND updated_at >= ?';
651
+ params.push(since);
652
+ }
653
+
654
+ const stmt = db.prepare(sql);
655
+ return stmt.all(...params).map(formatMemory);
656
+ }
657
+
658
+ /**
659
+ * Importa memorias desde export
660
+ */
661
+ export function importMemories(memories, options = {}) {
662
+ const { merge = true } = options;
663
+
664
+ const results = [];
665
+
666
+ for (const mem of memories) {
667
+ try {
668
+ const result = saveMemory({
669
+ topicKey: mem.topicKey,
670
+ type: mem.type,
671
+ content: mem.content,
672
+ projectPath: mem.projectPath,
673
+ tags: mem.tags,
674
+ importance: mem.importance,
675
+ shareable: mem.shareable,
676
+ codeSnippet: mem.codeSnippet,
677
+ errorMessage: mem.errorMessage,
678
+ solution: mem.solution
679
+ });
680
+
681
+ results.push({ success: true, id: result.id, operation: result.operation });
682
+ } catch (err) {
683
+ results.push({ success: false, error: err.message });
684
+ }
685
+ }
686
+
687
+ return results;
688
+ }
689
+
690
+ /**
691
+ * Limpieza de memorias obsoletas
692
+ */
693
+ export function pruneMemories(options = {}) {
694
+ const db = getDatabase();
695
+ const { olderThanDays = 90, lowConfidence = true, unused = true } = options;
696
+
697
+ const cutoff = new Date();
698
+ cutoff.setDate(cutoff.getDate() - olderThanDays);
699
+ const cutoffStr = cutoff.toISOString();
700
+
701
+ let sql = 'UPDATE memories SET is_deleted = 1 WHERE is_deleted = 0';
702
+ const params = [];
703
+
704
+ const conditions = [];
705
+
706
+ if (olderThanDays) {
707
+ conditions.push('created_at < ?');
708
+ params.push(cutoffStr);
709
+ }
710
+
711
+ if (lowConfidence) {
712
+ conditions.push('confidence < 0.4');
713
+ }
714
+
715
+ if (unused) {
716
+ conditions.push('access_count = 0');
717
+ }
718
+
719
+ if (conditions.length > 0) {
720
+ sql += ' AND (' + conditions.join(' OR ') + ')';
721
+ }
722
+
723
+ const stmt = db.prepare(sql);
724
+ const result = stmt.run(...params);
725
+
726
+ return { archived: result.changes };
727
+ }