@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.
- package/out/analysis/analyser-registry.d.ts +21 -0
- package/out/analysis/analyser-registry.js +33 -0
- package/out/analysis/analyser-registry.js.map +1 -0
- package/out/analysis/completion-tracking-analyser.d.ts +3 -0
- package/out/analysis/completion-tracking-analyser.js +37 -0
- package/out/analysis/completion-tracking-analyser.js.map +1 -0
- package/out/analysis/deprecation-impact-analyser.d.ts +3 -0
- package/out/analysis/deprecation-impact-analyser.js +21 -0
- package/out/analysis/deprecation-impact-analyser.js.map +1 -0
- package/out/analysis/file-related-analyser.d.ts +5 -0
- package/out/analysis/file-related-analyser.js +41 -0
- package/out/analysis/file-related-analyser.js.map +1 -0
- package/out/analysis/git-dates-analyser.d.ts +9 -0
- package/out/analysis/git-dates-analyser.js +39 -0
- package/out/analysis/git-dates-analyser.js.map +1 -0
- package/out/analysis/list-ideas-analyser.d.ts +3 -0
- package/out/analysis/list-ideas-analyser.js +7 -0
- package/out/analysis/list-ideas-analyser.js.map +1 -0
- package/out/analysis/local-graph-analyser.d.ts +6 -0
- package/out/analysis/local-graph-analyser.js +42 -0
- package/out/analysis/local-graph-analyser.js.map +1 -0
- package/out/analysis/semantic-search-analyser.d.ts +8 -0
- package/out/analysis/semantic-search-analyser.js +51 -0
- package/out/analysis/semantic-search-analyser.js.map +1 -0
- package/out/analysis-api.d.ts +31 -0
- package/out/analysis-api.js +136 -0
- package/out/analysis-api.js.map +1 -0
- package/out/core/analytical-store.d.ts +78 -0
- package/out/core/analytical-store.js +141 -0
- package/out/core/analytical-store.js.map +1 -0
- package/out/core/context-model.d.ts +187 -0
- package/out/core/context-model.js +36 -0
- package/out/core/context-model.js.map +1 -0
- package/out/core/context-signals.d.ts +149 -0
- package/out/core/context-signals.js +397 -0
- package/out/core/context-signals.js.map +1 -0
- package/out/core/file-path-match.d.ts +7 -0
- package/out/core/file-path-match.js +39 -0
- package/out/core/file-path-match.js.map +1 -0
- package/out/core/file-reference-resolve.d.ts +8 -0
- package/out/core/file-reference-resolve.js +28 -0
- package/out/core/file-reference-resolve.js.map +1 -0
- package/out/core/idea-references.d.ts +13 -0
- package/out/core/idea-references.js +25 -0
- package/out/core/idea-references.js.map +1 -0
- package/out/core/index-error.d.ts +23 -0
- package/out/core/index-error.js +33 -0
- package/out/core/index-error.js.map +1 -0
- package/out/core/types.d.ts +172 -0
- package/out/core/types.js +48 -0
- package/out/core/types.js.map +1 -0
- package/out/core/workspace-paths.d.ts +4 -0
- package/out/core/workspace-paths.js +92 -0
- package/out/core/workspace-paths.js.map +1 -0
- package/out/create-runtime.d.ts +21 -0
- package/out/create-runtime.js +39 -0
- package/out/create-runtime.js.map +1 -0
- package/out/export/build-export-snapshot.d.ts +3 -0
- package/out/export/build-export-snapshot.js +573 -0
- package/out/export/build-export-snapshot.js.map +1 -0
- package/out/export/html-export-assets.d.ts +3 -0
- package/out/export/html-export-assets.js +1066 -0
- package/out/export/html-export-assets.js.map +1 -0
- package/out/export/html-export-template.d.ts +17 -0
- package/out/export/html-export-template.js +930 -0
- package/out/export/html-export-template.js.map +1 -0
- package/out/export/html-export-utils.d.ts +45 -0
- package/out/export/html-export-utils.js +262 -0
- package/out/export/html-export-utils.js.map +1 -0
- package/out/export/types.d.ts +187 -0
- package/out/export/types.js +2 -0
- package/out/export/types.js.map +1 -0
- package/out/export/write-html-export.d.ts +2 -0
- package/out/export/write-html-export.js +143 -0
- package/out/export/write-html-export.js.map +1 -0
- package/out/headless-index-service.d.ts +23 -0
- package/out/headless-index-service.js +171 -0
- package/out/headless-index-service.js.map +1 -0
- package/out/index-store/idea-extractor.d.ts +6 -0
- package/out/index-store/idea-extractor.js +271 -0
- package/out/index-store/idea-extractor.js.map +1 -0
- package/out/index-store/schema.d.ts +10 -0
- package/out/index-store/schema.js +54 -0
- package/out/index-store/schema.js.map +1 -0
- package/out/index-store/sqlite-store.d.ts +66 -0
- package/out/index-store/sqlite-store.js +964 -0
- package/out/index-store/sqlite-store.js.map +1 -0
- package/out/index-store/webview-graph-queries.d.ts +62 -0
- package/out/index-store/webview-graph-queries.js +256 -0
- package/out/index-store/webview-graph-queries.js.map +1 -0
- package/out/index-store/webview-table-queries.d.ts +57 -0
- package/out/index-store/webview-table-queries.js +199 -0
- package/out/index-store/webview-table-queries.js.map +1 -0
- package/out/index.d.ts +40 -0
- package/out/index.js +27 -0
- package/out/index.js.map +1 -0
- 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
|