@projitive/mcp 1.2.0 → 2.0.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.
@@ -0,0 +1,623 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import duckdb from "@duckdb/node-api";
4
+ import initSqlJs from "sql.js";
5
+ const STORE_SCHEMA_VERSION = 3;
6
+ const SQL_HEADER = Buffer.from("SQLite format 3\0", "utf8");
7
+ const sqlRuntimePromise = initSqlJs();
8
+ const storeCache = new Map();
9
+ const storeLocks = new Map();
10
+ let duckdbConnectionPromise;
11
+ function defaultViewState(name) {
12
+ return {
13
+ name,
14
+ dirty: true,
15
+ lastSourceVersion: 0,
16
+ lastBuiltAt: "",
17
+ recordVersion: 1,
18
+ };
19
+ }
20
+ function defaultStore() {
21
+ return {
22
+ schema: "projitive-json-store",
23
+ tasks: [],
24
+ roadmaps: [],
25
+ meta: {
26
+ tasks_version: 0,
27
+ roadmaps_version: 0,
28
+ store_schema_version: STORE_SCHEMA_VERSION,
29
+ },
30
+ view_state: {
31
+ tasks_markdown: defaultViewState("tasks_markdown"),
32
+ roadmaps_markdown: defaultViewState("roadmaps_markdown"),
33
+ },
34
+ migration_history: [],
35
+ };
36
+ }
37
+ function nowIso() {
38
+ return new Date().toISOString();
39
+ }
40
+ function parseJsonOr(raw, fallback) {
41
+ if (typeof raw !== "string" || raw.trim().length === 0) {
42
+ return fallback;
43
+ }
44
+ try {
45
+ return JSON.parse(raw);
46
+ }
47
+ catch {
48
+ return fallback;
49
+ }
50
+ }
51
+ function normalizeTaskStatus(status) {
52
+ if (status === "IN_PROGRESS" || status === "BLOCKED" || status === "DONE") {
53
+ return status;
54
+ }
55
+ return "TODO";
56
+ }
57
+ function normalizeRoadmapStatus(status) {
58
+ return status === "done" ? "done" : "active";
59
+ }
60
+ function isSqliteBuffer(data) {
61
+ if (data.length < SQL_HEADER.length) {
62
+ return false;
63
+ }
64
+ return data.subarray(0, SQL_HEADER.length).equals(SQL_HEADER);
65
+ }
66
+ function normalizeStore(input) {
67
+ const base = defaultStore();
68
+ const meta = input.meta ?? {};
69
+ const tasks = Array.isArray(input.tasks)
70
+ ? input.tasks.map((task) => ({
71
+ id: String(task.id ?? ""),
72
+ title: String(task.title ?? ""),
73
+ status: normalizeTaskStatus(String(task.status ?? "TODO")),
74
+ owner: String(task.owner ?? ""),
75
+ summary: String(task.summary ?? ""),
76
+ updatedAt: String(task.updatedAt ?? nowIso()),
77
+ links: Array.isArray(task.links) ? task.links.map((item) => String(item)) : [],
78
+ roadmapRefs: Array.isArray(task.roadmapRefs) ? task.roadmapRefs.map((item) => String(item)) : [],
79
+ subState: task.subState,
80
+ blocker: task.blocker,
81
+ recordVersion: Number.isFinite(Number(task.recordVersion)) ? Number(task.recordVersion) : 1,
82
+ }))
83
+ : [];
84
+ const roadmaps = Array.isArray(input.roadmaps)
85
+ ? input.roadmaps.map((milestone) => ({
86
+ id: String(milestone.id ?? ""),
87
+ title: String(milestone.title ?? ""),
88
+ status: normalizeRoadmapStatus(String(milestone.status ?? "active")),
89
+ time: typeof milestone.time === "string" && milestone.time.length > 0 ? milestone.time : undefined,
90
+ updatedAt: String(milestone.updatedAt ?? nowIso()),
91
+ recordVersion: Number.isFinite(Number(milestone.recordVersion)) ? Number(milestone.recordVersion) : 1,
92
+ }))
93
+ : [];
94
+ const tasksView = input.view_state?.tasks_markdown;
95
+ const roadmapsView = input.view_state?.roadmaps_markdown;
96
+ return {
97
+ schema: "projitive-json-store",
98
+ tasks,
99
+ roadmaps,
100
+ meta: {
101
+ tasks_version: Number.isFinite(Number(meta.tasks_version)) ? Number(meta.tasks_version) : base.meta.tasks_version,
102
+ roadmaps_version: Number.isFinite(Number(meta.roadmaps_version)) ? Number(meta.roadmaps_version) : base.meta.roadmaps_version,
103
+ store_schema_version: STORE_SCHEMA_VERSION,
104
+ },
105
+ view_state: {
106
+ tasks_markdown: {
107
+ ...base.view_state.tasks_markdown,
108
+ dirty: typeof tasksView?.dirty === "boolean" ? tasksView.dirty : base.view_state.tasks_markdown.dirty,
109
+ lastSourceVersion: Number.isFinite(Number(tasksView?.lastSourceVersion))
110
+ ? Number(tasksView?.lastSourceVersion)
111
+ : base.view_state.tasks_markdown.lastSourceVersion,
112
+ lastBuiltAt: typeof tasksView?.lastBuiltAt === "string" ? tasksView.lastBuiltAt : base.view_state.tasks_markdown.lastBuiltAt,
113
+ recordVersion: Number.isFinite(Number(tasksView?.recordVersion))
114
+ ? Number(tasksView?.recordVersion)
115
+ : base.view_state.tasks_markdown.recordVersion,
116
+ },
117
+ roadmaps_markdown: {
118
+ ...base.view_state.roadmaps_markdown,
119
+ dirty: typeof roadmapsView?.dirty === "boolean" ? roadmapsView.dirty : base.view_state.roadmaps_markdown.dirty,
120
+ lastSourceVersion: Number.isFinite(Number(roadmapsView?.lastSourceVersion))
121
+ ? Number(roadmapsView?.lastSourceVersion)
122
+ : base.view_state.roadmaps_markdown.lastSourceVersion,
123
+ lastBuiltAt: typeof roadmapsView?.lastBuiltAt === "string" ? roadmapsView.lastBuiltAt : base.view_state.roadmaps_markdown.lastBuiltAt,
124
+ recordVersion: Number.isFinite(Number(roadmapsView?.recordVersion))
125
+ ? Number(roadmapsView?.recordVersion)
126
+ : base.view_state.roadmaps_markdown.recordVersion,
127
+ },
128
+ },
129
+ migration_history: Array.isArray(input.migration_history) ? input.migration_history : [],
130
+ };
131
+ }
132
+ async function migrateSqliteToJson(data) {
133
+ const SQL = await sqlRuntimePromise;
134
+ const db = new SQL.Database(new Uint8Array(data));
135
+ try {
136
+ const tasksResult = db.exec(`
137
+ SELECT id, title, status, owner, summary, updated_at, links_json, roadmap_refs_json, sub_state_json, blocker_json, COALESCE(record_version, 1)
138
+ FROM tasks
139
+ `);
140
+ const roadmapsResult = db.exec(`
141
+ SELECT id, title, status, time, updated_at, COALESCE(record_version, 1)
142
+ FROM roadmaps
143
+ `);
144
+ const metaResult = db.exec(`
145
+ SELECT key, value
146
+ FROM meta
147
+ WHERE key IN ('tasks_version', 'roadmaps_version', 'store_schema_version')
148
+ `);
149
+ const viewStateResult = db.exec(`
150
+ SELECT name, dirty, last_source_version, last_built_at, COALESCE(record_version, 1)
151
+ FROM view_state
152
+ WHERE name IN ('tasks_markdown', 'roadmaps_markdown')
153
+ `);
154
+ const tasks = tasksResult[0]?.values?.map((row) => ({
155
+ id: String(row[0]),
156
+ title: String(row[1]),
157
+ status: normalizeTaskStatus(String(row[2])),
158
+ owner: String(row[3]),
159
+ summary: String(row[4]),
160
+ updatedAt: String(row[5]),
161
+ links: parseJsonOr(row[6], []),
162
+ roadmapRefs: parseJsonOr(row[7], []),
163
+ subState: parseJsonOr(row[8], undefined),
164
+ blocker: parseJsonOr(row[9], undefined),
165
+ recordVersion: Number(row[10]) || 1,
166
+ })) ?? [];
167
+ const roadmaps = roadmapsResult[0]?.values?.map((row) => ({
168
+ id: String(row[0]),
169
+ title: String(row[1]),
170
+ status: normalizeRoadmapStatus(String(row[2])),
171
+ time: row[3] == null ? undefined : String(row[3]),
172
+ updatedAt: String(row[4]),
173
+ recordVersion: Number(row[5]) || 1,
174
+ })) ?? [];
175
+ const meta = defaultStore().meta;
176
+ const metaRows = metaResult[0]?.values ?? [];
177
+ for (const row of metaRows) {
178
+ const key = String(row[0]);
179
+ const value = Number.parseInt(String(row[1]), 10);
180
+ if (!Number.isFinite(value)) {
181
+ continue;
182
+ }
183
+ if (key === "tasks_version")
184
+ meta.tasks_version = value;
185
+ if (key === "roadmaps_version")
186
+ meta.roadmaps_version = value;
187
+ if (key === "store_schema_version")
188
+ meta.store_schema_version = value;
189
+ }
190
+ const tasksView = defaultViewState("tasks_markdown");
191
+ const roadmapsView = defaultViewState("roadmaps_markdown");
192
+ const viewRows = viewStateResult[0]?.values ?? [];
193
+ for (const row of viewRows) {
194
+ const name = String(row[0]);
195
+ const dirty = Number(row[1]) === 1;
196
+ const lastSourceVersion = Number(row[2]) || 0;
197
+ const lastBuiltAt = String(row[3] ?? "");
198
+ const recordVersion = Number(row[4]) || 1;
199
+ if (name === "tasks_markdown") {
200
+ tasksView.dirty = dirty;
201
+ tasksView.lastSourceVersion = lastSourceVersion;
202
+ tasksView.lastBuiltAt = lastBuiltAt;
203
+ tasksView.recordVersion = recordVersion;
204
+ }
205
+ if (name === "roadmaps_markdown") {
206
+ roadmapsView.dirty = dirty;
207
+ roadmapsView.lastSourceVersion = lastSourceVersion;
208
+ roadmapsView.lastBuiltAt = lastBuiltAt;
209
+ roadmapsView.recordVersion = recordVersion;
210
+ }
211
+ }
212
+ return normalizeStore({
213
+ schema: "projitive-json-store",
214
+ tasks,
215
+ roadmaps,
216
+ meta: {
217
+ tasks_version: meta.tasks_version,
218
+ roadmaps_version: meta.roadmaps_version,
219
+ store_schema_version: STORE_SCHEMA_VERSION,
220
+ },
221
+ view_state: {
222
+ tasks_markdown: tasksView,
223
+ roadmaps_markdown: roadmapsView,
224
+ },
225
+ migration_history: [],
226
+ });
227
+ }
228
+ finally {
229
+ db.close();
230
+ }
231
+ }
232
+ async function persistStore(dbPath, store) {
233
+ await fs.mkdir(path.dirname(dbPath), { recursive: true });
234
+ const tempPath = `${dbPath}.tmp-${process.pid}-${Date.now()}`;
235
+ const body = `${JSON.stringify(store, null, 2)}\n`;
236
+ await fs.writeFile(tempPath, body, "utf8");
237
+ await fs.rename(tempPath, dbPath);
238
+ }
239
+ async function loadStoreFromDisk(dbPath) {
240
+ const file = await fs.readFile(dbPath).catch(() => undefined);
241
+ if (!file || file.length === 0) {
242
+ return { store: defaultStore(), shouldPersist: true };
243
+ }
244
+ if (isSqliteBuffer(file)) {
245
+ const migrated = await migrateSqliteToJson(file);
246
+ return { store: migrated, shouldPersist: true };
247
+ }
248
+ const text = file.toString("utf8").trim();
249
+ if (text.length === 0) {
250
+ return { store: defaultStore(), shouldPersist: true };
251
+ }
252
+ try {
253
+ const parsed = JSON.parse(text);
254
+ const normalized = normalizeStore(parsed);
255
+ return { store: normalized, shouldPersist: normalized.meta.store_schema_version !== STORE_SCHEMA_VERSION };
256
+ }
257
+ catch {
258
+ // Legacy plain-text marker or corrupted content: bootstrap empty JSON store.
259
+ return { store: defaultStore(), shouldPersist: true };
260
+ }
261
+ }
262
+ async function openStore(dbPath) {
263
+ const cached = storeCache.get(dbPath);
264
+ if (cached) {
265
+ return cached;
266
+ }
267
+ const { store, shouldPersist } = await loadStoreFromDisk(dbPath);
268
+ storeCache.set(dbPath, store);
269
+ if (shouldPersist) {
270
+ await persistStore(dbPath, store);
271
+ }
272
+ return store;
273
+ }
274
+ async function withStoreLock(dbPath, action) {
275
+ const previous = storeLocks.get(dbPath) ?? Promise.resolve();
276
+ let resolveCurrent;
277
+ const current = new Promise((resolve) => {
278
+ resolveCurrent = resolve;
279
+ });
280
+ storeLocks.set(dbPath, previous.then(() => current));
281
+ await previous;
282
+ try {
283
+ return await action();
284
+ }
285
+ finally {
286
+ resolveCurrent?.();
287
+ if (storeLocks.get(dbPath) === current) {
288
+ storeLocks.delete(dbPath);
289
+ }
290
+ }
291
+ }
292
+ function bumpVersionAndDirtyView(store, kind) {
293
+ if (kind === "tasks") {
294
+ store.meta.tasks_version += 1;
295
+ const view = store.view_state.tasks_markdown;
296
+ view.dirty = true;
297
+ view.recordVersion += 1;
298
+ return store.meta.tasks_version;
299
+ }
300
+ store.meta.roadmaps_version += 1;
301
+ const view = store.view_state.roadmaps_markdown;
302
+ view.dirty = true;
303
+ view.recordVersion += 1;
304
+ return store.meta.roadmaps_version;
305
+ }
306
+ function toPublicTask(stored) {
307
+ return {
308
+ id: stored.id,
309
+ title: stored.title,
310
+ status: stored.status,
311
+ owner: stored.owner,
312
+ summary: stored.summary,
313
+ updatedAt: stored.updatedAt,
314
+ links: [...stored.links],
315
+ roadmapRefs: [...stored.roadmapRefs],
316
+ subState: stored.subState,
317
+ blocker: stored.blocker,
318
+ };
319
+ }
320
+ function toPublicRoadmap(stored) {
321
+ return {
322
+ id: stored.id,
323
+ title: stored.title,
324
+ status: stored.status,
325
+ time: stored.time,
326
+ updatedAt: stored.updatedAt,
327
+ };
328
+ }
329
+ function safeTime(value) {
330
+ const t = new Date(value).getTime();
331
+ return Number.isFinite(t) ? t : 0;
332
+ }
333
+ function normalizeStatusForSort(status) {
334
+ if (status === "IN_PROGRESS")
335
+ return 2;
336
+ if (status === "TODO")
337
+ return 1;
338
+ return 0;
339
+ }
340
+ async function getDuckdbConnection() {
341
+ if (!duckdbConnectionPromise) {
342
+ duckdbConnectionPromise = (async () => {
343
+ const instance = await duckdb.DuckDBInstance.create(":memory:");
344
+ return instance.connect();
345
+ })();
346
+ }
347
+ return duckdbConnectionPromise;
348
+ }
349
+ function normalizeStoredTaskLike(raw) {
350
+ if (!raw || typeof raw !== "object") {
351
+ return null;
352
+ }
353
+ const value = raw;
354
+ const id = value.id;
355
+ const title = value.title;
356
+ if (typeof id !== "string" || typeof title !== "string") {
357
+ return null;
358
+ }
359
+ const statusRaw = typeof value.status === "string" ? value.status : "TODO";
360
+ const owner = typeof value.owner === "string" ? value.owner : "";
361
+ const summary = typeof value.summary === "string" ? value.summary : "";
362
+ const updatedAt = typeof value.updatedAt === "string"
363
+ ? value.updatedAt
364
+ : (typeof value.updated_at === "string" ? value.updated_at : nowIso());
365
+ const links = Array.isArray(value.links) ? value.links.map((item) => String(item)) : [];
366
+ const roadmapRefs = Array.isArray(value.roadmapRefs)
367
+ ? value.roadmapRefs.map((item) => String(item))
368
+ : (Array.isArray(value.roadmap_refs) ? value.roadmap_refs.map((item) => String(item)) : []);
369
+ const recordVersionRaw = value.recordVersion ?? value.record_version;
370
+ const recordVersion = Number.isFinite(Number(recordVersionRaw)) ? Number(recordVersionRaw) : 1;
371
+ return {
372
+ id,
373
+ title,
374
+ status: normalizeTaskStatus(statusRaw),
375
+ owner,
376
+ summary,
377
+ updatedAt,
378
+ links,
379
+ roadmapRefs,
380
+ subState: value.subState,
381
+ blocker: value.blocker,
382
+ recordVersion,
383
+ };
384
+ }
385
+ function normalizeStoredRoadmapLike(raw) {
386
+ if (!raw || typeof raw !== "object") {
387
+ return null;
388
+ }
389
+ const value = raw;
390
+ const id = value.id;
391
+ const title = value.title;
392
+ if (typeof id !== "string" || typeof title !== "string") {
393
+ return null;
394
+ }
395
+ const statusRaw = typeof value.status === "string" ? value.status : "active";
396
+ const time = typeof value.time === "string" ? value.time : undefined;
397
+ const updatedAt = typeof value.updatedAt === "string"
398
+ ? value.updatedAt
399
+ : (typeof value.updated_at === "string" ? value.updated_at : nowIso());
400
+ const recordVersionRaw = value.recordVersion ?? value.record_version;
401
+ const recordVersion = Number.isFinite(Number(recordVersionRaw)) ? Number(recordVersionRaw) : 1;
402
+ return {
403
+ id,
404
+ title,
405
+ status: normalizeRoadmapStatus(statusRaw),
406
+ time,
407
+ updatedAt,
408
+ recordVersion,
409
+ };
410
+ }
411
+ async function runDuckdbQuery(sql) {
412
+ try {
413
+ const connection = await getDuckdbConnection();
414
+ const result = await connection.run(sql);
415
+ const rows = await result.getRowObjectsJS();
416
+ if (!rows || rows.length === 0) {
417
+ return undefined;
418
+ }
419
+ return rows;
420
+ }
421
+ catch {
422
+ return undefined;
423
+ }
424
+ }
425
+ async function loadTasksFromDuckdb(dbPath) {
426
+ const sql = `SELECT tasks FROM read_json_auto('${dbPath.replace(/'/g, "''")}') LIMIT 1;`;
427
+ const rows = await runDuckdbQuery(sql);
428
+ if (!rows || rows.length === 0) {
429
+ return undefined;
430
+ }
431
+ const rawTasks = rows[0]?.tasks;
432
+ if (!Array.isArray(rawTasks)) {
433
+ return undefined;
434
+ }
435
+ return rawTasks
436
+ .map((item) => normalizeStoredTaskLike(item))
437
+ .filter((item) => item != null);
438
+ }
439
+ async function loadRoadmapsFromDuckdb(dbPath) {
440
+ const sql = `SELECT roadmaps FROM read_json_auto('${dbPath.replace(/'/g, "''")}') LIMIT 1;`;
441
+ const rows = await runDuckdbQuery(sql);
442
+ if (!rows || rows.length === 0) {
443
+ return undefined;
444
+ }
445
+ const rawRoadmaps = rows[0]?.roadmaps;
446
+ if (!Array.isArray(rawRoadmaps)) {
447
+ return undefined;
448
+ }
449
+ return rawRoadmaps
450
+ .map((item) => normalizeStoredRoadmapLike(item))
451
+ .filter((item) => item != null);
452
+ }
453
+ export async function ensureStore(dbPath) {
454
+ await openStore(dbPath);
455
+ }
456
+ export async function getStoreVersion(dbPath, kind) {
457
+ const store = await openStore(dbPath);
458
+ return kind === "tasks" ? store.meta.tasks_version : store.meta.roadmaps_version;
459
+ }
460
+ export async function getMarkdownViewState(dbPath, viewName) {
461
+ const store = await openStore(dbPath);
462
+ const view = store.view_state[viewName];
463
+ return {
464
+ dirty: view.dirty,
465
+ lastSourceVersion: view.lastSourceVersion,
466
+ lastBuiltAt: view.lastBuiltAt,
467
+ };
468
+ }
469
+ export async function markMarkdownViewBuilt(dbPath, viewName, sourceVersion, builtAt = nowIso()) {
470
+ await withStoreLock(dbPath, async () => {
471
+ const store = await openStore(dbPath);
472
+ const view = store.view_state[viewName];
473
+ view.dirty = false;
474
+ view.lastSourceVersion = sourceVersion;
475
+ view.lastBuiltAt = builtAt;
476
+ view.recordVersion += 1;
477
+ await persistStore(dbPath, store);
478
+ });
479
+ }
480
+ export async function markMarkdownViewDirty(dbPath, viewName) {
481
+ await withStoreLock(dbPath, async () => {
482
+ const store = await openStore(dbPath);
483
+ const view = store.view_state[viewName];
484
+ view.dirty = true;
485
+ view.recordVersion += 1;
486
+ await persistStore(dbPath, store);
487
+ });
488
+ }
489
+ export async function loadTasksFromStore(dbPath) {
490
+ const tasksFromDuckdb = await loadTasksFromDuckdb(dbPath);
491
+ if (!tasksFromDuckdb) {
492
+ throw new Error("DuckDB task query failed");
493
+ }
494
+ return tasksFromDuckdb.map(toPublicTask);
495
+ }
496
+ export async function loadTaskStatusStatsFromStore(dbPath) {
497
+ const tasks = await loadTasksFromStore(dbPath);
498
+ const todo = tasks.filter((task) => task.status === "TODO").length;
499
+ const inProgress = tasks.filter((task) => task.status === "IN_PROGRESS").length;
500
+ const blocked = tasks.filter((task) => task.status === "BLOCKED").length;
501
+ const done = tasks.filter((task) => task.status === "DONE").length;
502
+ const total = tasks.length;
503
+ const latestUpdatedAt = tasks
504
+ .map((task) => task.updatedAt)
505
+ .sort((a, b) => safeTime(b) - safeTime(a))[0] ?? "";
506
+ return { todo, inProgress, blocked, done, total, latestUpdatedAt };
507
+ }
508
+ export async function loadActionableTasksFromStore(dbPath, limit) {
509
+ const tasks = await loadTasksFromStore(dbPath);
510
+ const sorted = tasks
511
+ .filter((task) => task.status === "IN_PROGRESS" || task.status === "TODO")
512
+ .sort((a, b) => {
513
+ const ap = normalizeStatusForSort(a.status);
514
+ const bp = normalizeStatusForSort(b.status);
515
+ if (bp !== ap) {
516
+ return bp - ap;
517
+ }
518
+ const timeDelta = safeTime(b.updatedAt) - safeTime(a.updatedAt);
519
+ if (timeDelta !== 0) {
520
+ return timeDelta;
521
+ }
522
+ return b.id.localeCompare(a.id);
523
+ });
524
+ if (typeof limit === "number" && Number.isFinite(limit) && limit > 0) {
525
+ return sorted.slice(0, Math.floor(limit));
526
+ }
527
+ return sorted;
528
+ }
529
+ export async function upsertTaskInStore(dbPath, task) {
530
+ await withStoreLock(dbPath, async () => {
531
+ const store = await openStore(dbPath);
532
+ const index = store.tasks.findIndex((item) => item.id === task.id);
533
+ if (index >= 0) {
534
+ const previous = store.tasks[index];
535
+ store.tasks[index] = {
536
+ ...task,
537
+ status: normalizeTaskStatus(task.status),
538
+ links: [...(task.links ?? [])],
539
+ roadmapRefs: [...(task.roadmapRefs ?? [])],
540
+ recordVersion: previous.recordVersion + 1,
541
+ };
542
+ }
543
+ else {
544
+ store.tasks.push({
545
+ ...task,
546
+ status: normalizeTaskStatus(task.status),
547
+ links: [...(task.links ?? [])],
548
+ roadmapRefs: [...(task.roadmapRefs ?? [])],
549
+ recordVersion: 1,
550
+ });
551
+ }
552
+ bumpVersionAndDirtyView(store, "tasks");
553
+ await persistStore(dbPath, store);
554
+ });
555
+ }
556
+ export async function replaceTasksInStore(dbPath, tasks) {
557
+ await withStoreLock(dbPath, async () => {
558
+ const store = await openStore(dbPath);
559
+ store.tasks = tasks.map((task) => ({
560
+ ...task,
561
+ status: normalizeTaskStatus(task.status),
562
+ links: [...(task.links ?? [])],
563
+ roadmapRefs: [...(task.roadmapRefs ?? [])],
564
+ recordVersion: 1,
565
+ }));
566
+ bumpVersionAndDirtyView(store, "tasks");
567
+ await persistStore(dbPath, store);
568
+ });
569
+ }
570
+ export async function loadRoadmapsFromStore(dbPath) {
571
+ const roadmapsFromDuckdb = await loadRoadmapsFromDuckdb(dbPath);
572
+ if (!roadmapsFromDuckdb) {
573
+ throw new Error("DuckDB roadmap query failed");
574
+ }
575
+ return roadmapsFromDuckdb.map(toPublicRoadmap);
576
+ }
577
+ export async function loadRoadmapIdsFromStore(dbPath) {
578
+ const roadmaps = await loadRoadmapsFromStore(dbPath);
579
+ return roadmaps
580
+ .sort((a, b) => {
581
+ const timeDelta = safeTime(b.updatedAt) - safeTime(a.updatedAt);
582
+ if (timeDelta !== 0) {
583
+ return timeDelta;
584
+ }
585
+ return b.id.localeCompare(a.id);
586
+ })
587
+ .map((item) => item.id);
588
+ }
589
+ export async function replaceRoadmapsInStore(dbPath, milestones) {
590
+ await withStoreLock(dbPath, async () => {
591
+ const store = await openStore(dbPath);
592
+ store.roadmaps = milestones.map((milestone) => ({
593
+ ...milestone,
594
+ status: normalizeRoadmapStatus(milestone.status),
595
+ recordVersion: 1,
596
+ }));
597
+ bumpVersionAndDirtyView(store, "roadmaps");
598
+ await persistStore(dbPath, store);
599
+ });
600
+ }
601
+ export async function upsertRoadmapInStore(dbPath, milestone) {
602
+ await withStoreLock(dbPath, async () => {
603
+ const store = await openStore(dbPath);
604
+ const index = store.roadmaps.findIndex((item) => item.id === milestone.id);
605
+ if (index >= 0) {
606
+ const previous = store.roadmaps[index];
607
+ store.roadmaps[index] = {
608
+ ...milestone,
609
+ status: normalizeRoadmapStatus(milestone.status),
610
+ recordVersion: previous.recordVersion + 1,
611
+ };
612
+ }
613
+ else {
614
+ store.roadmaps.push({
615
+ ...milestone,
616
+ status: normalizeRoadmapStatus(milestone.status),
617
+ recordVersion: 1,
618
+ });
619
+ }
620
+ bumpVersionAndDirtyView(store, "roadmaps");
621
+ await persistStore(dbPath, store);
622
+ });
623
+ }