stellar-memory 0.5.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 (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/dist/api/routes/analytics.d.ts +15 -0
  4. package/dist/api/routes/analytics.js +131 -0
  5. package/dist/api/routes/analytics.js.map +1 -0
  6. package/dist/api/routes/conflicts.d.ts +12 -0
  7. package/dist/api/routes/conflicts.js +67 -0
  8. package/dist/api/routes/conflicts.js.map +1 -0
  9. package/dist/api/routes/consolidation.d.ts +11 -0
  10. package/dist/api/routes/consolidation.js +63 -0
  11. package/dist/api/routes/consolidation.js.map +1 -0
  12. package/dist/api/routes/constellation.d.ts +4 -0
  13. package/dist/api/routes/constellation.js +84 -0
  14. package/dist/api/routes/constellation.js.map +1 -0
  15. package/dist/api/routes/memories.d.ts +4 -0
  16. package/dist/api/routes/memories.js +219 -0
  17. package/dist/api/routes/memories.js.map +1 -0
  18. package/dist/api/routes/observations.d.ts +10 -0
  19. package/dist/api/routes/observations.js +42 -0
  20. package/dist/api/routes/observations.js.map +1 -0
  21. package/dist/api/routes/orbit.d.ts +4 -0
  22. package/dist/api/routes/orbit.js +71 -0
  23. package/dist/api/routes/orbit.js.map +1 -0
  24. package/dist/api/routes/projects.d.ts +15 -0
  25. package/dist/api/routes/projects.js +121 -0
  26. package/dist/api/routes/projects.js.map +1 -0
  27. package/dist/api/routes/scan.d.ts +4 -0
  28. package/dist/api/routes/scan.js +403 -0
  29. package/dist/api/routes/scan.js.map +1 -0
  30. package/dist/api/routes/sun.d.ts +4 -0
  31. package/dist/api/routes/sun.js +43 -0
  32. package/dist/api/routes/sun.js.map +1 -0
  33. package/dist/api/routes/system.d.ts +4 -0
  34. package/dist/api/routes/system.js +70 -0
  35. package/dist/api/routes/system.js.map +1 -0
  36. package/dist/api/routes/temporal.d.ts +13 -0
  37. package/dist/api/routes/temporal.js +82 -0
  38. package/dist/api/routes/temporal.js.map +1 -0
  39. package/dist/api/server.d.ts +2 -0
  40. package/dist/api/server.js +99 -0
  41. package/dist/api/server.js.map +1 -0
  42. package/dist/api/websocket.d.ts +53 -0
  43. package/dist/api/websocket.js +168 -0
  44. package/dist/api/websocket.js.map +1 -0
  45. package/dist/cli/index.d.ts +12 -0
  46. package/dist/cli/index.js +35 -0
  47. package/dist/cli/index.js.map +1 -0
  48. package/dist/cli/init.d.ts +10 -0
  49. package/dist/cli/init.js +163 -0
  50. package/dist/cli/init.js.map +1 -0
  51. package/dist/engine/analytics.d.ts +93 -0
  52. package/dist/engine/analytics.js +437 -0
  53. package/dist/engine/analytics.js.map +1 -0
  54. package/dist/engine/conflict.d.ts +54 -0
  55. package/dist/engine/conflict.js +322 -0
  56. package/dist/engine/conflict.js.map +1 -0
  57. package/dist/engine/consolidation.d.ts +83 -0
  58. package/dist/engine/consolidation.js +368 -0
  59. package/dist/engine/consolidation.js.map +1 -0
  60. package/dist/engine/constellation.d.ts +66 -0
  61. package/dist/engine/constellation.js +382 -0
  62. package/dist/engine/constellation.js.map +1 -0
  63. package/dist/engine/corona.d.ts +53 -0
  64. package/dist/engine/corona.js +181 -0
  65. package/dist/engine/corona.js.map +1 -0
  66. package/dist/engine/embedding.d.ts +44 -0
  67. package/dist/engine/embedding.js +168 -0
  68. package/dist/engine/embedding.js.map +1 -0
  69. package/dist/engine/gravity.d.ts +63 -0
  70. package/dist/engine/gravity.js +121 -0
  71. package/dist/engine/gravity.js.map +1 -0
  72. package/dist/engine/multiproject.d.ts +75 -0
  73. package/dist/engine/multiproject.js +241 -0
  74. package/dist/engine/multiproject.js.map +1 -0
  75. package/dist/engine/observation.d.ts +82 -0
  76. package/dist/engine/observation.js +357 -0
  77. package/dist/engine/observation.js.map +1 -0
  78. package/dist/engine/orbit.d.ts +91 -0
  79. package/dist/engine/orbit.js +249 -0
  80. package/dist/engine/orbit.js.map +1 -0
  81. package/dist/engine/planet.d.ts +64 -0
  82. package/dist/engine/planet.js +432 -0
  83. package/dist/engine/planet.js.map +1 -0
  84. package/dist/engine/procedural.d.ts +71 -0
  85. package/dist/engine/procedural.js +259 -0
  86. package/dist/engine/procedural.js.map +1 -0
  87. package/dist/engine/quality.d.ts +48 -0
  88. package/dist/engine/quality.js +245 -0
  89. package/dist/engine/quality.js.map +1 -0
  90. package/dist/engine/repository.d.ts +79 -0
  91. package/dist/engine/repository.js +13 -0
  92. package/dist/engine/repository.js.map +1 -0
  93. package/dist/engine/sun.d.ts +61 -0
  94. package/dist/engine/sun.js +240 -0
  95. package/dist/engine/sun.js.map +1 -0
  96. package/dist/engine/temporal.d.ts +67 -0
  97. package/dist/engine/temporal.js +283 -0
  98. package/dist/engine/temporal.js.map +1 -0
  99. package/dist/engine/types.d.ts +179 -0
  100. package/dist/engine/types.js +27 -0
  101. package/dist/engine/types.js.map +1 -0
  102. package/dist/index.d.ts +2 -0
  103. package/dist/index.js +60 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/mcp/connector-registry.d.ts +20 -0
  106. package/dist/mcp/connector-registry.js +35 -0
  107. package/dist/mcp/connector-registry.js.map +1 -0
  108. package/dist/mcp/server.d.ts +13 -0
  109. package/dist/mcp/server.js +242 -0
  110. package/dist/mcp/server.js.map +1 -0
  111. package/dist/mcp/tools/daemon-tool.d.ts +16 -0
  112. package/dist/mcp/tools/daemon-tool.js +58 -0
  113. package/dist/mcp/tools/daemon-tool.js.map +1 -0
  114. package/dist/mcp/tools/ingestion-tools.d.ts +20 -0
  115. package/dist/mcp/tools/ingestion-tools.js +34 -0
  116. package/dist/mcp/tools/ingestion-tools.js.map +1 -0
  117. package/dist/mcp/tools/memory-tools.d.ts +122 -0
  118. package/dist/mcp/tools/memory-tools.js +1037 -0
  119. package/dist/mcp/tools/memory-tools.js.map +1 -0
  120. package/dist/scanner/cloud/github.d.ts +34 -0
  121. package/dist/scanner/cloud/github.js +260 -0
  122. package/dist/scanner/cloud/github.js.map +1 -0
  123. package/dist/scanner/cloud/google-drive.d.ts +30 -0
  124. package/dist/scanner/cloud/google-drive.js +289 -0
  125. package/dist/scanner/cloud/google-drive.js.map +1 -0
  126. package/dist/scanner/cloud/notion.d.ts +33 -0
  127. package/dist/scanner/cloud/notion.js +231 -0
  128. package/dist/scanner/cloud/notion.js.map +1 -0
  129. package/dist/scanner/cloud/slack.d.ts +38 -0
  130. package/dist/scanner/cloud/slack.js +282 -0
  131. package/dist/scanner/cloud/slack.js.map +1 -0
  132. package/dist/scanner/cloud/types.d.ts +73 -0
  133. package/dist/scanner/cloud/types.js +9 -0
  134. package/dist/scanner/cloud/types.js.map +1 -0
  135. package/dist/scanner/index.d.ts +35 -0
  136. package/dist/scanner/index.js +420 -0
  137. package/dist/scanner/index.js.map +1 -0
  138. package/dist/scanner/local/filesystem.d.ts +33 -0
  139. package/dist/scanner/local/filesystem.js +203 -0
  140. package/dist/scanner/local/filesystem.js.map +1 -0
  141. package/dist/scanner/local/git.d.ts +24 -0
  142. package/dist/scanner/local/git.js +161 -0
  143. package/dist/scanner/local/git.js.map +1 -0
  144. package/dist/scanner/local/parsers/code.d.ts +3 -0
  145. package/dist/scanner/local/parsers/code.js +127 -0
  146. package/dist/scanner/local/parsers/code.js.map +1 -0
  147. package/dist/scanner/local/parsers/index.d.ts +11 -0
  148. package/dist/scanner/local/parsers/index.js +24 -0
  149. package/dist/scanner/local/parsers/index.js.map +1 -0
  150. package/dist/scanner/local/parsers/json-parser.d.ts +3 -0
  151. package/dist/scanner/local/parsers/json-parser.js +117 -0
  152. package/dist/scanner/local/parsers/json-parser.js.map +1 -0
  153. package/dist/scanner/local/parsers/markdown.d.ts +3 -0
  154. package/dist/scanner/local/parsers/markdown.js +120 -0
  155. package/dist/scanner/local/parsers/markdown.js.map +1 -0
  156. package/dist/scanner/local/parsers/text.d.ts +3 -0
  157. package/dist/scanner/local/parsers/text.js +41 -0
  158. package/dist/scanner/local/parsers/text.js.map +1 -0
  159. package/dist/scanner/metadata-scanner.d.ts +67 -0
  160. package/dist/scanner/metadata-scanner.js +356 -0
  161. package/dist/scanner/metadata-scanner.js.map +1 -0
  162. package/dist/scanner/types.d.ts +47 -0
  163. package/dist/scanner/types.js +19 -0
  164. package/dist/scanner/types.js.map +1 -0
  165. package/dist/service/daemon.d.ts +23 -0
  166. package/dist/service/daemon.js +105 -0
  167. package/dist/service/daemon.js.map +1 -0
  168. package/dist/service/scheduler.d.ts +73 -0
  169. package/dist/service/scheduler.js +281 -0
  170. package/dist/service/scheduler.js.map +1 -0
  171. package/dist/storage/database.d.ts +10 -0
  172. package/dist/storage/database.js +265 -0
  173. package/dist/storage/database.js.map +1 -0
  174. package/dist/storage/queries.d.ts +85 -0
  175. package/dist/storage/queries.js +865 -0
  176. package/dist/storage/queries.js.map +1 -0
  177. package/dist/storage/sqlite-repository.d.ts +32 -0
  178. package/dist/storage/sqlite-repository.js +68 -0
  179. package/dist/storage/sqlite-repository.js.map +1 -0
  180. package/dist/storage/vec.d.ts +62 -0
  181. package/dist/storage/vec.js +111 -0
  182. package/dist/storage/vec.js.map +1 -0
  183. package/dist/utils/config.d.ts +5 -0
  184. package/dist/utils/config.js +60 -0
  185. package/dist/utils/config.js.map +1 -0
  186. package/dist/utils/logger.d.ts +36 -0
  187. package/dist/utils/logger.js +86 -0
  188. package/dist/utils/logger.js.map +1 -0
  189. package/dist/utils/time.d.ts +21 -0
  190. package/dist/utils/time.js +42 -0
  191. package/dist/utils/time.js.map +1 -0
  192. package/dist/utils/tokenizer.d.ts +13 -0
  193. package/dist/utils/tokenizer.js +46 -0
  194. package/dist/utils/tokenizer.js.map +1 -0
  195. package/package.json +77 -0
  196. package/scripts/check-node.mjs +36 -0
  197. package/scripts/setup.mjs +157 -0
@@ -0,0 +1,865 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getDatabase } from './database.js';
3
+ import { ORBIT_ZONES } from '../engine/types.js';
4
+ import { createLogger } from '../utils/logger.js';
5
+ const log = createLogger('queries');
6
+ // ---------------------------------------------------------------------------
7
+ // Deserializers — parse JSON fields coming out of SQLite
8
+ // ---------------------------------------------------------------------------
9
+ // Cast helpers — node:sqlite returns Record<string, SQLOutputValue> from .get()/.all().
10
+ // We cast through unknown because we know the schema guarantees the shape.
11
+ function asRawMemory(row) {
12
+ return row;
13
+ }
14
+ function asRawSunState(row) {
15
+ return row;
16
+ }
17
+ function deserializeMemory(row) {
18
+ return {
19
+ ...row,
20
+ type: row.type,
21
+ tags: parseJsonArray(row.tags),
22
+ metadata: parseJsonObject(row.metadata),
23
+ source: row.source ?? 'manual',
24
+ source_path: row.source_path ?? null,
25
+ source_hash: row.source_hash ?? null,
26
+ content_hash: row.content_hash ?? null,
27
+ valid_from: row.valid_from ?? undefined,
28
+ valid_until: row.valid_until ?? undefined,
29
+ superseded_by: row.superseded_by ?? undefined,
30
+ consolidated_into: row.consolidated_into ?? undefined,
31
+ quality_score: row.quality_score ?? undefined,
32
+ is_universal: row.is_universal ? Boolean(row.is_universal) : undefined,
33
+ };
34
+ }
35
+ function deserializeConstellationEdge(row) {
36
+ return {
37
+ ...row,
38
+ relation: row.relation,
39
+ metadata: parseJsonObject(row.metadata),
40
+ };
41
+ }
42
+ function deserializeConflict(row) {
43
+ return {
44
+ ...row,
45
+ severity: row.severity,
46
+ status: row.status,
47
+ resolution: row.resolution ?? undefined,
48
+ resolved_at: row.resolved_at ?? undefined,
49
+ };
50
+ }
51
+ function deserializeObservation(row) {
52
+ return {
53
+ ...row,
54
+ extracted_memories: parseJsonArray(row.extracted_memories),
55
+ source: row.source,
56
+ };
57
+ }
58
+ function asRawDataSource(row) {
59
+ return row;
60
+ }
61
+ function deserializeDataSource(row) {
62
+ return {
63
+ ...row,
64
+ type: row.type,
65
+ status: row.status,
66
+ config: parseJsonObject(row.config),
67
+ };
68
+ }
69
+ function deserializeSunState(row) {
70
+ return {
71
+ ...row,
72
+ recent_decisions: parseJsonArray(row.recent_decisions),
73
+ next_steps: parseJsonArray(row.next_steps),
74
+ active_errors: parseJsonArray(row.active_errors),
75
+ };
76
+ }
77
+ function parseJsonArray(value) {
78
+ if (typeof value !== 'string' || value === '')
79
+ return [];
80
+ try {
81
+ const parsed = JSON.parse(value);
82
+ return Array.isArray(parsed) ? parsed : [];
83
+ }
84
+ catch {
85
+ log.warn('JSON array parse failed', { raw: String(value).slice(0, 100) });
86
+ return [];
87
+ }
88
+ }
89
+ function parseJsonObject(value) {
90
+ if (typeof value !== 'string' || value === '')
91
+ return {};
92
+ try {
93
+ const parsed = JSON.parse(value);
94
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
95
+ ? parsed
96
+ : {};
97
+ }
98
+ catch {
99
+ log.warn('JSON object parse failed', { raw: String(value).slice(0, 100) });
100
+ return {};
101
+ }
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Memory CRUD
105
+ // ---------------------------------------------------------------------------
106
+ export function insertMemory(memory) {
107
+ const db = getDatabase();
108
+ const now = new Date().toISOString();
109
+ const id = memory.id ?? randomUUID();
110
+ const project = memory.project ?? 'default';
111
+ const content = memory.content ?? '';
112
+ const summary = memory.summary ?? '';
113
+ const type = memory.type ?? 'observation';
114
+ const tags = JSON.stringify(memory.tags ?? []);
115
+ const distance = memory.distance ?? 5.0;
116
+ const importance = memory.importance ?? 0.5;
117
+ const velocity = memory.velocity ?? 0.0;
118
+ const impact = memory.impact ?? 0.5;
119
+ const access_count = memory.access_count ?? 0;
120
+ const last_accessed_at = memory.last_accessed_at ?? null;
121
+ const metadata = JSON.stringify(memory.metadata ?? {});
122
+ const source = memory.source ?? 'manual';
123
+ const source_path = memory.source_path ?? null;
124
+ const source_hash = memory.source_hash ?? null;
125
+ const content_hash = memory.content_hash ?? null;
126
+ const created_at = memory.created_at ?? now;
127
+ const updated_at = memory.updated_at ?? now;
128
+ const deleted_at = memory.deleted_at ?? null;
129
+ db.prepare(`
130
+ INSERT INTO memories (
131
+ id, project, content, summary, type, tags,
132
+ distance, importance, velocity, impact,
133
+ access_count, last_accessed_at, metadata,
134
+ source, source_path, source_hash, content_hash,
135
+ created_at, updated_at, deleted_at
136
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
137
+ `).run(id, project, content, summary, type, tags, distance, importance, velocity, impact, access_count, last_accessed_at, metadata, source, source_path, source_hash, content_hash, created_at, updated_at, deleted_at);
138
+ // Return the fully resolved Memory object (no second DB hit needed)
139
+ return {
140
+ id, project, content, summary,
141
+ type: type,
142
+ tags: memory.tags ?? [],
143
+ distance, importance, velocity, impact,
144
+ access_count, last_accessed_at, metadata: memory.metadata ?? {},
145
+ source, source_path, source_hash, content_hash,
146
+ created_at, updated_at, deleted_at,
147
+ };
148
+ }
149
+ export function getMemoryById(id) {
150
+ const db = getDatabase();
151
+ const row = db.prepare(`
152
+ SELECT * FROM memories WHERE id = ?
153
+ `).get(id);
154
+ return row ? deserializeMemory(asRawMemory(row)) : null;
155
+ }
156
+ export function getMemoryByIds(ids) {
157
+ if (ids.length === 0)
158
+ return [];
159
+ const db = getDatabase();
160
+ const placeholders = ids.map(() => '?').join(', ');
161
+ const rows = db.prepare(`
162
+ SELECT * FROM memories
163
+ WHERE id IN (${placeholders})
164
+ AND deleted_at IS NULL
165
+ `).all(...ids);
166
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
167
+ }
168
+ export function getMemoriesByProject(project, includeDeleted = false) {
169
+ const db = getDatabase();
170
+ const sql = includeDeleted
171
+ ? `SELECT * FROM memories WHERE project = ? ORDER BY distance ASC`
172
+ : `SELECT * FROM memories WHERE project = ? AND deleted_at IS NULL ORDER BY distance ASC`;
173
+ const rows = db.prepare(sql).all(project);
174
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
175
+ }
176
+ /**
177
+ * Get memories created within the last `hoursAgo` hours for a project.
178
+ * Used by auto-commit on shutdown to summarize the current session.
179
+ */
180
+ export function getRecentMemories(project, hoursAgo = 3) {
181
+ const db = getDatabase();
182
+ const cutoff = new Date(Date.now() - hoursAgo * 60 * 60 * 1000).toISOString();
183
+ const rows = db.prepare(`
184
+ SELECT * FROM memories
185
+ WHERE project = ?
186
+ AND deleted_at IS NULL
187
+ AND created_at > ?
188
+ ORDER BY created_at DESC
189
+ `).all(project, cutoff);
190
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
191
+ }
192
+ export function getMemoriesInZone(project, zone) {
193
+ const db = getDatabase();
194
+ const { min, max } = ORBIT_ZONES[zone];
195
+ const rows = db.prepare(`
196
+ SELECT * FROM memories
197
+ WHERE project = ?
198
+ AND deleted_at IS NULL
199
+ AND distance >= ?
200
+ AND distance < ?
201
+ ORDER BY distance ASC
202
+ `).all(project, min, max);
203
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
204
+ }
205
+ export function updateMemoryOrbit(id, distance, importance, velocity) {
206
+ const db = getDatabase();
207
+ const now = new Date().toISOString();
208
+ db.prepare(`
209
+ UPDATE memories
210
+ SET distance = ?, importance = ?, velocity = ?, updated_at = ?
211
+ WHERE id = ?
212
+ `).run(distance, importance, velocity, now, id);
213
+ }
214
+ export function updateMemoryAccess(id) {
215
+ const db = getDatabase();
216
+ const now = new Date().toISOString();
217
+ db.prepare(`
218
+ UPDATE memories
219
+ SET access_count = access_count + 1,
220
+ last_accessed_at = ?,
221
+ updated_at = ?
222
+ WHERE id = ?
223
+ `).run(now, now, id);
224
+ }
225
+ export function softDeleteMemory(id) {
226
+ const db = getDatabase();
227
+ const now = new Date().toISOString();
228
+ db.prepare(`
229
+ UPDATE memories
230
+ SET deleted_at = ?, updated_at = ?
231
+ WHERE id = ?
232
+ `).run(now, now, id);
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // Full-text search (FTS5)
236
+ // ---------------------------------------------------------------------------
237
+ /**
238
+ * Escape a user-supplied string for use in an FTS5 MATCH clause.
239
+ * Wraps the entire query in double-quotes and escapes internal double-quotes
240
+ * so it is treated as a literal phrase rather than FTS5 query syntax.
241
+ */
242
+ function escapeFtsQuery(query) {
243
+ // Split into individual words, quote each one to escape FTS5 operators,
244
+ // then join with spaces (implicit AND). This avoids phrase-matching issues
245
+ // while still preventing FTS5 syntax errors from special characters.
246
+ const words = query.trim().split(/\s+/).filter(w => w.length > 0);
247
+ if (words.length === 0)
248
+ return '""';
249
+ return words.map(w => '"' + w.replace(/"/g, '""') + '"').join(' ');
250
+ }
251
+ export function searchMemories(project, query, limit = 20) {
252
+ const db = getDatabase();
253
+ const escapedQuery = escapeFtsQuery(query);
254
+ // FTS5 MATCH uses its own query syntax; we join on rowid to get the full row
255
+ const rows = db.prepare(`
256
+ SELECT m.*
257
+ FROM memories m
258
+ JOIN memories_fts fts ON m.rowid = fts.rowid
259
+ WHERE memories_fts MATCH ?
260
+ AND m.project = ?
261
+ AND m.deleted_at IS NULL
262
+ AND (m.valid_until IS NULL OR m.valid_until > datetime('now'))
263
+ ORDER BY rank
264
+ LIMIT ?
265
+ `).all(escapedQuery, project, limit);
266
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
267
+ }
268
+ // ---------------------------------------------------------------------------
269
+ // Distance-ranged FTS5 search (used by tiered recall pipeline)
270
+ // ---------------------------------------------------------------------------
271
+ export function searchMemoriesInRange(project, query, minDistance, maxDistance, limit) {
272
+ const db = getDatabase();
273
+ const escapedQuery = escapeFtsQuery(query);
274
+ const rows = db.prepare(`
275
+ SELECT m.*
276
+ FROM memories m
277
+ JOIN memories_fts fts ON m.rowid = fts.rowid
278
+ WHERE memories_fts MATCH ?
279
+ AND m.project = ?
280
+ AND m.deleted_at IS NULL
281
+ AND (m.valid_until IS NULL OR m.valid_until > datetime('now'))
282
+ AND m.distance >= ?
283
+ AND m.distance < ?
284
+ ORDER BY rank
285
+ LIMIT ?
286
+ `).all(escapedQuery, project, minDistance, maxDistance, limit);
287
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
288
+ }
289
+ // ---------------------------------------------------------------------------
290
+ // Nearest memories (by orbital distance — closest to the "sun" first)
291
+ // ---------------------------------------------------------------------------
292
+ export function getNearestMemories(project, limit) {
293
+ const db = getDatabase();
294
+ const rows = db.prepare(`
295
+ SELECT * FROM memories
296
+ WHERE project = ? AND deleted_at IS NULL
297
+ ORDER BY distance ASC
298
+ LIMIT ?
299
+ `).all(project, limit);
300
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
301
+ }
302
+ // ---------------------------------------------------------------------------
303
+ // Sun state
304
+ // ---------------------------------------------------------------------------
305
+ export function getSunState(project) {
306
+ const db = getDatabase();
307
+ const row = db.prepare(`
308
+ SELECT * FROM sun_state WHERE project = ?
309
+ `).get(project);
310
+ return row ? deserializeSunState(asRawSunState(row)) : null;
311
+ }
312
+ export function upsertSunState(state) {
313
+ const db = getDatabase();
314
+ const now = new Date().toISOString();
315
+ // Fetch existing row so we can merge rather than blindly overwrite fields
316
+ const existing = getSunState(state.project);
317
+ const content = state.content ?? existing?.content ?? '';
318
+ const current_work = state.current_work ?? existing?.current_work ?? '';
319
+ const recent_decisions = JSON.stringify(state.recent_decisions ?? existing?.recent_decisions ?? []);
320
+ const next_steps = JSON.stringify(state.next_steps ?? existing?.next_steps ?? []);
321
+ const active_errors = JSON.stringify(state.active_errors ?? existing?.active_errors ?? []);
322
+ const project_context = state.project_context ?? existing?.project_context ?? '';
323
+ const token_count = state.token_count ?? existing?.token_count ?? 0;
324
+ const last_commit_at = state.last_commit_at ?? existing?.last_commit_at ?? null;
325
+ db.prepare(`
326
+ INSERT INTO sun_state (
327
+ project, content, current_work,
328
+ recent_decisions, next_steps, active_errors,
329
+ project_context, token_count, last_commit_at, updated_at
330
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
331
+ ON CONFLICT(project) DO UPDATE SET
332
+ content = excluded.content,
333
+ current_work = excluded.current_work,
334
+ recent_decisions = excluded.recent_decisions,
335
+ next_steps = excluded.next_steps,
336
+ active_errors = excluded.active_errors,
337
+ project_context = excluded.project_context,
338
+ token_count = excluded.token_count,
339
+ last_commit_at = excluded.last_commit_at,
340
+ updated_at = excluded.updated_at
341
+ `).run(state.project, content, current_work, recent_decisions, next_steps, active_errors, project_context, token_count, last_commit_at, now);
342
+ }
343
+ // ---------------------------------------------------------------------------
344
+ // Orbit log
345
+ // ---------------------------------------------------------------------------
346
+ export function insertOrbitLog(change) {
347
+ const db = getDatabase();
348
+ const now = new Date().toISOString();
349
+ db.prepare(`
350
+ INSERT INTO orbit_log (
351
+ memory_id, project,
352
+ old_distance, new_distance,
353
+ old_importance, new_importance,
354
+ trigger, created_at
355
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
356
+ `).run(change.memory_id, change.project, change.old_distance, change.new_distance, change.old_importance, change.new_importance, change.trigger, now);
357
+ }
358
+ export function cleanupOrbitLog(retentionDays = 90) {
359
+ const db = getDatabase();
360
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString();
361
+ const result = db.prepare('DELETE FROM orbit_log WHERE created_at < ?').run(cutoff);
362
+ return Number(result.changes);
363
+ }
364
+ // ---------------------------------------------------------------------------
365
+ // Source-path deduplication
366
+ // ---------------------------------------------------------------------------
367
+ /**
368
+ * Check whether a memory already exists for the given source_path + source_hash.
369
+ * Returns true if an identical (path, hash) pair is already stored and not deleted.
370
+ */
371
+ export function memoryExistsForSource(sourcePath, sourceHash) {
372
+ const db = getDatabase();
373
+ const row = db.prepare(`
374
+ SELECT id FROM memories
375
+ WHERE source_path = ? AND source_hash = ? AND deleted_at IS NULL
376
+ LIMIT 1
377
+ `).get(sourcePath, sourceHash);
378
+ return row !== undefined;
379
+ }
380
+ /**
381
+ * Find a memory by source_path (regardless of hash). Used to update stale entries.
382
+ */
383
+ export function getMemoryBySourcePath(sourcePath) {
384
+ const db = getDatabase();
385
+ const row = db.prepare(`
386
+ SELECT * FROM memories
387
+ WHERE source_path = ? AND deleted_at IS NULL
388
+ LIMIT 1
389
+ `).get(sourcePath);
390
+ return row ? deserializeMemory(asRawMemory(row)) : null;
391
+ }
392
+ /**
393
+ * Find a non-deleted memory in the given project that has the same content hash.
394
+ * Used by createMemory() for content-level deduplication.
395
+ */
396
+ export function getMemoryByContentHash(project, contentHash) {
397
+ const db = getDatabase();
398
+ const row = db.prepare(`
399
+ SELECT * FROM memories
400
+ WHERE project = ? AND content_hash = ? AND deleted_at IS NULL
401
+ LIMIT 1
402
+ `).get(project, contentHash);
403
+ return row ? deserializeMemory(asRawMemory(row)) : null;
404
+ }
405
+ // ---------------------------------------------------------------------------
406
+ // Data sources CRUD
407
+ // ---------------------------------------------------------------------------
408
+ export function insertDataSource(ds) {
409
+ const db = getDatabase();
410
+ const now = new Date().toISOString();
411
+ db.prepare(`
412
+ INSERT INTO data_sources (id, path, type, status, last_scanned_at, file_count, total_size, config, created_at, updated_at)
413
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
414
+ `).run(ds.id, ds.path, ds.type, ds.status, ds.last_scanned_at ?? null, ds.file_count ?? 0, ds.total_size ?? 0, JSON.stringify(ds.config ?? {}), now, now);
415
+ return { ...ds, config: ds.config ?? {}, created_at: now, updated_at: now };
416
+ }
417
+ export function updateDataSource(id, patch) {
418
+ const db = getDatabase();
419
+ const now = new Date().toISOString();
420
+ const sets = ['updated_at = ?'];
421
+ const values = [now];
422
+ if (patch.status !== undefined) {
423
+ sets.push('status = ?');
424
+ values.push(patch.status);
425
+ }
426
+ if (patch.last_scanned_at !== undefined) {
427
+ sets.push('last_scanned_at = ?');
428
+ values.push(patch.last_scanned_at);
429
+ }
430
+ if (patch.file_count !== undefined) {
431
+ sets.push('file_count = ?');
432
+ values.push(patch.file_count);
433
+ }
434
+ if (patch.total_size !== undefined) {
435
+ sets.push('total_size = ?');
436
+ values.push(patch.total_size);
437
+ }
438
+ if (patch.config !== undefined) {
439
+ sets.push('config = ?');
440
+ values.push(JSON.stringify(patch.config));
441
+ }
442
+ values.push(id);
443
+ db.prepare(`UPDATE data_sources SET ${sets.join(', ')} WHERE id = ?`).run(...values);
444
+ }
445
+ export function getAllDataSources() {
446
+ const db = getDatabase();
447
+ const rows = db.prepare(`SELECT * FROM data_sources ORDER BY created_at DESC`).all();
448
+ return rows.map((r) => deserializeDataSource(asRawDataSource(r)));
449
+ }
450
+ export function getDataSourceByPath(path) {
451
+ const db = getDatabase();
452
+ const row = db.prepare(`SELECT * FROM data_sources WHERE path = ? LIMIT 1`).get(path);
453
+ return row ? deserializeDataSource(asRawDataSource(row)) : null;
454
+ }
455
+ // ---------------------------------------------------------------------------
456
+ // Constellation queries (Knowledge Graph)
457
+ // ---------------------------------------------------------------------------
458
+ export function createEdge(edge) {
459
+ const db = getDatabase();
460
+ db.prepare(`
461
+ INSERT INTO constellation_edges (id, source_id, target_id, relation, weight, project, metadata, created_at)
462
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
463
+ ON CONFLICT(source_id, target_id, relation) DO UPDATE SET
464
+ weight = excluded.weight,
465
+ metadata = excluded.metadata
466
+ `).run(edge.id, edge.source_id, edge.target_id, edge.relation, edge.weight, edge.project, JSON.stringify(edge.metadata ?? {}), edge.created_at);
467
+ }
468
+ export function getEdges(memoryId, project) {
469
+ const db = getDatabase();
470
+ const rows = db.prepare(`
471
+ SELECT * FROM constellation_edges
472
+ WHERE (source_id = ? OR target_id = ?) AND project = ?
473
+ ORDER BY weight DESC
474
+ `).all(memoryId, memoryId, project);
475
+ return rows.map((r) => deserializeConstellationEdge(r));
476
+ }
477
+ export function getConstellation(memoryId, project, depth = 1) {
478
+ const db = getDatabase();
479
+ const visitedNodeIds = new Set([memoryId]);
480
+ const allEdges = [];
481
+ let frontier = [memoryId];
482
+ for (let d = 0; d < depth; d++) {
483
+ if (frontier.length === 0)
484
+ break;
485
+ const placeholders = frontier.map(() => '?').join(', ');
486
+ const edgeRows = db.prepare(`
487
+ SELECT * FROM constellation_edges
488
+ WHERE (source_id IN (${placeholders}) OR target_id IN (${placeholders}))
489
+ AND project = ?
490
+ `).all(...frontier, ...frontier, project);
491
+ for (const r of edgeRows) {
492
+ const edge = deserializeConstellationEdge(r);
493
+ allEdges.push(edge);
494
+ visitedNodeIds.add(edge.source_id);
495
+ visitedNodeIds.add(edge.target_id);
496
+ }
497
+ frontier = [...visitedNodeIds].filter((id) => !frontier.includes(id) && id !== memoryId);
498
+ }
499
+ const nodes = getMemoryByIds([...visitedNodeIds]);
500
+ return { nodes, edges: allEdges };
501
+ }
502
+ /**
503
+ * Get all constellation neighbors for a batch of memory IDs.
504
+ * Returns a map from memory ID → set of neighbor IDs.
505
+ */
506
+ export function getEdgesForBatch(memoryIds, project) {
507
+ if (memoryIds.length === 0)
508
+ return new Map();
509
+ const db = getDatabase();
510
+ const placeholders = memoryIds.map(() => '?').join(', ');
511
+ const rows = db.prepare(`
512
+ SELECT source_id, target_id FROM constellation_edges
513
+ WHERE (source_id IN (${placeholders}) OR target_id IN (${placeholders}))
514
+ AND project = ?
515
+ `).all(...memoryIds, ...memoryIds, project);
516
+ const idSet = new Set(memoryIds);
517
+ const result = new Map();
518
+ for (const r of rows) {
519
+ const row = r;
520
+ // For each memory in our batch, record its neighbor
521
+ if (idSet.has(row.source_id)) {
522
+ const neighbors = result.get(row.source_id) ?? new Set();
523
+ neighbors.add(row.target_id);
524
+ result.set(row.source_id, neighbors);
525
+ }
526
+ if (idSet.has(row.target_id)) {
527
+ const neighbors = result.get(row.target_id) ?? new Set();
528
+ neighbors.add(row.source_id);
529
+ result.set(row.target_id, neighbors);
530
+ }
531
+ }
532
+ return result;
533
+ }
534
+ export function deleteEdge(id) {
535
+ const db = getDatabase();
536
+ db.prepare(`DELETE FROM constellation_edges WHERE id = ?`).run(id);
537
+ }
538
+ // ---------------------------------------------------------------------------
539
+ // Conflict queries
540
+ // ---------------------------------------------------------------------------
541
+ export function createConflict(conflict) {
542
+ const db = getDatabase();
543
+ db.prepare(`
544
+ INSERT INTO memory_conflicts (
545
+ id, memory_id, conflicting_memory_id, severity,
546
+ description, status, resolution, project, created_at, resolved_at
547
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
548
+ `).run(conflict.id, conflict.memory_id, conflict.conflicting_memory_id, conflict.severity, conflict.description, conflict.status, conflict.resolution ?? null, conflict.project, conflict.created_at, conflict.resolved_at ?? null);
549
+ }
550
+ export function getConflicts(project, status) {
551
+ const db = getDatabase();
552
+ const rows = status
553
+ ? db.prepare(`
554
+ SELECT * FROM memory_conflicts
555
+ WHERE project = ? AND status = ?
556
+ ORDER BY created_at DESC
557
+ `).all(project, status)
558
+ : db.prepare(`
559
+ SELECT * FROM memory_conflicts
560
+ WHERE project = ?
561
+ ORDER BY created_at DESC
562
+ `).all(project);
563
+ return rows.map((r) => deserializeConflict(r));
564
+ }
565
+ export function getConflictsForMemory(memoryId) {
566
+ const db = getDatabase();
567
+ const rows = db.prepare(`
568
+ SELECT * FROM memory_conflicts
569
+ WHERE memory_id = ? OR conflicting_memory_id = ?
570
+ ORDER BY created_at DESC
571
+ `).all(memoryId, memoryId);
572
+ return rows.map((r) => deserializeConflict(r));
573
+ }
574
+ export function resolveConflict(id, resolution) {
575
+ const db = getDatabase();
576
+ const now = new Date().toISOString();
577
+ db.prepare(`
578
+ UPDATE memory_conflicts
579
+ SET status = 'resolved', resolution = ?, resolved_at = ?
580
+ WHERE id = ?
581
+ `).run(resolution, now, id);
582
+ }
583
+ // ---------------------------------------------------------------------------
584
+ // Observation queries
585
+ // ---------------------------------------------------------------------------
586
+ export function createObservation(entry) {
587
+ const db = getDatabase();
588
+ db.prepare(`
589
+ INSERT INTO observation_log (id, content, extracted_memories, source, project, created_at)
590
+ VALUES (?, ?, ?, ?, ?, ?)
591
+ `).run(entry.id, entry.content, JSON.stringify(entry.extracted_memories), entry.source, entry.project, entry.created_at);
592
+ }
593
+ export function getObservations(project, limit = 20) {
594
+ const db = getDatabase();
595
+ const rows = db.prepare(`
596
+ SELECT * FROM observation_log
597
+ WHERE project = ?
598
+ ORDER BY created_at DESC
599
+ LIMIT ?
600
+ `).all(project, limit);
601
+ return rows.map((r) => deserializeObservation(r));
602
+ }
603
+ // ---------------------------------------------------------------------------
604
+ // Temporal queries
605
+ // ---------------------------------------------------------------------------
606
+ export function getMemoriesAtTime(project, timestamp) {
607
+ const db = getDatabase();
608
+ const rows = db.prepare(`
609
+ SELECT * FROM memories
610
+ WHERE project = ?
611
+ AND deleted_at IS NULL
612
+ AND (valid_from IS NULL OR valid_from <= ?)
613
+ AND (valid_until IS NULL OR valid_until > ?)
614
+ ORDER BY distance ASC
615
+ `).all(project, timestamp, timestamp);
616
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
617
+ }
618
+ export function supersedMemory(memoryId, newMemoryId) {
619
+ const db = getDatabase();
620
+ const now = new Date().toISOString();
621
+ db.prepare(`
622
+ UPDATE memories
623
+ SET superseded_by = ?, valid_until = ?, updated_at = ?
624
+ WHERE id = ?
625
+ `).run(newMemoryId, now, now, memoryId);
626
+ }
627
+ export function getSupersessionChain(memoryId) {
628
+ const db = getDatabase();
629
+ const chain = [];
630
+ let currentId = memoryId;
631
+ while (currentId) {
632
+ const row = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(currentId);
633
+ if (!row)
634
+ break;
635
+ const mem = deserializeMemory(asRawMemory(row));
636
+ chain.push(mem);
637
+ currentId = mem.superseded_by ?? null;
638
+ }
639
+ return chain;
640
+ }
641
+ // ---------------------------------------------------------------------------
642
+ // Consolidation queries
643
+ // ---------------------------------------------------------------------------
644
+ export function consolidateMemories(sourceIds, targetId) {
645
+ if (sourceIds.length === 0)
646
+ return;
647
+ const db = getDatabase();
648
+ const now = new Date().toISOString();
649
+ const placeholders = sourceIds.map(() => '?').join(', ');
650
+ db.prepare(`
651
+ UPDATE memories
652
+ SET consolidated_into = ?, updated_at = ?
653
+ WHERE id IN (${placeholders})
654
+ `).run(targetId, now, ...sourceIds);
655
+ }
656
+ export function getConsolidationHistory(memoryId) {
657
+ const db = getDatabase();
658
+ const rows = db.prepare(`
659
+ SELECT * FROM memories
660
+ WHERE consolidated_into = ?
661
+ ORDER BY created_at ASC
662
+ `).all(memoryId);
663
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
664
+ }
665
+ // ---------------------------------------------------------------------------
666
+ // Multi-project queries
667
+ // ---------------------------------------------------------------------------
668
+ export function getUniversalMemories(limit = 50) {
669
+ const db = getDatabase();
670
+ const rows = db.prepare(`
671
+ SELECT * FROM memories
672
+ WHERE is_universal = 1 AND deleted_at IS NULL
673
+ ORDER BY importance DESC
674
+ LIMIT ?
675
+ `).all(limit);
676
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
677
+ }
678
+ export function setUniversal(memoryId, isUniversal) {
679
+ const db = getDatabase();
680
+ const now = new Date().toISOString();
681
+ db.prepare(`
682
+ UPDATE memories SET is_universal = ?, updated_at = ? WHERE id = ?
683
+ `).run(isUniversal ? 1 : 0, now, memoryId);
684
+ }
685
+ export function listProjects() {
686
+ const db = getDatabase();
687
+ const rows = db.prepare(`
688
+ SELECT project, COUNT(*) as count
689
+ FROM memories
690
+ WHERE deleted_at IS NULL
691
+ GROUP BY project
692
+ ORDER BY count DESC
693
+ `).all();
694
+ return rows.map((r) => {
695
+ const row = r;
696
+ return { project: row.project, count: row.count };
697
+ });
698
+ }
699
+ // ---------------------------------------------------------------------------
700
+ // Quality queries
701
+ // ---------------------------------------------------------------------------
702
+ export function updateQualityScore(memoryId, score) {
703
+ const db = getDatabase();
704
+ const now = new Date().toISOString();
705
+ db.prepare(`
706
+ UPDATE memories SET quality_score = ?, updated_at = ? WHERE id = ?
707
+ `).run(score, now, memoryId);
708
+ }
709
+ export function getMemoriesByQuality(project, minScore = 0.0, maxScore = 1.0) {
710
+ const db = getDatabase();
711
+ const rows = db.prepare(`
712
+ SELECT * FROM memories
713
+ WHERE project = ?
714
+ AND deleted_at IS NULL
715
+ AND quality_score >= ?
716
+ AND quality_score <= ?
717
+ ORDER BY quality_score DESC
718
+ `).all(project, minScore, maxScore);
719
+ return rows.map((r) => deserializeMemory(asRawMemory(r)));
720
+ }
721
+ // ---------------------------------------------------------------------------
722
+ // Analytics queries
723
+ // ---------------------------------------------------------------------------
724
+ export function getTopTags(project, limit = 20) {
725
+ const db = getDatabase();
726
+ // Tags are stored as JSON arrays — we use the memories table and parse in JS
727
+ const rows = db.prepare(`
728
+ SELECT tags FROM memories
729
+ WHERE project = ? AND deleted_at IS NULL
730
+ `).all(project);
731
+ const tagCounts = new Map();
732
+ for (const r of rows) {
733
+ const row = r;
734
+ const tags = parseJsonArray(row.tags);
735
+ for (const tag of tags) {
736
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
737
+ }
738
+ }
739
+ return [...tagCounts.entries()]
740
+ .map(([tag, count]) => ({ tag, count }))
741
+ .sort((a, b) => b.count - a.count)
742
+ .slice(0, limit);
743
+ }
744
+ export function getActivityTimeline(project, days = 30) {
745
+ const db = getDatabase();
746
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
747
+ const createdRows = db.prepare(`
748
+ SELECT date(created_at) as date, COUNT(*) as count
749
+ FROM memories
750
+ WHERE project = ? AND date(created_at) >= ?
751
+ GROUP BY date(created_at)
752
+ `).all(project, cutoff);
753
+ const accessedRows = db.prepare(`
754
+ SELECT date(last_accessed_at) as date, COUNT(*) as count
755
+ FROM memories
756
+ WHERE project = ?
757
+ AND last_accessed_at IS NOT NULL
758
+ AND date(last_accessed_at) >= ?
759
+ GROUP BY date(last_accessed_at)
760
+ `).all(project, cutoff);
761
+ const timeline = new Map();
762
+ for (const r of createdRows) {
763
+ const row = r;
764
+ const entry = timeline.get(row.date) ?? { created: 0, accessed: 0 };
765
+ entry.created = row.count;
766
+ timeline.set(row.date, entry);
767
+ }
768
+ for (const r of accessedRows) {
769
+ const row = r;
770
+ const entry = timeline.get(row.date) ?? { created: 0, accessed: 0 };
771
+ entry.accessed = row.count;
772
+ timeline.set(row.date, entry);
773
+ }
774
+ return [...timeline.entries()]
775
+ .map(([date, counts]) => ({ date, ...counts }))
776
+ .sort((a, b) => a.date.localeCompare(b.date));
777
+ }
778
+ export function getRecallSuccessRate(project) {
779
+ const db = getDatabase();
780
+ const result = db.prepare(`
781
+ SELECT
782
+ COUNT(*) as total,
783
+ SUM(CASE WHEN access_count > 0 THEN 1 ELSE 0 END) as accessed
784
+ FROM memories
785
+ WHERE project = ? AND deleted_at IS NULL
786
+ `).get(project);
787
+ const row = result;
788
+ if (!row || row.total === 0)
789
+ return 0;
790
+ return row.accessed / row.total;
791
+ }
792
+ export function getAnalytics(project) {
793
+ const db = getDatabase();
794
+ // Aggregate stats
795
+ const statsRow = db.prepare(`
796
+ SELECT
797
+ COUNT(*) as total_memories,
798
+ AVG(CASE WHEN quality_score IS NOT NULL THEN quality_score ELSE 0.5 END) as avg_quality,
799
+ AVG(importance) as avg_importance,
800
+ SUM(CASE WHEN consolidated_into IS NOT NULL THEN 1 ELSE 0 END) as consolidation_count
801
+ FROM memories
802
+ WHERE project = ? AND deleted_at IS NULL
803
+ `).get(project);
804
+ const stats = (statsRow ?? {});
805
+ // Zone distribution
806
+ const zoneRows = db.prepare(`
807
+ SELECT
808
+ CASE
809
+ WHEN distance < 1.0 THEN 'core'
810
+ WHEN distance < 5.0 THEN 'near'
811
+ WHEN distance < 15.0 THEN 'active'
812
+ WHEN distance < 40.0 THEN 'archive'
813
+ WHEN distance < 70.0 THEN 'fading'
814
+ ELSE 'forgotten'
815
+ END as zone,
816
+ COUNT(*) as count
817
+ FROM memories
818
+ WHERE project = ? AND deleted_at IS NULL
819
+ GROUP BY zone
820
+ `).all(project);
821
+ const zone_distribution = {};
822
+ for (const r of zoneRows) {
823
+ const row = r;
824
+ zone_distribution[row.zone] = row.count;
825
+ }
826
+ // Type distribution
827
+ const typeRows = db.prepare(`
828
+ SELECT type, COUNT(*) as count
829
+ FROM memories
830
+ WHERE project = ? AND deleted_at IS NULL
831
+ GROUP BY type
832
+ `).all(project);
833
+ const type_distribution = {};
834
+ for (const r of typeRows) {
835
+ const row = r;
836
+ type_distribution[row.type] = row.count;
837
+ }
838
+ // Conflict count
839
+ const conflictRow = db.prepare(`
840
+ SELECT COUNT(*) as count FROM memory_conflicts
841
+ WHERE project = ? AND status = 'open'
842
+ `).get(project);
843
+ const conflict_count = (conflictRow?.count) ?? 0;
844
+ // Activity timeline (last 30 days)
845
+ const timelineRows = getActivityTimeline(project, 30);
846
+ const activity_timeline = timelineRows.map((row) => ({
847
+ date: row.date,
848
+ created: row.created,
849
+ accessed: row.accessed,
850
+ forgotten: 0, // soft-delete count per day — simplified to 0 here
851
+ }));
852
+ return {
853
+ total_memories: stats.total_memories ?? 0,
854
+ zone_distribution,
855
+ type_distribution,
856
+ avg_quality: stats.avg_quality ?? 0.5,
857
+ avg_importance: stats.avg_importance ?? 0.5,
858
+ recall_success_rate: getRecallSuccessRate(project),
859
+ consolidation_count: stats.consolidation_count ?? 0,
860
+ conflict_count,
861
+ top_tags: getTopTags(project),
862
+ activity_timeline,
863
+ };
864
+ }
865
+ //# sourceMappingURL=queries.js.map