@reqlan/analytical 0.4.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 (97) hide show
  1. package/out/analysis/analyser-registry.d.ts +21 -0
  2. package/out/analysis/analyser-registry.js +33 -0
  3. package/out/analysis/analyser-registry.js.map +1 -0
  4. package/out/analysis/completion-tracking-analyser.d.ts +3 -0
  5. package/out/analysis/completion-tracking-analyser.js +37 -0
  6. package/out/analysis/completion-tracking-analyser.js.map +1 -0
  7. package/out/analysis/deprecation-impact-analyser.d.ts +3 -0
  8. package/out/analysis/deprecation-impact-analyser.js +21 -0
  9. package/out/analysis/deprecation-impact-analyser.js.map +1 -0
  10. package/out/analysis/file-related-analyser.d.ts +5 -0
  11. package/out/analysis/file-related-analyser.js +41 -0
  12. package/out/analysis/file-related-analyser.js.map +1 -0
  13. package/out/analysis/git-dates-analyser.d.ts +9 -0
  14. package/out/analysis/git-dates-analyser.js +39 -0
  15. package/out/analysis/git-dates-analyser.js.map +1 -0
  16. package/out/analysis/list-ideas-analyser.d.ts +3 -0
  17. package/out/analysis/list-ideas-analyser.js +7 -0
  18. package/out/analysis/list-ideas-analyser.js.map +1 -0
  19. package/out/analysis/local-graph-analyser.d.ts +6 -0
  20. package/out/analysis/local-graph-analyser.js +42 -0
  21. package/out/analysis/local-graph-analyser.js.map +1 -0
  22. package/out/analysis/semantic-search-analyser.d.ts +8 -0
  23. package/out/analysis/semantic-search-analyser.js +51 -0
  24. package/out/analysis/semantic-search-analyser.js.map +1 -0
  25. package/out/analysis-api.d.ts +31 -0
  26. package/out/analysis-api.js +136 -0
  27. package/out/analysis-api.js.map +1 -0
  28. package/out/core/analytical-store.d.ts +78 -0
  29. package/out/core/analytical-store.js +141 -0
  30. package/out/core/analytical-store.js.map +1 -0
  31. package/out/core/context-model.d.ts +187 -0
  32. package/out/core/context-model.js +36 -0
  33. package/out/core/context-model.js.map +1 -0
  34. package/out/core/context-signals.d.ts +149 -0
  35. package/out/core/context-signals.js +397 -0
  36. package/out/core/context-signals.js.map +1 -0
  37. package/out/core/file-path-match.d.ts +7 -0
  38. package/out/core/file-path-match.js +39 -0
  39. package/out/core/file-path-match.js.map +1 -0
  40. package/out/core/file-reference-resolve.d.ts +8 -0
  41. package/out/core/file-reference-resolve.js +28 -0
  42. package/out/core/file-reference-resolve.js.map +1 -0
  43. package/out/core/idea-references.d.ts +13 -0
  44. package/out/core/idea-references.js +25 -0
  45. package/out/core/idea-references.js.map +1 -0
  46. package/out/core/index-error.d.ts +23 -0
  47. package/out/core/index-error.js +33 -0
  48. package/out/core/index-error.js.map +1 -0
  49. package/out/core/types.d.ts +172 -0
  50. package/out/core/types.js +48 -0
  51. package/out/core/types.js.map +1 -0
  52. package/out/core/workspace-paths.d.ts +4 -0
  53. package/out/core/workspace-paths.js +92 -0
  54. package/out/core/workspace-paths.js.map +1 -0
  55. package/out/create-runtime.d.ts +21 -0
  56. package/out/create-runtime.js +39 -0
  57. package/out/create-runtime.js.map +1 -0
  58. package/out/export/build-export-snapshot.d.ts +3 -0
  59. package/out/export/build-export-snapshot.js +573 -0
  60. package/out/export/build-export-snapshot.js.map +1 -0
  61. package/out/export/html-export-assets.d.ts +3 -0
  62. package/out/export/html-export-assets.js +1066 -0
  63. package/out/export/html-export-assets.js.map +1 -0
  64. package/out/export/html-export-template.d.ts +17 -0
  65. package/out/export/html-export-template.js +930 -0
  66. package/out/export/html-export-template.js.map +1 -0
  67. package/out/export/html-export-utils.d.ts +45 -0
  68. package/out/export/html-export-utils.js +262 -0
  69. package/out/export/html-export-utils.js.map +1 -0
  70. package/out/export/types.d.ts +187 -0
  71. package/out/export/types.js +2 -0
  72. package/out/export/types.js.map +1 -0
  73. package/out/export/write-html-export.d.ts +2 -0
  74. package/out/export/write-html-export.js +143 -0
  75. package/out/export/write-html-export.js.map +1 -0
  76. package/out/headless-index-service.d.ts +23 -0
  77. package/out/headless-index-service.js +171 -0
  78. package/out/headless-index-service.js.map +1 -0
  79. package/out/index-store/idea-extractor.d.ts +6 -0
  80. package/out/index-store/idea-extractor.js +271 -0
  81. package/out/index-store/idea-extractor.js.map +1 -0
  82. package/out/index-store/schema.d.ts +10 -0
  83. package/out/index-store/schema.js +54 -0
  84. package/out/index-store/schema.js.map +1 -0
  85. package/out/index-store/sqlite-store.d.ts +66 -0
  86. package/out/index-store/sqlite-store.js +964 -0
  87. package/out/index-store/sqlite-store.js.map +1 -0
  88. package/out/index-store/webview-graph-queries.d.ts +62 -0
  89. package/out/index-store/webview-graph-queries.js +256 -0
  90. package/out/index-store/webview-graph-queries.js.map +1 -0
  91. package/out/index-store/webview-table-queries.d.ts +57 -0
  92. package/out/index-store/webview-table-queries.js +199 -0
  93. package/out/index-store/webview-table-queries.js.map +1 -0
  94. package/out/index.d.ts +40 -0
  95. package/out/index.js +27 -0
  96. package/out/index.js.map +1 -0
  97. package/package.json +52 -0
@@ -0,0 +1,964 @@
1
+ /**
2
+ * SQLite-backed persistence for the workspace idea graph index.
3
+ * Uses a bundled asm.js build so VSIX installs do not depend on native modules.
4
+ */
5
+ import initSqlJs from 'sql.js/dist/sql-asm.js';
6
+ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
7
+ import { basename, dirname } from 'node:path';
8
+ import { BASE_MIGRATIONS, SCHEMA_VERSION, VERSION_MIGRATIONS } from './schema.js';
9
+ import { BLOCKING_STATUSES, ideaStatus, ideaTags, parseAttributes } from '../core/types.js';
10
+ import { resolveReferencedFilePath } from '../core/file-reference-resolve.js';
11
+ import { buildGraphTruncationOrderClause } from './webview-graph-queries.js';
12
+ import { attributeValuesForKeys, buildIdeasetsOrderClause, buildIdeasetsWhereClause, buildIdeasOrderClause, buildIdeasWhereClause, buildReferencesOrderClause, buildReferencesWhereClause } from './webview-table-queries.js';
13
+ import { buildGraphFilterWhereClause } from './webview-graph-queries.js';
14
+ export class SqliteIndexStore {
15
+ constructor(db) {
16
+ this.db = db;
17
+ }
18
+ static async open(dbPath) {
19
+ const db = await openDatabase(dbPath);
20
+ await run(db, 'PRAGMA foreign_keys = ON');
21
+ const store = new SqliteIndexStore(db);
22
+ await store.migrate();
23
+ return store;
24
+ }
25
+ async close() {
26
+ await closeDatabase(this.db);
27
+ }
28
+ /** Close the in-memory database without writing back to disk. */
29
+ closeWithoutPersist() {
30
+ this.db.db.close();
31
+ }
32
+ static async deleteDatabaseFile(dbPath) {
33
+ try {
34
+ await unlink(dbPath);
35
+ }
36
+ catch (error) {
37
+ if (!isMissingFileError(error)) {
38
+ throw error;
39
+ }
40
+ }
41
+ }
42
+ async getDocumentHash(fileUri) {
43
+ const row = await get(this.db, 'SELECT content_hash FROM documents WHERE file_uri = ?', fileUri);
44
+ return row?.content_hash;
45
+ }
46
+ async upsertDocument(fileUri, contentHash, ideas, edges) {
47
+ await run(this.db, 'BEGIN');
48
+ try {
49
+ await run(this.db, 'DELETE FROM edges WHERE source_id IN (SELECT id FROM ideas WHERE file_uri = ?)', fileUri);
50
+ await run(this.db, 'DELETE FROM ideas WHERE file_uri = ?', fileUri);
51
+ const insertIdeaSql = `
52
+ INSERT INTO ideas (
53
+ id, name, kind, file_uri, line_start, line_end, summary,
54
+ attributes_json, content_hash, git_created_at, git_modified_at
55
+ ) VALUES (
56
+ ?, ?, ?, ?, ?, ?, ?,
57
+ ?, ?, ?, ?
58
+ )
59
+ `;
60
+ for (const idea of ideas) {
61
+ await run(this.db, insertIdeaSql, idea.id, idea.name, idea.kind, idea.fileUri, idea.lineStart, idea.lineEnd, idea.summary, idea.attributesJson, idea.contentHash, idea.gitCreatedAt ?? null, idea.gitModifiedAt ?? null);
62
+ }
63
+ const insertEdgeSql = `
64
+ INSERT OR IGNORE INTO edges (
65
+ id, source_id, target_id, target_file, kind, label,
66
+ source_line, snippet, is_resolved
67
+ )
68
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
69
+ `;
70
+ for (const edge of edges) {
71
+ await run(this.db, insertEdgeSql, edge.id, edge.sourceId, edge.targetId ?? null, edge.targetFile ?? null, edge.kind, edge.label ?? null, edge.sourceLine ?? null, edge.snippet ?? null, edge.isResolved === false ? 0 : 1);
72
+ }
73
+ await run(this.db, `
74
+ INSERT INTO documents (file_uri, content_hash, indexed_at)
75
+ VALUES (?, ?, datetime('now'))
76
+ ON CONFLICT(file_uri) DO UPDATE SET
77
+ content_hash = excluded.content_hash,
78
+ indexed_at = excluded.indexed_at
79
+ `, fileUri, contentHash);
80
+ await run(this.db, 'COMMIT');
81
+ }
82
+ catch (error) {
83
+ await run(this.db, 'ROLLBACK');
84
+ throw error;
85
+ }
86
+ }
87
+ async removeDocument(fileUri) {
88
+ await run(this.db, 'BEGIN');
89
+ try {
90
+ await run(this.db, 'DELETE FROM edges WHERE source_id IN (SELECT id FROM ideas WHERE file_uri = ?)', fileUri);
91
+ await run(this.db, 'DELETE FROM ideas WHERE file_uri = ?', fileUri);
92
+ await run(this.db, 'DELETE FROM documents WHERE file_uri = ?', fileUri);
93
+ await run(this.db, 'COMMIT');
94
+ }
95
+ catch (error) {
96
+ await run(this.db, 'ROLLBACK');
97
+ throw error;
98
+ }
99
+ }
100
+ async clearAll() {
101
+ await run(this.db, 'BEGIN');
102
+ try {
103
+ await run(this.db, 'DELETE FROM edges');
104
+ await run(this.db, 'DELETE FROM ideas');
105
+ await run(this.db, 'DELETE FROM documents');
106
+ await run(this.db, 'COMMIT');
107
+ }
108
+ catch (error) {
109
+ await run(this.db, 'ROLLBACK');
110
+ throw error;
111
+ }
112
+ }
113
+ async listAllIdeas() {
114
+ const rows = await all(this.db, `
115
+ SELECT id, name, kind, file_uri, line_start, summary, attributes_json
116
+ FROM ideas
117
+ ORDER BY file_uri, line_start
118
+ `);
119
+ return rows.map(row => this.toSummary(row));
120
+ }
121
+ async getIdea(id) {
122
+ const row = await get(this.db, `
123
+ SELECT id, name, kind, file_uri, line_start, summary, attributes_json,
124
+ git_created_at, git_modified_at
125
+ FROM ideas WHERE id = ?
126
+ `, id);
127
+ return row ? this.toSummary(row) : undefined;
128
+ }
129
+ async getIdeasInFile(fileUri) {
130
+ const rows = await all(this.db, `
131
+ SELECT id, name, kind, file_uri, line_start, summary, attributes_json
132
+ FROM ideas WHERE file_uri = ?
133
+ ORDER BY line_start
134
+ `, fileUri);
135
+ return rows.map(row => this.toSummary(row));
136
+ }
137
+ async getIdeaAtLine(fileUri, line) {
138
+ const row = await get(this.db, `
139
+ SELECT id, name, kind, file_uri, line_start, summary, attributes_json,
140
+ git_created_at, git_modified_at
141
+ FROM ideas
142
+ WHERE file_uri = ? AND line_start <= ? AND ? <= line_end AND kind != 'ideaset'
143
+ ORDER BY line_start DESC
144
+ LIMIT 1
145
+ `, fileUri, line, line);
146
+ return row ? this.toSummary(row) : undefined;
147
+ }
148
+ /** Innermost ideaset whose line range contains `line` (0-based). */
149
+ async getIdeasetAtLine(fileUri, line) {
150
+ const row = await get(this.db, `
151
+ SELECT id, name, kind, file_uri, line_start, summary, attributes_json,
152
+ git_created_at, git_modified_at
153
+ FROM ideas
154
+ WHERE file_uri = ? AND line_start <= ? AND ? <= line_end AND kind = 'ideaset'
155
+ ORDER BY line_start DESC
156
+ LIMIT 1
157
+ `, fileUri, line, line);
158
+ return row ? this.toSummary(row) : undefined;
159
+ }
160
+ async listIdeasInFileWithRanges(fileUri) {
161
+ const rows = await all(this.db, `
162
+ SELECT id, name, kind, file_uri, line_start, line_end, summary, attributes_json,
163
+ git_created_at, git_modified_at
164
+ FROM ideas
165
+ WHERE file_uri = ? AND kind != 'ideaset'
166
+ ORDER BY line_start ASC
167
+ `, fileUri);
168
+ return rows.map(row => ({
169
+ ...this.toSummary(row),
170
+ lineEnd: row.line_end
171
+ }));
172
+ }
173
+ async listIdeasetsInFileWithRanges(fileUri) {
174
+ const rows = await all(this.db, `
175
+ SELECT id, name, kind, file_uri, line_start, line_end, summary, attributes_json,
176
+ git_created_at, git_modified_at
177
+ FROM ideas
178
+ WHERE file_uri = ? AND kind = 'ideaset'
179
+ ORDER BY line_start ASC
180
+ `, fileUri);
181
+ return rows.map(row => ({
182
+ ...this.toSummary(row),
183
+ lineEnd: row.line_end
184
+ }));
185
+ }
186
+ async listReferencesForIdea(ideaId) {
187
+ const outbound = await all(this.db, `
188
+ SELECT
189
+ e.id AS edge_id,
190
+ e.kind,
191
+ e.source_id,
192
+ e.target_id,
193
+ e.target_file,
194
+ e.label,
195
+ e.source_line,
196
+ e.snippet,
197
+ e.is_resolved,
198
+ ti.name AS target_name,
199
+ ti.file_uri AS target_uri,
200
+ ti.line_start AS target_line
201
+ FROM edges e
202
+ LEFT JOIN ideas ti ON ti.id = e.target_id
203
+ WHERE e.source_id = ?
204
+ ORDER BY e.kind, e.id
205
+ `, ideaId);
206
+ const inbound = await all(this.db, `
207
+ SELECT
208
+ e.id AS edge_id,
209
+ e.kind,
210
+ e.source_id,
211
+ e.target_id,
212
+ e.target_file,
213
+ e.label,
214
+ e.source_line,
215
+ e.snippet,
216
+ e.is_resolved,
217
+ si.name AS source_name,
218
+ si.file_uri AS source_uri,
219
+ si.line_start AS source_line_idea
220
+ FROM edges e
221
+ JOIN ideas si ON si.id = e.source_id
222
+ WHERE e.target_id = ?
223
+ ORDER BY e.kind, e.id
224
+ `, ideaId);
225
+ const rows = [];
226
+ for (const row of outbound) {
227
+ rows.push(toOutboundReferenceListRow(row));
228
+ }
229
+ for (const row of inbound) {
230
+ rows.push(toInboundReferenceListRow(row));
231
+ }
232
+ return rows;
233
+ }
234
+ async listReferencesWithinHopDepth(ideaId, hopDepth) {
235
+ const depth = Math.max(1, Math.round(hopDepth));
236
+ const edgeIds = new Set();
237
+ const rows = [];
238
+ const visited = new Set();
239
+ let frontier = [ideaId];
240
+ for (let level = 0; level < depth && frontier.length > 0; level += 1) {
241
+ const nextFrontier = new Set();
242
+ for (const id of frontier) {
243
+ if (visited.has(id)) {
244
+ continue;
245
+ }
246
+ visited.add(id);
247
+ for (const row of await this.listReferencesForIdea(id)) {
248
+ if (!edgeIds.has(row.edgeId)) {
249
+ edgeIds.add(row.edgeId);
250
+ rows.push(row);
251
+ }
252
+ const neighbor = row.direction === 'inbound' ? row.sourceIdeaId : row.targetIdeaId;
253
+ if (neighbor && !visited.has(neighbor)) {
254
+ nextFrontier.add(neighbor);
255
+ }
256
+ }
257
+ }
258
+ frontier = [...nextFrontier];
259
+ }
260
+ return rows;
261
+ }
262
+ async countUnresolvedForIdea(ideaId) {
263
+ return (await get(this.db, `
264
+ SELECT COUNT(*) AS count FROM edges
265
+ WHERE source_id = ? AND is_resolved = 0
266
+ `, ideaId)).count;
267
+ }
268
+ async countEdgesFromFile(fileUri) {
269
+ return (await get(this.db, `
270
+ SELECT COUNT(*) AS count
271
+ FROM edges
272
+ WHERE source_id IN (SELECT id FROM ideas WHERE file_uri = ?)
273
+ `, fileUri)).count;
274
+ }
275
+ async listAncestorChain(ideaId, maxDepth = 8) {
276
+ const ancestors = [];
277
+ const visited = new Set([ideaId]);
278
+ let frontier = [ideaId];
279
+ for (let depth = 0; depth < maxDepth; depth++) {
280
+ const nextFrontier = [];
281
+ for (const currentId of frontier) {
282
+ for (const edge of await this.getEdgesFrom(currentId)) {
283
+ if (edge.kind !== 'references' || !edge.targetId || visited.has(edge.targetId)) {
284
+ continue;
285
+ }
286
+ visited.add(edge.targetId);
287
+ const idea = await this.getIdea(edge.targetId);
288
+ if (idea) {
289
+ ancestors.push(idea);
290
+ nextFrontier.push(edge.targetId);
291
+ }
292
+ }
293
+ }
294
+ if (nextFrontier.length === 0) {
295
+ break;
296
+ }
297
+ frontier = nextFrontier;
298
+ }
299
+ return ancestors;
300
+ }
301
+ async buildAncestorChainResult(ideaId, maxDepth = 8) {
302
+ const ancestors = await this.listAncestorChain(ideaId, maxDepth);
303
+ const statusRollup = {};
304
+ const blocking = [];
305
+ for (const ancestor of ancestors) {
306
+ const status = (ancestor.status ?? 'unspecified').toLowerCase();
307
+ statusRollup[status] = (statusRollup[status] ?? 0) + 1;
308
+ if (BLOCKING_STATUSES.has(status)) {
309
+ blocking.push(ancestor);
310
+ }
311
+ }
312
+ return { ideaId, ancestors, statusRollup, blocking };
313
+ }
314
+ async getEdgesFrom(sourceId) {
315
+ const rows = await all(this.db, 'SELECT * FROM edges WHERE source_id = ?', sourceId);
316
+ return rows.map(mapEdgeRow);
317
+ }
318
+ async getEdgesTo(targetId) {
319
+ const rows = await all(this.db, 'SELECT * FROM edges WHERE target_id = ?', targetId);
320
+ return rows.map(mapEdgeRow);
321
+ }
322
+ /** Batched fetch of every edge touching any of the given node ids (source or target). */
323
+ async getEdgesForNodes(nodeIds) {
324
+ if (nodeIds.length === 0) {
325
+ return [];
326
+ }
327
+ const placeholders = nodeIds.map(() => '?').join(',');
328
+ const rows = await all(this.db, `SELECT * FROM edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`, ...nodeIds, ...nodeIds);
329
+ return rows.map(mapEdgeRow);
330
+ }
331
+ /** Batched fetch of idea summaries by id, preserving no particular order. */
332
+ async getIdeasByIds(ids) {
333
+ if (ids.length === 0) {
334
+ return [];
335
+ }
336
+ const placeholders = ids.map(() => '?').join(',');
337
+ const rows = await all(this.db, `SELECT id, name, kind, file_uri, line_start, summary, attributes_json
338
+ FROM ideas WHERE id IN (${placeholders})`, ...ids);
339
+ return rows.map(row => this.toSummary(row));
340
+ }
341
+ async getEdgesReferencingFile(filePath) {
342
+ const rows = await all(this.db, `
343
+ SELECT * FROM edges
344
+ WHERE target_file LIKE ? OR target_file LIKE ?
345
+ `, `%${filePath}%`, filePath);
346
+ return rows.map(mapEdgeRow);
347
+ }
348
+ async getAllEdges() {
349
+ const rows = await all(this.db, 'SELECT * FROM edges');
350
+ return rows.map(mapEdgeRow);
351
+ }
352
+ async getAllIdeasRaw() {
353
+ const rows = await all(this.db, 'SELECT * FROM ideas');
354
+ return rows.map(mapIdeaRow);
355
+ }
356
+ async updateGitDates(id, createdAt, modifiedAt) {
357
+ await run(this.db, `
358
+ UPDATE ideas SET git_created_at = ?, git_modified_at = ?
359
+ WHERE id = ?
360
+ `, createdAt ?? null, modifiedAt ?? null, id);
361
+ }
362
+ async counts() {
363
+ const ideas = (await get(this.db, 'SELECT COUNT(*) as count FROM ideas')).count;
364
+ const edges = (await get(this.db, 'SELECT COUNT(*) as count FROM edges')).count;
365
+ return { ideas, edges };
366
+ }
367
+ async getAttributeCatalog() {
368
+ const ideas = await this.getAllIdeasRaw();
369
+ const keys = new Set();
370
+ const valuesByKey = new Map();
371
+ for (const idea of ideas) {
372
+ const attributes = parseAttributes(idea.attributesJson);
373
+ for (const [key, value] of Object.entries(attributes)) {
374
+ keys.add(key);
375
+ if (!valuesByKey.has(key)) {
376
+ valuesByKey.set(key, new Set());
377
+ }
378
+ const bucket = valuesByKey.get(key);
379
+ if (typeof value === 'string' && value) {
380
+ bucket.add(value);
381
+ }
382
+ else if (Array.isArray(value)) {
383
+ for (const entry of value) {
384
+ if (entry) {
385
+ bucket.add(String(entry));
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+ return {
392
+ keys: [...keys].sort((left, right) => left.localeCompare(right)),
393
+ valuesByKey: Object.fromEntries([...valuesByKey.entries()].map(([key, values]) => [key, [...values].sort()]))
394
+ };
395
+ }
396
+ async searchByNameOrSummary(query) {
397
+ const pattern = `%${query}%`;
398
+ const rows = await all(this.db, `
399
+ SELECT id, name, kind, file_uri, line_start, summary, attributes_json
400
+ FROM ideas
401
+ WHERE name LIKE ? OR summary LIKE ?
402
+ ORDER BY name
403
+ `, pattern, pattern);
404
+ return rows.map(row => this.toSummary(row));
405
+ }
406
+ async listIdeasForGraphQuery(query, limit) {
407
+ const { sql, params } = buildGraphFilterWhereClause(query);
408
+ const orderSql = buildGraphTruncationOrderClause(query.truncationBasis);
409
+ const totalMatching = (await get(this.db, `SELECT COUNT(*) as count FROM ideas i WHERE ${sql}`, ...params)).count;
410
+ const rows = await all(this.db, `
411
+ SELECT id, name, kind, file_uri, line_start, summary, attributes_json
412
+ FROM ideas i
413
+ WHERE ${sql}
414
+ ORDER BY ${orderSql}
415
+ LIMIT ?
416
+ `, ...params, limit);
417
+ return {
418
+ candidates: rows.map(row => this.toSummary(row)),
419
+ totalMatching
420
+ };
421
+ }
422
+ async countIdeas(query = { page: 0, pageSize: 50, attributeColumns: [], referenceFilters: [] }) {
423
+ const { sql, params } = buildIdeasWhereClause(query);
424
+ return (await get(this.db, `SELECT COUNT(*) as count FROM ideas i WHERE ${sql}`, ...params)).count;
425
+ }
426
+ async listIdeasPage(query) {
427
+ const { sql: whereSql, params: whereParams } = buildIdeasWhereClause(query);
428
+ const orderSql = buildIdeasOrderClause(query);
429
+ const rows = await all(this.db, `
430
+ SELECT
431
+ i.id,
432
+ i.name,
433
+ i.file_uri,
434
+ i.line_start,
435
+ i.summary,
436
+ i.attributes_json,
437
+ (
438
+ SELECT COUNT(*)
439
+ FROM edges e
440
+ WHERE e.source_id = i.id
441
+ ) AS outbound_count,
442
+ (
443
+ SELECT COUNT(*)
444
+ FROM edges e
445
+ WHERE e.target_id = i.id
446
+ ) AS inbound_count,
447
+ (
448
+ SELECT COUNT(*)
449
+ FROM edges e
450
+ WHERE e.source_id = i.id OR e.target_id = i.id
451
+ ) AS reference_count
452
+ FROM ideas i
453
+ WHERE ${whereSql}
454
+ ORDER BY ${orderSql}
455
+ LIMIT ? OFFSET ?
456
+ `, ...whereParams, query.pageSize, query.page * query.pageSize);
457
+ const ideaIds = rows.map(row => row.id);
458
+ const referencesByIdea = await this.listReferenceChipsForIdeas(ideaIds);
459
+ return rows.map(row => {
460
+ const references = referencesByIdea.get(row.id) ?? { outbound: [], inbound: [] };
461
+ return toIdeaTableRow(row, query.attributeColumns, references.outbound, references.inbound);
462
+ });
463
+ }
464
+ async listReferenceChipsForIdeas(ideaIds) {
465
+ const result = new Map();
466
+ if (ideaIds.length === 0) {
467
+ return result;
468
+ }
469
+ const placeholders = ideaIds.map(() => '?').join(', ');
470
+ const rows = await all(this.db, `
471
+ SELECT
472
+ e.id,
473
+ e.kind,
474
+ e.source_id,
475
+ e.target_id,
476
+ e.target_file,
477
+ e.label,
478
+ si.name AS source_name,
479
+ si.file_uri AS source_uri,
480
+ si.line_start AS source_line,
481
+ ti.name AS target_name,
482
+ ti.file_uri AS target_uri,
483
+ ti.line_start AS target_line
484
+ FROM edges e
485
+ JOIN ideas si ON si.id = e.source_id
486
+ LEFT JOIN ideas ti ON ti.id = e.target_id
487
+ WHERE e.source_id IN (${placeholders}) OR e.target_id IN (${placeholders})
488
+ ORDER BY e.id
489
+ `, ...ideaIds, ...ideaIds);
490
+ for (const ideaId of ideaIds) {
491
+ result.set(ideaId, { outbound: [], inbound: [] });
492
+ }
493
+ for (const row of rows) {
494
+ if (ideaIds.includes(row.source_id)) {
495
+ result.get(row.source_id).outbound.push(toOutgoingReferenceChip(row));
496
+ }
497
+ if (row.target_id && ideaIds.includes(row.target_id)) {
498
+ result.get(row.target_id).inbound.push(toIncomingReferenceChip(row));
499
+ }
500
+ }
501
+ return result;
502
+ }
503
+ async countIdeasets(query = { page: 0, pageSize: 50 }) {
504
+ const { sql, params } = buildIdeasetsWhereClause(query);
505
+ const row = await get(this.db, `
506
+ SELECT COUNT(*) AS count
507
+ FROM (
508
+ SELECT
509
+ d.file_uri AS id,
510
+ 'file' AS kind,
511
+ d.file_uri AS file_uri,
512
+ 0 AS line_start,
513
+ NULL AS name,
514
+ (
515
+ SELECT COUNT(*)
516
+ FROM ideas i
517
+ WHERE i.file_uri = d.file_uri
518
+ ) AS member_count
519
+ FROM documents d
520
+ UNION ALL
521
+ SELECT
522
+ i.id,
523
+ 'explicit' AS kind,
524
+ i.file_uri,
525
+ i.line_start,
526
+ i.name,
527
+ (
528
+ SELECT COUNT(*)
529
+ FROM edges e
530
+ WHERE e.source_id = i.id AND e.kind = 'ideaset_member'
531
+ ) AS member_count
532
+ FROM ideas i
533
+ WHERE i.kind = 'ideaset'
534
+ ) ideasets
535
+ WHERE ${sql}
536
+ `, ...params);
537
+ return row?.count ?? 0;
538
+ }
539
+ async listIdeasetsPage(query) {
540
+ const { sql: whereSql, params: whereParams } = buildIdeasetsWhereClause(query);
541
+ const orderSql = buildIdeasetsOrderClause(query);
542
+ const rows = await all(this.db, `
543
+ SELECT *
544
+ FROM (
545
+ SELECT
546
+ d.file_uri AS id,
547
+ 'file' AS kind,
548
+ d.file_uri AS file_uri,
549
+ 0 AS line_start,
550
+ NULL AS name,
551
+ (
552
+ SELECT COUNT(*)
553
+ FROM ideas i
554
+ WHERE i.file_uri = d.file_uri
555
+ ) AS member_count
556
+ FROM documents d
557
+ UNION ALL
558
+ SELECT
559
+ i.id,
560
+ 'explicit' AS kind,
561
+ i.file_uri,
562
+ i.line_start,
563
+ i.name,
564
+ (
565
+ SELECT COUNT(*)
566
+ FROM edges e
567
+ WHERE e.source_id = i.id AND e.kind = 'ideaset_member'
568
+ ) AS member_count
569
+ FROM ideas i
570
+ WHERE i.kind = 'ideaset'
571
+ ) ideasets
572
+ WHERE ${whereSql}
573
+ ORDER BY ${orderSql}
574
+ LIMIT ? OFFSET ?
575
+ `, ...whereParams, query.pageSize, query.page * query.pageSize);
576
+ const ideasets = rows.map(row => toIdeasetTableRow(row));
577
+ return Promise.all(ideasets.map(async (ideaset) => ({
578
+ ...ideaset,
579
+ members: await this.listIdeasetMembers(ideaset.id, ideaset.kind, ideaset.fileUri)
580
+ })));
581
+ }
582
+ async listIdeasetMembers(ideasetId, kind, fileUri) {
583
+ const rows = kind === 'file'
584
+ ? await all(this.db, `
585
+ SELECT name, file_uri, line_start
586
+ FROM ideas
587
+ WHERE file_uri = ? AND kind != 'ideaset'
588
+ ORDER BY line_start
589
+ `, fileUri)
590
+ : await all(this.db, `
591
+ SELECT i.name, i.file_uri, i.line_start
592
+ FROM edges e
593
+ JOIN ideas i ON i.id = e.target_id
594
+ WHERE e.source_id = ? AND e.kind = 'ideaset_member'
595
+ ORDER BY i.line_start
596
+ `, ideasetId);
597
+ return rows.map(row => ({
598
+ name: row.name,
599
+ fileUri: row.file_uri,
600
+ lineStart: row.line_start
601
+ }));
602
+ }
603
+ async countReferences(query = { page: 0, pageSize: 50 }) {
604
+ const { sql, params } = buildReferencesWhereClause(query);
605
+ return (await get(this.db, `
606
+ SELECT COUNT(*) as count
607
+ FROM edges e
608
+ JOIN ideas si ON si.id = e.source_id
609
+ LEFT JOIN ideas ti ON ti.id = e.target_id
610
+ WHERE ${sql}
611
+ `, ...params)).count;
612
+ }
613
+ async listReferencesPage(query) {
614
+ const { sql: whereSql, params: whereParams } = buildReferencesWhereClause(query);
615
+ const orderSql = buildReferencesOrderClause(query);
616
+ const rows = await all(this.db, `
617
+ SELECT
618
+ e.kind,
619
+ e.target_id,
620
+ e.target_file,
621
+ e.label,
622
+ si.name AS source_name,
623
+ si.file_uri AS source_uri,
624
+ si.line_start AS source_line,
625
+ ti.name AS target_name,
626
+ ti.file_uri AS target_uri
627
+ FROM edges e
628
+ JOIN ideas si ON si.id = e.source_id
629
+ LEFT JOIN ideas ti ON ti.id = e.target_id
630
+ WHERE ${whereSql}
631
+ ORDER BY ${orderSql}
632
+ LIMIT ? OFFSET ?
633
+ `, ...whereParams, query.pageSize, query.page * query.pageSize);
634
+ return rows.map(row => toReferenceTableRow(row));
635
+ }
636
+ toSummary(row) {
637
+ const attributes = parseAttributes(row.attributes_json);
638
+ return {
639
+ id: row.id,
640
+ name: row.name,
641
+ kind: row.kind,
642
+ fileUri: row.file_uri,
643
+ lineStart: row.line_start,
644
+ summary: row.summary,
645
+ status: ideaStatus(attributes),
646
+ tags: ideaTags(attributes),
647
+ gitCreatedAt: row.git_created_at ?? undefined,
648
+ gitModifiedAt: row.git_modified_at ?? undefined
649
+ };
650
+ }
651
+ async migrate() {
652
+ for (const statement of BASE_MIGRATIONS) {
653
+ await exec(this.db, statement);
654
+ }
655
+ const currentVersion = Number((await get(this.db, `SELECT value FROM meta WHERE key = 'schema_version'`))?.value ?? '1');
656
+ for (let version = currentVersion + 1; version <= SCHEMA_VERSION; version++) {
657
+ const steps = VERSION_MIGRATIONS[version] ?? [];
658
+ for (const statement of steps) {
659
+ try {
660
+ await exec(this.db, statement);
661
+ }
662
+ catch (error) {
663
+ const message = error instanceof Error ? error.message : String(error);
664
+ if (!message.includes('duplicate column')) {
665
+ throw error;
666
+ }
667
+ }
668
+ }
669
+ }
670
+ await run(this.db, `
671
+ INSERT INTO meta (key, value) VALUES (?, ?)
672
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
673
+ `, 'schema_version', String(SCHEMA_VERSION));
674
+ }
675
+ }
676
+ async function openDatabase(path) {
677
+ await mkdir(dirname(path), { recursive: true });
678
+ const SQL = await getSqlJsModule();
679
+ const bytes = await readDatabaseBytes(path);
680
+ return {
681
+ db: bytes ? new SQL.Database(bytes) : new SQL.Database(),
682
+ dbPath: path,
683
+ inTransaction: false,
684
+ dirty: false
685
+ };
686
+ }
687
+ async function closeDatabase(db) {
688
+ await persistDatabase(db);
689
+ db.db.close();
690
+ }
691
+ async function run(db, sql, ...params) {
692
+ db.db.run(sql, params);
693
+ const statementKind = classifyStatement(sql);
694
+ if (statementKind === 'begin') {
695
+ db.inTransaction = true;
696
+ return;
697
+ }
698
+ if (statementKind === 'commit') {
699
+ db.inTransaction = false;
700
+ db.dirty = true;
701
+ await persistDatabase(db);
702
+ return;
703
+ }
704
+ if (statementKind === 'rollback') {
705
+ db.inTransaction = false;
706
+ db.dirty = false;
707
+ return;
708
+ }
709
+ if (statementKind === 'write') {
710
+ db.dirty = true;
711
+ if (!db.inTransaction) {
712
+ await persistDatabase(db);
713
+ }
714
+ }
715
+ }
716
+ async function get(db, sql, ...params) {
717
+ const rows = await all(db, sql, ...params);
718
+ return rows[0];
719
+ }
720
+ async function all(db, sql, ...params) {
721
+ const statement = db.db.prepare(sql, params);
722
+ const rows = [];
723
+ try {
724
+ while (statement.step()) {
725
+ rows.push(statement.getAsObject());
726
+ }
727
+ return rows;
728
+ }
729
+ finally {
730
+ statement.free();
731
+ }
732
+ }
733
+ async function exec(db, sql) {
734
+ db.db.exec(sql);
735
+ db.dirty = true;
736
+ if (!db.inTransaction) {
737
+ await persistDatabase(db);
738
+ }
739
+ }
740
+ let sqlJsModulePromise;
741
+ async function getSqlJsModule() {
742
+ sqlJsModulePromise ?? (sqlJsModulePromise = initSqlJs({}));
743
+ return sqlJsModulePromise;
744
+ }
745
+ async function readDatabaseBytes(path) {
746
+ try {
747
+ const file = await readFile(path);
748
+ return new Uint8Array(file);
749
+ }
750
+ catch (error) {
751
+ if (isMissingFileError(error)) {
752
+ return undefined;
753
+ }
754
+ throw error;
755
+ }
756
+ }
757
+ async function persistDatabase(db) {
758
+ if (!db.dirty) {
759
+ return;
760
+ }
761
+ await writeFile(db.dbPath, db.db.export());
762
+ db.dirty = false;
763
+ }
764
+ function classifyStatement(sql) {
765
+ const keyword = sql.trimStart().match(/^[A-Za-z]+/)?.[0]?.toUpperCase();
766
+ if (keyword === 'BEGIN') {
767
+ return 'begin';
768
+ }
769
+ if (keyword === 'COMMIT') {
770
+ return 'commit';
771
+ }
772
+ if (keyword === 'ROLLBACK') {
773
+ return 'rollback';
774
+ }
775
+ if (keyword && ['INSERT', 'UPDATE', 'DELETE', 'REPLACE', 'CREATE', 'DROP', 'ALTER', 'PRAGMA'].includes(keyword)) {
776
+ return 'write';
777
+ }
778
+ return 'read';
779
+ }
780
+ function isMissingFileError(error) {
781
+ return typeof error === 'object'
782
+ && error !== null
783
+ && 'code' in error
784
+ && error.code === 'ENOENT';
785
+ }
786
+ function toIdeaTableRow(row, attributeColumns, outboundReferences, inboundReferences) {
787
+ const attributes = parseAttributes(row.attributes_json);
788
+ const otherAttributeItems = formatOtherAttributeItems(attributes);
789
+ const fanout = row.inbound_count + row.outbound_count;
790
+ const stabilityCue = Math.min(1, Math.max(0, 1 - fanout / 24));
791
+ const stabilityLabel = stabilityCue >= 0.75 ? 'Stable' : stabilityCue >= 0.45 ? 'Active' : 'High churn risk';
792
+ return {
793
+ id: row.id,
794
+ title: row.name,
795
+ path: row.file_uri,
796
+ mainAttribute: row.summary || undefined,
797
+ otherAttributes: otherAttributeItems.join('; '),
798
+ otherAttributeItems,
799
+ attributeValues: attributeValuesForKeys(row.attributes_json, attributeColumns),
800
+ referenceCount: row.reference_count,
801
+ outboundCount: row.outbound_count,
802
+ inboundCount: row.inbound_count,
803
+ outboundReferences,
804
+ inboundReferences,
805
+ fileUri: row.file_uri,
806
+ lineStart: row.line_start,
807
+ stabilityCue,
808
+ stabilityLabel
809
+ };
810
+ }
811
+ function toIdeasetTableRow(row) {
812
+ const name = row.kind === 'explicit' && row.name
813
+ ? row.name
814
+ : implicitIdeasetName(row.file_uri);
815
+ return {
816
+ id: row.id,
817
+ name,
818
+ path: row.file_uri,
819
+ kind: row.kind,
820
+ memberCount: row.member_count,
821
+ members: [],
822
+ fileUri: row.file_uri,
823
+ lineStart: row.line_start
824
+ };
825
+ }
826
+ function implicitIdeasetName(fileUri) {
827
+ const fileName = basename(fileUri);
828
+ return fileName.endsWith('.rq') ? fileName.slice(0, -3) : fileName;
829
+ }
830
+ function formatOtherAttributeItems(attributes) {
831
+ const parts = [];
832
+ for (const [key, value] of Object.entries(attributes)) {
833
+ let rendered;
834
+ if (typeof value === 'string' && value) {
835
+ rendered = `${key}: ${value}`;
836
+ }
837
+ else if (Array.isArray(value) && value.length > 0) {
838
+ rendered = `${key}: ${value.join(', ')}`;
839
+ }
840
+ else if (value === true) {
841
+ rendered = key;
842
+ }
843
+ if (rendered) {
844
+ parts.push(rendered);
845
+ }
846
+ }
847
+ return parts;
848
+ }
849
+ function toOutgoingReferenceChip(row) {
850
+ const targetName = row.target_name ?? row.label ?? row.target_file ?? '—';
851
+ const filterKey = row.target_id
852
+ ? `outbound:idea:${row.target_id}`
853
+ : `outbound:file:${row.target_file ?? row.label ?? ''}`;
854
+ return {
855
+ label: targetName,
856
+ fileUri: row.target_uri ?? row.source_uri,
857
+ line: row.target_line ?? row.source_line,
858
+ direction: 'outbound',
859
+ filterKey
860
+ };
861
+ }
862
+ function toIncomingReferenceChip(row) {
863
+ return {
864
+ label: row.source_name,
865
+ fileUri: row.source_uri,
866
+ line: row.source_line,
867
+ direction: 'inbound',
868
+ filterKey: `inbound:idea:${row.source_id}`
869
+ };
870
+ }
871
+ function toReferenceTableRow(row) {
872
+ const targetPath = row.target_uri ?? row.target_file ?? '';
873
+ const targetName = row.target_name ?? row.label ?? row.target_file ?? '—';
874
+ return {
875
+ sourcePath: row.source_uri,
876
+ sourceName: row.source_name,
877
+ targetPath,
878
+ targetName,
879
+ isInRq: row.target_id !== null,
880
+ referenceType: toReferenceViewType(row.kind),
881
+ sourceFileUri: row.source_uri,
882
+ sourceLineStart: row.source_line
883
+ };
884
+ }
885
+ function toReferenceViewType(kind) {
886
+ switch (kind) {
887
+ case 'file_reference':
888
+ return 'file';
889
+ case 'comment_link':
890
+ return 'comment';
891
+ default:
892
+ return 'sub-idea';
893
+ }
894
+ }
895
+ function toOutboundReferenceListRow(row) {
896
+ const targetName = row.target_name ?? row.label ?? row.target_file ?? 'unknown';
897
+ const rawTargetPath = row.target_uri ?? row.target_file ?? '';
898
+ const targetPath = !row.target_uri && row.target_file
899
+ ? resolveReferencedFilePath(row.target_file, row.source_id)
900
+ : rawTargetPath;
901
+ return {
902
+ edgeId: row.edge_id,
903
+ direction: 'outbound',
904
+ kind: row.kind,
905
+ label: row.label ?? targetName,
906
+ targetName,
907
+ targetPath,
908
+ targetLine: row.target_line ?? undefined,
909
+ sourceLine: row.source_line ?? undefined,
910
+ snippet: row.snippet ?? undefined,
911
+ isResolved: row.is_resolved !== 0,
912
+ sourceIdeaId: row.source_id,
913
+ targetIdeaId: row.target_id ?? undefined
914
+ };
915
+ }
916
+ function toInboundReferenceListRow(row) {
917
+ const targetName = row.source_name ?? 'unknown';
918
+ const targetPath = row.source_uri ?? '';
919
+ return {
920
+ edgeId: row.edge_id,
921
+ direction: 'inbound',
922
+ kind: row.kind,
923
+ label: row.label ?? targetName,
924
+ targetName,
925
+ targetPath,
926
+ targetLine: row.source_line_idea ?? undefined,
927
+ sourceLine: row.source_line ?? undefined,
928
+ snippet: row.snippet ?? undefined,
929
+ isResolved: row.is_resolved !== 0,
930
+ sourceIdeaId: row.source_id,
931
+ targetIdeaId: row.target_id ?? undefined
932
+ };
933
+ }
934
+ function mapEdgeRow(row) {
935
+ return {
936
+ id: row.id,
937
+ sourceId: row.source_id,
938
+ targetId: row.target_id ?? undefined,
939
+ targetFile: row.target_file ?? undefined,
940
+ kind: row.kind,
941
+ label: row.label ?? undefined,
942
+ sourceLine: row.source_line ?? undefined,
943
+ snippet: row.snippet ?? undefined,
944
+ isResolved: row.is_resolved === undefined || row.is_resolved === null
945
+ ? true
946
+ : row.is_resolved !== 0
947
+ };
948
+ }
949
+ function mapIdeaRow(row) {
950
+ return {
951
+ id: row.id,
952
+ name: row.name,
953
+ kind: row.kind,
954
+ fileUri: row.file_uri,
955
+ lineStart: row.line_start,
956
+ lineEnd: row.line_end,
957
+ summary: row.summary,
958
+ attributesJson: row.attributes_json,
959
+ contentHash: row.content_hash,
960
+ gitCreatedAt: row.git_created_at ?? undefined,
961
+ gitModifiedAt: row.git_modified_at ?? undefined
962
+ };
963
+ }
964
+ //# sourceMappingURL=sqlite-store.js.map