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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/api/routes/analytics.d.ts +15 -0
- package/dist/api/routes/analytics.js +131 -0
- package/dist/api/routes/analytics.js.map +1 -0
- package/dist/api/routes/conflicts.d.ts +12 -0
- package/dist/api/routes/conflicts.js +67 -0
- package/dist/api/routes/conflicts.js.map +1 -0
- package/dist/api/routes/consolidation.d.ts +11 -0
- package/dist/api/routes/consolidation.js +63 -0
- package/dist/api/routes/consolidation.js.map +1 -0
- package/dist/api/routes/constellation.d.ts +4 -0
- package/dist/api/routes/constellation.js +84 -0
- package/dist/api/routes/constellation.js.map +1 -0
- package/dist/api/routes/memories.d.ts +4 -0
- package/dist/api/routes/memories.js +219 -0
- package/dist/api/routes/memories.js.map +1 -0
- package/dist/api/routes/observations.d.ts +10 -0
- package/dist/api/routes/observations.js +42 -0
- package/dist/api/routes/observations.js.map +1 -0
- package/dist/api/routes/orbit.d.ts +4 -0
- package/dist/api/routes/orbit.js +71 -0
- package/dist/api/routes/orbit.js.map +1 -0
- package/dist/api/routes/projects.d.ts +15 -0
- package/dist/api/routes/projects.js +121 -0
- package/dist/api/routes/projects.js.map +1 -0
- package/dist/api/routes/scan.d.ts +4 -0
- package/dist/api/routes/scan.js +403 -0
- package/dist/api/routes/scan.js.map +1 -0
- package/dist/api/routes/sun.d.ts +4 -0
- package/dist/api/routes/sun.js +43 -0
- package/dist/api/routes/sun.js.map +1 -0
- package/dist/api/routes/system.d.ts +4 -0
- package/dist/api/routes/system.js +70 -0
- package/dist/api/routes/system.js.map +1 -0
- package/dist/api/routes/temporal.d.ts +13 -0
- package/dist/api/routes/temporal.js +82 -0
- package/dist/api/routes/temporal.js.map +1 -0
- package/dist/api/server.d.ts +2 -0
- package/dist/api/server.js +99 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/websocket.d.ts +53 -0
- package/dist/api/websocket.js +168 -0
- package/dist/api/websocket.js.map +1 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.js +35 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +10 -0
- package/dist/cli/init.js +163 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/engine/analytics.d.ts +93 -0
- package/dist/engine/analytics.js +437 -0
- package/dist/engine/analytics.js.map +1 -0
- package/dist/engine/conflict.d.ts +54 -0
- package/dist/engine/conflict.js +322 -0
- package/dist/engine/conflict.js.map +1 -0
- package/dist/engine/consolidation.d.ts +83 -0
- package/dist/engine/consolidation.js +368 -0
- package/dist/engine/consolidation.js.map +1 -0
- package/dist/engine/constellation.d.ts +66 -0
- package/dist/engine/constellation.js +382 -0
- package/dist/engine/constellation.js.map +1 -0
- package/dist/engine/corona.d.ts +53 -0
- package/dist/engine/corona.js +181 -0
- package/dist/engine/corona.js.map +1 -0
- package/dist/engine/embedding.d.ts +44 -0
- package/dist/engine/embedding.js +168 -0
- package/dist/engine/embedding.js.map +1 -0
- package/dist/engine/gravity.d.ts +63 -0
- package/dist/engine/gravity.js +121 -0
- package/dist/engine/gravity.js.map +1 -0
- package/dist/engine/multiproject.d.ts +75 -0
- package/dist/engine/multiproject.js +241 -0
- package/dist/engine/multiproject.js.map +1 -0
- package/dist/engine/observation.d.ts +82 -0
- package/dist/engine/observation.js +357 -0
- package/dist/engine/observation.js.map +1 -0
- package/dist/engine/orbit.d.ts +91 -0
- package/dist/engine/orbit.js +249 -0
- package/dist/engine/orbit.js.map +1 -0
- package/dist/engine/planet.d.ts +64 -0
- package/dist/engine/planet.js +432 -0
- package/dist/engine/planet.js.map +1 -0
- package/dist/engine/procedural.d.ts +71 -0
- package/dist/engine/procedural.js +259 -0
- package/dist/engine/procedural.js.map +1 -0
- package/dist/engine/quality.d.ts +48 -0
- package/dist/engine/quality.js +245 -0
- package/dist/engine/quality.js.map +1 -0
- package/dist/engine/repository.d.ts +79 -0
- package/dist/engine/repository.js +13 -0
- package/dist/engine/repository.js.map +1 -0
- package/dist/engine/sun.d.ts +61 -0
- package/dist/engine/sun.js +240 -0
- package/dist/engine/sun.js.map +1 -0
- package/dist/engine/temporal.d.ts +67 -0
- package/dist/engine/temporal.js +283 -0
- package/dist/engine/temporal.js.map +1 -0
- package/dist/engine/types.d.ts +179 -0
- package/dist/engine/types.js +27 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/connector-registry.d.ts +20 -0
- package/dist/mcp/connector-registry.js +35 -0
- package/dist/mcp/connector-registry.js.map +1 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +242 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/daemon-tool.d.ts +16 -0
- package/dist/mcp/tools/daemon-tool.js +58 -0
- package/dist/mcp/tools/daemon-tool.js.map +1 -0
- package/dist/mcp/tools/ingestion-tools.d.ts +20 -0
- package/dist/mcp/tools/ingestion-tools.js +34 -0
- package/dist/mcp/tools/ingestion-tools.js.map +1 -0
- package/dist/mcp/tools/memory-tools.d.ts +122 -0
- package/dist/mcp/tools/memory-tools.js +1037 -0
- package/dist/mcp/tools/memory-tools.js.map +1 -0
- package/dist/scanner/cloud/github.d.ts +34 -0
- package/dist/scanner/cloud/github.js +260 -0
- package/dist/scanner/cloud/github.js.map +1 -0
- package/dist/scanner/cloud/google-drive.d.ts +30 -0
- package/dist/scanner/cloud/google-drive.js +289 -0
- package/dist/scanner/cloud/google-drive.js.map +1 -0
- package/dist/scanner/cloud/notion.d.ts +33 -0
- package/dist/scanner/cloud/notion.js +231 -0
- package/dist/scanner/cloud/notion.js.map +1 -0
- package/dist/scanner/cloud/slack.d.ts +38 -0
- package/dist/scanner/cloud/slack.js +282 -0
- package/dist/scanner/cloud/slack.js.map +1 -0
- package/dist/scanner/cloud/types.d.ts +73 -0
- package/dist/scanner/cloud/types.js +9 -0
- package/dist/scanner/cloud/types.js.map +1 -0
- package/dist/scanner/index.d.ts +35 -0
- package/dist/scanner/index.js +420 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/local/filesystem.d.ts +33 -0
- package/dist/scanner/local/filesystem.js +203 -0
- package/dist/scanner/local/filesystem.js.map +1 -0
- package/dist/scanner/local/git.d.ts +24 -0
- package/dist/scanner/local/git.js +161 -0
- package/dist/scanner/local/git.js.map +1 -0
- package/dist/scanner/local/parsers/code.d.ts +3 -0
- package/dist/scanner/local/parsers/code.js +127 -0
- package/dist/scanner/local/parsers/code.js.map +1 -0
- package/dist/scanner/local/parsers/index.d.ts +11 -0
- package/dist/scanner/local/parsers/index.js +24 -0
- package/dist/scanner/local/parsers/index.js.map +1 -0
- package/dist/scanner/local/parsers/json-parser.d.ts +3 -0
- package/dist/scanner/local/parsers/json-parser.js +117 -0
- package/dist/scanner/local/parsers/json-parser.js.map +1 -0
- package/dist/scanner/local/parsers/markdown.d.ts +3 -0
- package/dist/scanner/local/parsers/markdown.js +120 -0
- package/dist/scanner/local/parsers/markdown.js.map +1 -0
- package/dist/scanner/local/parsers/text.d.ts +3 -0
- package/dist/scanner/local/parsers/text.js +41 -0
- package/dist/scanner/local/parsers/text.js.map +1 -0
- package/dist/scanner/metadata-scanner.d.ts +67 -0
- package/dist/scanner/metadata-scanner.js +356 -0
- package/dist/scanner/metadata-scanner.js.map +1 -0
- package/dist/scanner/types.d.ts +47 -0
- package/dist/scanner/types.js +19 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/service/daemon.d.ts +23 -0
- package/dist/service/daemon.js +105 -0
- package/dist/service/daemon.js.map +1 -0
- package/dist/service/scheduler.d.ts +73 -0
- package/dist/service/scheduler.js +281 -0
- package/dist/service/scheduler.js.map +1 -0
- package/dist/storage/database.d.ts +10 -0
- package/dist/storage/database.js +265 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/queries.d.ts +85 -0
- package/dist/storage/queries.js +865 -0
- package/dist/storage/queries.js.map +1 -0
- package/dist/storage/sqlite-repository.d.ts +32 -0
- package/dist/storage/sqlite-repository.js +68 -0
- package/dist/storage/sqlite-repository.js.map +1 -0
- package/dist/storage/vec.d.ts +62 -0
- package/dist/storage/vec.js +111 -0
- package/dist/storage/vec.js.map +1 -0
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +60 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/time.d.ts +21 -0
- package/dist/utils/time.js +42 -0
- package/dist/utils/time.js.map +1 -0
- package/dist/utils/tokenizer.d.ts +13 -0
- package/dist/utils/tokenizer.js +46 -0
- package/dist/utils/tokenizer.js.map +1 -0
- package/package.json +77 -0
- package/scripts/check-node.mjs +36 -0
- 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
|