@kjerneverk/riotplan-format 1.0.0-dev.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/README.md +221 -0
- package/dist/index.d.ts +973 -0
- package/dist/index.js +1791 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/schema.sql +123 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1791 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { readFileSync, existsSync, statSync, unlinkSync, rmSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { dirname, join, extname } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
const DEFAULT_SQLITE_CONFIG = {
|
|
7
|
+
walMode: true,
|
|
8
|
+
pragmas: {
|
|
9
|
+
"journal_mode": "WAL",
|
|
10
|
+
"synchronous": "NORMAL",
|
|
11
|
+
"foreign_keys": true,
|
|
12
|
+
"temp_store": "memory"
|
|
13
|
+
},
|
|
14
|
+
extension: ".plan"
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_DIRECTORY_CONFIG = {
|
|
17
|
+
usePlanSubdir: true,
|
|
18
|
+
permissions: "0755"
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_FORMAT_CONFIG = {
|
|
21
|
+
defaultFormat: "directory",
|
|
22
|
+
sqlite: DEFAULT_SQLITE_CONFIG,
|
|
23
|
+
directory: DEFAULT_DIRECTORY_CONFIG
|
|
24
|
+
};
|
|
25
|
+
function mergeFormatConfig(userConfig) {
|
|
26
|
+
if (!userConfig) {
|
|
27
|
+
return DEFAULT_FORMAT_CONFIG;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
defaultFormat: userConfig.defaultFormat ?? DEFAULT_FORMAT_CONFIG.defaultFormat,
|
|
31
|
+
sqlite: {
|
|
32
|
+
...DEFAULT_SQLITE_CONFIG,
|
|
33
|
+
...userConfig.sqlite,
|
|
34
|
+
pragmas: {
|
|
35
|
+
...DEFAULT_SQLITE_CONFIG.pragmas,
|
|
36
|
+
...userConfig.sqlite?.pragmas
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
directory: {
|
|
40
|
+
...DEFAULT_DIRECTORY_CONFIG,
|
|
41
|
+
...userConfig.directory
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
46
|
+
const __dirname$1 = dirname(__filename$1);
|
|
47
|
+
class SqliteStorageProvider {
|
|
48
|
+
format = "sqlite";
|
|
49
|
+
path;
|
|
50
|
+
db;
|
|
51
|
+
planId = null;
|
|
52
|
+
constructor(planPath) {
|
|
53
|
+
this.path = planPath;
|
|
54
|
+
this.db = new Database(planPath);
|
|
55
|
+
this.db.pragma("journal_mode = WAL");
|
|
56
|
+
this.db.pragma("foreign_keys = ON");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Initialize the database with schema
|
|
60
|
+
*/
|
|
61
|
+
initializeSchema() {
|
|
62
|
+
const schemaPath = join(__dirname$1, "schema.sql");
|
|
63
|
+
const schema = readFileSync(schemaPath, "utf-8");
|
|
64
|
+
this.db.exec(schema);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the plan ID, loading it if necessary
|
|
68
|
+
*/
|
|
69
|
+
getPlanId() {
|
|
70
|
+
if (this.planId !== null) {
|
|
71
|
+
return this.planId;
|
|
72
|
+
}
|
|
73
|
+
const row = this.db.prepare("SELECT id FROM plans LIMIT 1").get();
|
|
74
|
+
if (!row) {
|
|
75
|
+
throw new Error("No plan found in database");
|
|
76
|
+
}
|
|
77
|
+
this.planId = row.id;
|
|
78
|
+
return this.planId;
|
|
79
|
+
}
|
|
80
|
+
// ==================== Core Operations ====================
|
|
81
|
+
async exists() {
|
|
82
|
+
try {
|
|
83
|
+
const row = this.db.prepare("SELECT COUNT(*) as count FROM plans").get();
|
|
84
|
+
return row.count > 0;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async initialize(metadata) {
|
|
90
|
+
try {
|
|
91
|
+
this.initializeSchema();
|
|
92
|
+
const stmt = this.db.prepare(`
|
|
93
|
+
INSERT INTO plans (code, name, description, stage, created_at, updated_at, schema_version)
|
|
94
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
95
|
+
`);
|
|
96
|
+
const result = stmt.run(
|
|
97
|
+
metadata.id,
|
|
98
|
+
metadata.name,
|
|
99
|
+
metadata.description || null,
|
|
100
|
+
metadata.stage,
|
|
101
|
+
metadata.createdAt,
|
|
102
|
+
metadata.updatedAt,
|
|
103
|
+
metadata.schemaVersion
|
|
104
|
+
);
|
|
105
|
+
this.planId = result.lastInsertRowid;
|
|
106
|
+
return { success: true };
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: error instanceof Error ? error.message : "Failed to initialize plan"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async close() {
|
|
115
|
+
this.db.close();
|
|
116
|
+
}
|
|
117
|
+
// ==================== Metadata Operations ====================
|
|
118
|
+
async getMetadata() {
|
|
119
|
+
try {
|
|
120
|
+
const row = this.db.prepare(`
|
|
121
|
+
SELECT code, name, description, stage, created_at, updated_at, schema_version
|
|
122
|
+
FROM plans WHERE id = ?
|
|
123
|
+
`).get(this.getPlanId());
|
|
124
|
+
if (!row) {
|
|
125
|
+
return { success: false, error: "Plan not found" };
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
data: {
|
|
130
|
+
id: row.code,
|
|
131
|
+
name: row.name,
|
|
132
|
+
description: row.description || void 0,
|
|
133
|
+
stage: row.stage,
|
|
134
|
+
createdAt: row.created_at,
|
|
135
|
+
updatedAt: row.updated_at,
|
|
136
|
+
schemaVersion: row.schema_version
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
error: error instanceof Error ? error.message : "Failed to get metadata"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async updateMetadata(metadata) {
|
|
147
|
+
try {
|
|
148
|
+
const updates = [];
|
|
149
|
+
const values = [];
|
|
150
|
+
if (metadata.name !== void 0) {
|
|
151
|
+
updates.push("name = ?");
|
|
152
|
+
values.push(metadata.name);
|
|
153
|
+
}
|
|
154
|
+
if (metadata.description !== void 0) {
|
|
155
|
+
updates.push("description = ?");
|
|
156
|
+
values.push(metadata.description);
|
|
157
|
+
}
|
|
158
|
+
if (metadata.stage !== void 0) {
|
|
159
|
+
updates.push("stage = ?");
|
|
160
|
+
values.push(metadata.stage);
|
|
161
|
+
}
|
|
162
|
+
updates.push("updated_at = ?");
|
|
163
|
+
values.push((/* @__PURE__ */ new Date()).toISOString());
|
|
164
|
+
values.push(this.getPlanId());
|
|
165
|
+
this.db.prepare(`UPDATE plans SET ${updates.join(", ")} WHERE id = ?`).run(...values);
|
|
166
|
+
return { success: true };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: error instanceof Error ? error.message : "Failed to update metadata"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ==================== Step Operations ====================
|
|
175
|
+
async getSteps() {
|
|
176
|
+
try {
|
|
177
|
+
const rows = this.db.prepare(`
|
|
178
|
+
SELECT number, code, title, description, status, started_at, completed_at, content
|
|
179
|
+
FROM plan_steps WHERE plan_id = ? ORDER BY number
|
|
180
|
+
`).all(this.getPlanId());
|
|
181
|
+
const steps = rows.map((row) => ({
|
|
182
|
+
number: row.number,
|
|
183
|
+
code: row.code,
|
|
184
|
+
title: row.title,
|
|
185
|
+
description: row.description || void 0,
|
|
186
|
+
status: row.status,
|
|
187
|
+
startedAt: row.started_at || void 0,
|
|
188
|
+
completedAt: row.completed_at || void 0,
|
|
189
|
+
content: row.content
|
|
190
|
+
}));
|
|
191
|
+
return { success: true, data: steps };
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
error: error instanceof Error ? error.message : "Failed to get steps"
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async getStep(number) {
|
|
200
|
+
try {
|
|
201
|
+
const row = this.db.prepare(`
|
|
202
|
+
SELECT number, code, title, description, status, started_at, completed_at, content
|
|
203
|
+
FROM plan_steps WHERE plan_id = ? AND number = ?
|
|
204
|
+
`).get(this.getPlanId(), number);
|
|
205
|
+
if (!row) {
|
|
206
|
+
return { success: true, data: null };
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
success: true,
|
|
210
|
+
data: {
|
|
211
|
+
number: row.number,
|
|
212
|
+
code: row.code,
|
|
213
|
+
title: row.title,
|
|
214
|
+
description: row.description || void 0,
|
|
215
|
+
status: row.status,
|
|
216
|
+
startedAt: row.started_at || void 0,
|
|
217
|
+
completedAt: row.completed_at || void 0,
|
|
218
|
+
content: row.content
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
error: error instanceof Error ? error.message : "Failed to get step"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async addStep(step) {
|
|
229
|
+
try {
|
|
230
|
+
this.db.prepare(`
|
|
231
|
+
INSERT INTO plan_steps (plan_id, number, code, title, description, status, started_at, completed_at, content)
|
|
232
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
233
|
+
`).run(
|
|
234
|
+
this.getPlanId(),
|
|
235
|
+
step.number,
|
|
236
|
+
step.code,
|
|
237
|
+
step.title,
|
|
238
|
+
step.description || null,
|
|
239
|
+
step.status,
|
|
240
|
+
step.startedAt || null,
|
|
241
|
+
step.completedAt || null,
|
|
242
|
+
step.content
|
|
243
|
+
);
|
|
244
|
+
return { success: true };
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
error: error instanceof Error ? error.message : "Failed to add step"
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async updateStep(number, updates) {
|
|
253
|
+
try {
|
|
254
|
+
const fields = [];
|
|
255
|
+
const values = [];
|
|
256
|
+
if (updates.code !== void 0) {
|
|
257
|
+
fields.push("code = ?");
|
|
258
|
+
values.push(updates.code);
|
|
259
|
+
}
|
|
260
|
+
if (updates.title !== void 0) {
|
|
261
|
+
fields.push("title = ?");
|
|
262
|
+
values.push(updates.title);
|
|
263
|
+
}
|
|
264
|
+
if (updates.description !== void 0) {
|
|
265
|
+
fields.push("description = ?");
|
|
266
|
+
values.push(updates.description);
|
|
267
|
+
}
|
|
268
|
+
if (updates.status !== void 0) {
|
|
269
|
+
fields.push("status = ?");
|
|
270
|
+
values.push(updates.status);
|
|
271
|
+
}
|
|
272
|
+
if (updates.startedAt !== void 0) {
|
|
273
|
+
fields.push("started_at = ?");
|
|
274
|
+
values.push(updates.startedAt);
|
|
275
|
+
}
|
|
276
|
+
if (updates.completedAt !== void 0) {
|
|
277
|
+
fields.push("completed_at = ?");
|
|
278
|
+
values.push(updates.completedAt);
|
|
279
|
+
}
|
|
280
|
+
if (updates.content !== void 0) {
|
|
281
|
+
fields.push("content = ?");
|
|
282
|
+
values.push(updates.content);
|
|
283
|
+
}
|
|
284
|
+
if (fields.length === 0) {
|
|
285
|
+
return { success: true };
|
|
286
|
+
}
|
|
287
|
+
values.push(this.getPlanId(), number);
|
|
288
|
+
this.db.prepare(`UPDATE plan_steps SET ${fields.join(", ")} WHERE plan_id = ? AND number = ?`).run(...values);
|
|
289
|
+
return { success: true };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
error: error instanceof Error ? error.message : "Failed to update step"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async deleteStep(number) {
|
|
298
|
+
try {
|
|
299
|
+
this.db.prepare("DELETE FROM plan_steps WHERE plan_id = ? AND number = ?").run(this.getPlanId(), number);
|
|
300
|
+
return { success: true };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return {
|
|
303
|
+
success: false,
|
|
304
|
+
error: error instanceof Error ? error.message : "Failed to delete step"
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// ==================== File Operations ====================
|
|
309
|
+
async getFiles() {
|
|
310
|
+
try {
|
|
311
|
+
const rows = this.db.prepare(`
|
|
312
|
+
SELECT file_type, filename, content, created_at, updated_at
|
|
313
|
+
FROM plan_files WHERE plan_id = ?
|
|
314
|
+
`).all(this.getPlanId());
|
|
315
|
+
const files = rows.map((row) => ({
|
|
316
|
+
type: row.file_type,
|
|
317
|
+
filename: row.filename,
|
|
318
|
+
content: row.content,
|
|
319
|
+
createdAt: row.created_at,
|
|
320
|
+
updatedAt: row.updated_at
|
|
321
|
+
}));
|
|
322
|
+
return { success: true, data: files };
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return {
|
|
325
|
+
success: false,
|
|
326
|
+
error: error instanceof Error ? error.message : "Failed to get files"
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async getFile(type, filename) {
|
|
331
|
+
try {
|
|
332
|
+
const row = this.db.prepare(`
|
|
333
|
+
SELECT file_type, filename, content, created_at, updated_at
|
|
334
|
+
FROM plan_files WHERE plan_id = ? AND file_type = ? AND filename = ?
|
|
335
|
+
`).get(this.getPlanId(), type, filename);
|
|
336
|
+
if (!row) {
|
|
337
|
+
return { success: true, data: null };
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
success: true,
|
|
341
|
+
data: {
|
|
342
|
+
type: row.file_type,
|
|
343
|
+
filename: row.filename,
|
|
344
|
+
content: row.content,
|
|
345
|
+
createdAt: row.created_at,
|
|
346
|
+
updatedAt: row.updated_at
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
error: error instanceof Error ? error.message : "Failed to get file"
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async saveFile(file) {
|
|
357
|
+
try {
|
|
358
|
+
this.db.prepare(`
|
|
359
|
+
INSERT INTO plan_files (plan_id, file_type, filename, content, created_at, updated_at)
|
|
360
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
361
|
+
ON CONFLICT(plan_id, file_type, filename) DO UPDATE SET
|
|
362
|
+
content = excluded.content,
|
|
363
|
+
updated_at = excluded.updated_at
|
|
364
|
+
`).run(
|
|
365
|
+
this.getPlanId(),
|
|
366
|
+
file.type,
|
|
367
|
+
file.filename,
|
|
368
|
+
file.content,
|
|
369
|
+
file.createdAt,
|
|
370
|
+
file.updatedAt
|
|
371
|
+
);
|
|
372
|
+
return { success: true };
|
|
373
|
+
} catch (error) {
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
error: error instanceof Error ? error.message : "Failed to save file"
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async deleteFile(type, filename) {
|
|
381
|
+
try {
|
|
382
|
+
this.db.prepare("DELETE FROM plan_files WHERE plan_id = ? AND file_type = ? AND filename = ?").run(this.getPlanId(), type, filename);
|
|
383
|
+
return { success: true };
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return {
|
|
386
|
+
success: false,
|
|
387
|
+
error: error instanceof Error ? error.message : "Failed to delete file"
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// ==================== Timeline Operations ====================
|
|
392
|
+
async getTimelineEvents(options) {
|
|
393
|
+
try {
|
|
394
|
+
let sql = "SELECT id, timestamp, event_type, data FROM timeline_events WHERE plan_id = ?";
|
|
395
|
+
const params = [this.getPlanId()];
|
|
396
|
+
if (options?.since) {
|
|
397
|
+
sql += " AND timestamp >= ?";
|
|
398
|
+
params.push(options.since);
|
|
399
|
+
}
|
|
400
|
+
if (options?.type) {
|
|
401
|
+
sql += " AND event_type = ?";
|
|
402
|
+
params.push(options.type);
|
|
403
|
+
}
|
|
404
|
+
sql += " ORDER BY timestamp DESC";
|
|
405
|
+
if (options?.limit) {
|
|
406
|
+
sql += " LIMIT ?";
|
|
407
|
+
params.push(options.limit);
|
|
408
|
+
}
|
|
409
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
410
|
+
const events = rows.map((row) => ({
|
|
411
|
+
id: row.id,
|
|
412
|
+
timestamp: row.timestamp,
|
|
413
|
+
type: row.event_type,
|
|
414
|
+
data: JSON.parse(row.data)
|
|
415
|
+
}));
|
|
416
|
+
return { success: true, data: events };
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
error: error instanceof Error ? error.message : "Failed to get timeline events"
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async addTimelineEvent(event) {
|
|
425
|
+
try {
|
|
426
|
+
this.db.prepare(`
|
|
427
|
+
INSERT INTO timeline_events (id, plan_id, timestamp, event_type, data)
|
|
428
|
+
VALUES (?, ?, ?, ?, ?)
|
|
429
|
+
`).run(
|
|
430
|
+
event.id || randomUUID(),
|
|
431
|
+
this.getPlanId(),
|
|
432
|
+
event.timestamp,
|
|
433
|
+
event.type,
|
|
434
|
+
JSON.stringify(event.data)
|
|
435
|
+
);
|
|
436
|
+
return { success: true };
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return {
|
|
439
|
+
success: false,
|
|
440
|
+
error: error instanceof Error ? error.message : "Failed to add timeline event"
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// ==================== Evidence Operations ====================
|
|
445
|
+
async getEvidence() {
|
|
446
|
+
try {
|
|
447
|
+
const rows = this.db.prepare(`
|
|
448
|
+
SELECT id, description, source, source_url, gathering_method, content, file_path,
|
|
449
|
+
relevance_score, original_query, summary, created_at
|
|
450
|
+
FROM evidence_records WHERE plan_id = ?
|
|
451
|
+
`).all(this.getPlanId());
|
|
452
|
+
const evidence = rows.map((row) => ({
|
|
453
|
+
id: row.id,
|
|
454
|
+
description: row.description,
|
|
455
|
+
source: row.source || void 0,
|
|
456
|
+
sourceUrl: row.source_url || void 0,
|
|
457
|
+
gatheringMethod: row.gathering_method,
|
|
458
|
+
content: row.content || void 0,
|
|
459
|
+
filePath: row.file_path || void 0,
|
|
460
|
+
relevanceScore: row.relevance_score ?? void 0,
|
|
461
|
+
originalQuery: row.original_query || void 0,
|
|
462
|
+
summary: row.summary || void 0,
|
|
463
|
+
createdAt: row.created_at
|
|
464
|
+
}));
|
|
465
|
+
return { success: true, data: evidence };
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
error: error instanceof Error ? error.message : "Failed to get evidence"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async addEvidence(evidence) {
|
|
474
|
+
try {
|
|
475
|
+
this.db.prepare(`
|
|
476
|
+
INSERT INTO evidence_records (id, plan_id, description, source, source_url, gathering_method,
|
|
477
|
+
content, file_path, relevance_score, original_query, summary, created_at)
|
|
478
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
479
|
+
`).run(
|
|
480
|
+
evidence.id || randomUUID(),
|
|
481
|
+
this.getPlanId(),
|
|
482
|
+
evidence.description,
|
|
483
|
+
evidence.source || null,
|
|
484
|
+
evidence.sourceUrl || null,
|
|
485
|
+
evidence.gatheringMethod || null,
|
|
486
|
+
evidence.content || null,
|
|
487
|
+
evidence.filePath || null,
|
|
488
|
+
evidence.relevanceScore ?? null,
|
|
489
|
+
evidence.originalQuery || null,
|
|
490
|
+
evidence.summary || null,
|
|
491
|
+
evidence.createdAt
|
|
492
|
+
);
|
|
493
|
+
return { success: true };
|
|
494
|
+
} catch (error) {
|
|
495
|
+
return {
|
|
496
|
+
success: false,
|
|
497
|
+
error: error instanceof Error ? error.message : "Failed to add evidence"
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// ==================== Feedback Operations ====================
|
|
502
|
+
async getFeedback() {
|
|
503
|
+
try {
|
|
504
|
+
const rows = this.db.prepare(`
|
|
505
|
+
SELECT id, title, platform, content, participants, created_at
|
|
506
|
+
FROM feedback_records WHERE plan_id = ?
|
|
507
|
+
`).all(this.getPlanId());
|
|
508
|
+
const feedback = rows.map((row) => ({
|
|
509
|
+
id: row.id,
|
|
510
|
+
title: row.title || void 0,
|
|
511
|
+
platform: row.platform || void 0,
|
|
512
|
+
content: row.content,
|
|
513
|
+
participants: row.participants ? JSON.parse(row.participants) : void 0,
|
|
514
|
+
createdAt: row.created_at
|
|
515
|
+
}));
|
|
516
|
+
return { success: true, data: feedback };
|
|
517
|
+
} catch (error) {
|
|
518
|
+
return {
|
|
519
|
+
success: false,
|
|
520
|
+
error: error instanceof Error ? error.message : "Failed to get feedback"
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async addFeedback(feedback) {
|
|
525
|
+
try {
|
|
526
|
+
this.db.prepare(`
|
|
527
|
+
INSERT INTO feedback_records (id, plan_id, title, platform, content, participants, created_at)
|
|
528
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
529
|
+
`).run(
|
|
530
|
+
feedback.id || randomUUID(),
|
|
531
|
+
this.getPlanId(),
|
|
532
|
+
feedback.title || null,
|
|
533
|
+
feedback.platform || null,
|
|
534
|
+
feedback.content,
|
|
535
|
+
feedback.participants ? JSON.stringify(feedback.participants) : null,
|
|
536
|
+
feedback.createdAt
|
|
537
|
+
);
|
|
538
|
+
return { success: true };
|
|
539
|
+
} catch (error) {
|
|
540
|
+
return {
|
|
541
|
+
success: false,
|
|
542
|
+
error: error instanceof Error ? error.message : "Failed to add feedback"
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// ==================== Checkpoint Operations ====================
|
|
547
|
+
async getCheckpoints() {
|
|
548
|
+
try {
|
|
549
|
+
const rows = this.db.prepare(`
|
|
550
|
+
SELECT name, message, created_at, snapshot
|
|
551
|
+
FROM checkpoints WHERE plan_id = ? ORDER BY created_at DESC
|
|
552
|
+
`).all(this.getPlanId());
|
|
553
|
+
const checkpoints = rows.map((row) => ({
|
|
554
|
+
name: row.name,
|
|
555
|
+
message: row.message,
|
|
556
|
+
createdAt: row.created_at,
|
|
557
|
+
snapshot: JSON.parse(row.snapshot)
|
|
558
|
+
}));
|
|
559
|
+
return { success: true, data: checkpoints };
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
error: error instanceof Error ? error.message : "Failed to get checkpoints"
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async getCheckpoint(name) {
|
|
568
|
+
try {
|
|
569
|
+
const row = this.db.prepare(`
|
|
570
|
+
SELECT name, message, created_at, snapshot
|
|
571
|
+
FROM checkpoints WHERE plan_id = ? AND name = ?
|
|
572
|
+
`).get(this.getPlanId(), name);
|
|
573
|
+
if (!row) {
|
|
574
|
+
return { success: true, data: null };
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
success: true,
|
|
578
|
+
data: {
|
|
579
|
+
name: row.name,
|
|
580
|
+
message: row.message,
|
|
581
|
+
createdAt: row.created_at,
|
|
582
|
+
snapshot: JSON.parse(row.snapshot)
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
} catch (error) {
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
error: error instanceof Error ? error.message : "Failed to get checkpoint"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async createCheckpoint(checkpoint) {
|
|
593
|
+
try {
|
|
594
|
+
this.db.prepare(`
|
|
595
|
+
INSERT INTO checkpoints (plan_id, name, message, created_at, snapshot)
|
|
596
|
+
VALUES (?, ?, ?, ?, ?)
|
|
597
|
+
ON CONFLICT(plan_id, name) DO UPDATE SET
|
|
598
|
+
message = excluded.message,
|
|
599
|
+
created_at = excluded.created_at,
|
|
600
|
+
snapshot = excluded.snapshot
|
|
601
|
+
`).run(
|
|
602
|
+
this.getPlanId(),
|
|
603
|
+
checkpoint.name,
|
|
604
|
+
checkpoint.message,
|
|
605
|
+
checkpoint.createdAt,
|
|
606
|
+
JSON.stringify(checkpoint.snapshot)
|
|
607
|
+
);
|
|
608
|
+
return { success: true };
|
|
609
|
+
} catch (error) {
|
|
610
|
+
return {
|
|
611
|
+
success: false,
|
|
612
|
+
error: error instanceof Error ? error.message : "Failed to create checkpoint"
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async restoreCheckpoint(name) {
|
|
617
|
+
try {
|
|
618
|
+
const checkpointResult = await this.getCheckpoint(name);
|
|
619
|
+
if (!checkpointResult.success || !checkpointResult.data) {
|
|
620
|
+
return { success: false, error: `Checkpoint not found: ${name}` };
|
|
621
|
+
}
|
|
622
|
+
const snapshot = checkpointResult.data.snapshot;
|
|
623
|
+
const planId = this.getPlanId();
|
|
624
|
+
const restore = this.db.transaction(() => {
|
|
625
|
+
this.db.prepare(`
|
|
626
|
+
UPDATE plans SET name = ?, description = ?, stage = ?, updated_at = ?
|
|
627
|
+
WHERE id = ?
|
|
628
|
+
`).run(
|
|
629
|
+
snapshot.metadata.name,
|
|
630
|
+
snapshot.metadata.description || null,
|
|
631
|
+
snapshot.metadata.stage,
|
|
632
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
633
|
+
planId
|
|
634
|
+
);
|
|
635
|
+
for (const step of snapshot.steps) {
|
|
636
|
+
this.db.prepare(`
|
|
637
|
+
UPDATE plan_steps SET status = ?, started_at = ?, completed_at = ?
|
|
638
|
+
WHERE plan_id = ? AND number = ?
|
|
639
|
+
`).run(
|
|
640
|
+
step.status,
|
|
641
|
+
step.startedAt || null,
|
|
642
|
+
step.completedAt || null,
|
|
643
|
+
planId,
|
|
644
|
+
step.number
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
for (const file of snapshot.files) {
|
|
648
|
+
this.db.prepare(`
|
|
649
|
+
INSERT INTO plan_files (plan_id, file_type, filename, content, created_at, updated_at)
|
|
650
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
651
|
+
ON CONFLICT(plan_id, file_type, filename) DO UPDATE SET
|
|
652
|
+
content = excluded.content,
|
|
653
|
+
updated_at = excluded.updated_at
|
|
654
|
+
`).run(
|
|
655
|
+
planId,
|
|
656
|
+
file.type,
|
|
657
|
+
file.filename,
|
|
658
|
+
file.content,
|
|
659
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
660
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
restore();
|
|
665
|
+
return { success: true };
|
|
666
|
+
} catch (error) {
|
|
667
|
+
return {
|
|
668
|
+
success: false,
|
|
669
|
+
error: error instanceof Error ? error.message : "Failed to restore checkpoint"
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// ==================== Search Operations ====================
|
|
674
|
+
async search(query) {
|
|
675
|
+
try {
|
|
676
|
+
const results = [];
|
|
677
|
+
const planId = this.getPlanId();
|
|
678
|
+
const searchPattern = `%${query}%`;
|
|
679
|
+
const stepRows = this.db.prepare(`
|
|
680
|
+
SELECT number, title, content FROM plan_steps
|
|
681
|
+
WHERE plan_id = ? AND (title LIKE ? OR content LIKE ?)
|
|
682
|
+
`).all(planId, searchPattern, searchPattern);
|
|
683
|
+
for (const row of stepRows) {
|
|
684
|
+
results.push({
|
|
685
|
+
type: "step",
|
|
686
|
+
id: String(row.number),
|
|
687
|
+
snippet: this.extractSnippet(row.content, query),
|
|
688
|
+
score: this.calculateScore(row.content, query)
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
const fileRows = this.db.prepare(`
|
|
692
|
+
SELECT file_type, filename, content FROM plan_files
|
|
693
|
+
WHERE plan_id = ? AND content LIKE ?
|
|
694
|
+
`).all(planId, searchPattern);
|
|
695
|
+
for (const row of fileRows) {
|
|
696
|
+
results.push({
|
|
697
|
+
type: "file",
|
|
698
|
+
id: row.filename,
|
|
699
|
+
snippet: this.extractSnippet(row.content, query),
|
|
700
|
+
score: this.calculateScore(row.content, query)
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
const evidenceRows = this.db.prepare(`
|
|
704
|
+
SELECT id, description, content FROM evidence_records
|
|
705
|
+
WHERE plan_id = ? AND (description LIKE ? OR content LIKE ?)
|
|
706
|
+
`).all(planId, searchPattern, searchPattern);
|
|
707
|
+
for (const row of evidenceRows) {
|
|
708
|
+
results.push({
|
|
709
|
+
type: "evidence",
|
|
710
|
+
id: row.id,
|
|
711
|
+
snippet: this.extractSnippet(row.content || row.description, query),
|
|
712
|
+
score: this.calculateScore(row.content || row.description, query)
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
results.sort((a, b) => b.score - a.score);
|
|
716
|
+
return { success: true, data: results };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
return {
|
|
719
|
+
success: false,
|
|
720
|
+
error: error instanceof Error ? error.message : "Failed to search"
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
extractSnippet(content, query) {
|
|
725
|
+
const lowerContent = content.toLowerCase();
|
|
726
|
+
const lowerQuery = query.toLowerCase();
|
|
727
|
+
const index = lowerContent.indexOf(lowerQuery);
|
|
728
|
+
if (index === -1) {
|
|
729
|
+
return content.slice(0, 100) + "...";
|
|
730
|
+
}
|
|
731
|
+
const start = Math.max(0, index - 50);
|
|
732
|
+
const end = Math.min(content.length, index + query.length + 50);
|
|
733
|
+
let snippet = content.slice(start, end);
|
|
734
|
+
if (start > 0) snippet = "..." + snippet;
|
|
735
|
+
if (end < content.length) snippet = snippet + "...";
|
|
736
|
+
return snippet;
|
|
737
|
+
}
|
|
738
|
+
calculateScore(content, query) {
|
|
739
|
+
const lowerContent = content.toLowerCase();
|
|
740
|
+
const lowerQuery = query.toLowerCase();
|
|
741
|
+
let count = 0;
|
|
742
|
+
let pos = 0;
|
|
743
|
+
while ((pos = lowerContent.indexOf(lowerQuery, pos)) !== -1) {
|
|
744
|
+
count++;
|
|
745
|
+
pos += lowerQuery.length;
|
|
746
|
+
}
|
|
747
|
+
return Math.min(1, count / 10);
|
|
748
|
+
}
|
|
749
|
+
// ==================== Utility Methods ====================
|
|
750
|
+
/**
|
|
751
|
+
* Create a snapshot of the current plan state for checkpoints
|
|
752
|
+
*/
|
|
753
|
+
async createSnapshot() {
|
|
754
|
+
const metadataResult = await this.getMetadata();
|
|
755
|
+
const stepsResult = await this.getSteps();
|
|
756
|
+
const filesResult = await this.getFiles();
|
|
757
|
+
if (!metadataResult.success || !metadataResult.data) {
|
|
758
|
+
throw new Error("Failed to get metadata for snapshot");
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
metadata: metadataResult.data,
|
|
762
|
+
steps: (stepsResult.data || []).map((s) => ({
|
|
763
|
+
number: s.number,
|
|
764
|
+
status: s.status,
|
|
765
|
+
startedAt: s.startedAt,
|
|
766
|
+
completedAt: s.completedAt
|
|
767
|
+
})),
|
|
768
|
+
files: (filesResult.data || []).map((f) => ({
|
|
769
|
+
type: f.type,
|
|
770
|
+
filename: f.filename,
|
|
771
|
+
content: f.content
|
|
772
|
+
}))
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function createSqliteProvider(planPath) {
|
|
777
|
+
return new SqliteStorageProvider(planPath);
|
|
778
|
+
}
|
|
779
|
+
class DirectoryStorageProvider {
|
|
780
|
+
format = "directory";
|
|
781
|
+
path;
|
|
782
|
+
constructor(planPath) {
|
|
783
|
+
this.path = planPath;
|
|
784
|
+
}
|
|
785
|
+
async exists() {
|
|
786
|
+
throw new Error("DirectoryStorageProvider.exists() not implemented - use main riotplan package");
|
|
787
|
+
}
|
|
788
|
+
async initialize(_metadata) {
|
|
789
|
+
throw new Error("DirectoryStorageProvider.initialize() not implemented - use main riotplan package");
|
|
790
|
+
}
|
|
791
|
+
async close() {
|
|
792
|
+
}
|
|
793
|
+
// Metadata operations
|
|
794
|
+
async getMetadata() {
|
|
795
|
+
throw new Error("DirectoryStorageProvider.getMetadata() not implemented - use main riotplan package");
|
|
796
|
+
}
|
|
797
|
+
async updateMetadata(_updates) {
|
|
798
|
+
throw new Error("DirectoryStorageProvider.updateMetadata() not implemented - use main riotplan package");
|
|
799
|
+
}
|
|
800
|
+
// Step operations
|
|
801
|
+
async getSteps() {
|
|
802
|
+
throw new Error("DirectoryStorageProvider.getSteps() not implemented - use main riotplan package");
|
|
803
|
+
}
|
|
804
|
+
async getStep(_number) {
|
|
805
|
+
throw new Error("DirectoryStorageProvider.getStep() not implemented - use main riotplan package");
|
|
806
|
+
}
|
|
807
|
+
async addStep(_step) {
|
|
808
|
+
throw new Error("DirectoryStorageProvider.addStep() not implemented - use main riotplan package");
|
|
809
|
+
}
|
|
810
|
+
async updateStep(_number, _updates) {
|
|
811
|
+
throw new Error("DirectoryStorageProvider.updateStep() not implemented - use main riotplan package");
|
|
812
|
+
}
|
|
813
|
+
async deleteStep(_number) {
|
|
814
|
+
throw new Error("DirectoryStorageProvider.deleteStep() not implemented - use main riotplan package");
|
|
815
|
+
}
|
|
816
|
+
// File operations
|
|
817
|
+
async getFiles() {
|
|
818
|
+
throw new Error("DirectoryStorageProvider.getFiles() not implemented - use main riotplan package");
|
|
819
|
+
}
|
|
820
|
+
async getFile(_type, _filename) {
|
|
821
|
+
throw new Error("DirectoryStorageProvider.getFile() not implemented - use main riotplan package");
|
|
822
|
+
}
|
|
823
|
+
async saveFile(_file) {
|
|
824
|
+
throw new Error("DirectoryStorageProvider.saveFile() not implemented - use main riotplan package");
|
|
825
|
+
}
|
|
826
|
+
async deleteFile(_type, _filename) {
|
|
827
|
+
throw new Error("DirectoryStorageProvider.deleteFile() not implemented - use main riotplan package");
|
|
828
|
+
}
|
|
829
|
+
// Timeline operations
|
|
830
|
+
async getTimelineEvents() {
|
|
831
|
+
throw new Error("DirectoryStorageProvider.getTimelineEvents() not implemented - use main riotplan package");
|
|
832
|
+
}
|
|
833
|
+
async addTimelineEvent(_event) {
|
|
834
|
+
throw new Error("DirectoryStorageProvider.addTimelineEvent() not implemented - use main riotplan package");
|
|
835
|
+
}
|
|
836
|
+
// Evidence operations
|
|
837
|
+
async getEvidence() {
|
|
838
|
+
throw new Error("DirectoryStorageProvider.getEvidence() not implemented - use main riotplan package");
|
|
839
|
+
}
|
|
840
|
+
async addEvidence(_evidence) {
|
|
841
|
+
throw new Error("DirectoryStorageProvider.addEvidence() not implemented - use main riotplan package");
|
|
842
|
+
}
|
|
843
|
+
// Feedback operations
|
|
844
|
+
async getFeedback() {
|
|
845
|
+
throw new Error("DirectoryStorageProvider.getFeedback() not implemented - use main riotplan package");
|
|
846
|
+
}
|
|
847
|
+
async addFeedback(_feedback) {
|
|
848
|
+
throw new Error("DirectoryStorageProvider.addFeedback() not implemented - use main riotplan package");
|
|
849
|
+
}
|
|
850
|
+
// Checkpoint operations
|
|
851
|
+
async getCheckpoints() {
|
|
852
|
+
throw new Error("DirectoryStorageProvider.getCheckpoints() not implemented - use main riotplan package");
|
|
853
|
+
}
|
|
854
|
+
async getCheckpoint(_name) {
|
|
855
|
+
throw new Error("DirectoryStorageProvider.getCheckpoint() not implemented - use main riotplan package");
|
|
856
|
+
}
|
|
857
|
+
async createCheckpoint(_checkpoint) {
|
|
858
|
+
throw new Error("DirectoryStorageProvider.createCheckpoint() not implemented - use main riotplan package");
|
|
859
|
+
}
|
|
860
|
+
async restoreCheckpoint(_name) {
|
|
861
|
+
throw new Error("DirectoryStorageProvider.restoreCheckpoint() not implemented - use main riotplan package");
|
|
862
|
+
}
|
|
863
|
+
// Search operations
|
|
864
|
+
async search(_query) {
|
|
865
|
+
throw new Error("DirectoryStorageProvider.search() not implemented - use main riotplan package");
|
|
866
|
+
}
|
|
867
|
+
// Snapshot operations
|
|
868
|
+
async createSnapshot() {
|
|
869
|
+
throw new Error("DirectoryStorageProvider.createSnapshot() not implemented - use main riotplan package");
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function createDirectoryProvider(planPath) {
|
|
873
|
+
return new DirectoryStorageProvider(planPath);
|
|
874
|
+
}
|
|
875
|
+
const SQLITE_HEADER = Buffer.from("SQLite format 3\0");
|
|
876
|
+
const DIRECTORY_PLAN_MARKERS = [
|
|
877
|
+
"SUMMARY.md",
|
|
878
|
+
"STATUS.md",
|
|
879
|
+
"IDEA.md",
|
|
880
|
+
"EXECUTION_PLAN.md"
|
|
881
|
+
];
|
|
882
|
+
function detectPlanFormat(planPath) {
|
|
883
|
+
if (!existsSync(planPath)) {
|
|
884
|
+
return "unknown";
|
|
885
|
+
}
|
|
886
|
+
const stats = statSync(planPath);
|
|
887
|
+
if (stats.isDirectory()) {
|
|
888
|
+
for (const marker of DIRECTORY_PLAN_MARKERS) {
|
|
889
|
+
if (existsSync(join(planPath, marker))) {
|
|
890
|
+
return "directory";
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const planDir = join(planPath, "plan");
|
|
894
|
+
if (existsSync(planDir) && statSync(planDir).isDirectory()) {
|
|
895
|
+
return "directory";
|
|
896
|
+
}
|
|
897
|
+
return "unknown";
|
|
898
|
+
}
|
|
899
|
+
if (stats.isFile()) {
|
|
900
|
+
if (hasSqliteHeader(planPath)) {
|
|
901
|
+
return "sqlite";
|
|
902
|
+
}
|
|
903
|
+
if (planPath.endsWith(".plan")) {
|
|
904
|
+
return "unknown";
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return "unknown";
|
|
908
|
+
}
|
|
909
|
+
function hasSqliteHeader(filePath) {
|
|
910
|
+
try {
|
|
911
|
+
const fd = readFileSync(filePath, { flag: "r" });
|
|
912
|
+
if (fd.length < SQLITE_HEADER.length) {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
return fd.subarray(0, SQLITE_HEADER.length).equals(SQLITE_HEADER);
|
|
916
|
+
} catch {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function isSqlitePath(planPath, config) {
|
|
921
|
+
const extension = config?.sqlite?.extension ?? DEFAULT_FORMAT_CONFIG.sqlite.extension;
|
|
922
|
+
return planPath.endsWith(extension);
|
|
923
|
+
}
|
|
924
|
+
function isDirectoryPath(planPath) {
|
|
925
|
+
if (existsSync(planPath)) {
|
|
926
|
+
return statSync(planPath).isDirectory();
|
|
927
|
+
}
|
|
928
|
+
const ext = extname(planPath);
|
|
929
|
+
return ext === "" || ext === ".";
|
|
930
|
+
}
|
|
931
|
+
function getFormatExtension(format, config) {
|
|
932
|
+
if (format === "sqlite") {
|
|
933
|
+
const extension = config?.sqlite?.extension ?? DEFAULT_FORMAT_CONFIG.sqlite.extension;
|
|
934
|
+
return extension;
|
|
935
|
+
}
|
|
936
|
+
return "";
|
|
937
|
+
}
|
|
938
|
+
function ensureFormatExtension(planPath, format, config) {
|
|
939
|
+
if (format === "sqlite") {
|
|
940
|
+
const extension = getFormatExtension(format, config);
|
|
941
|
+
if (!planPath.endsWith(extension)) {
|
|
942
|
+
return `${planPath}${extension}`;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return planPath;
|
|
946
|
+
}
|
|
947
|
+
function inferFormatFromPath(planPath, config) {
|
|
948
|
+
if (existsSync(planPath)) {
|
|
949
|
+
const detected = detectPlanFormat(planPath);
|
|
950
|
+
if (detected !== "unknown") {
|
|
951
|
+
return detected;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (isSqlitePath(planPath, config)) {
|
|
955
|
+
return "sqlite";
|
|
956
|
+
}
|
|
957
|
+
if (isDirectoryPath(planPath)) {
|
|
958
|
+
return "directory";
|
|
959
|
+
}
|
|
960
|
+
return config?.defaultFormat ?? DEFAULT_FORMAT_CONFIG.defaultFormat;
|
|
961
|
+
}
|
|
962
|
+
function validatePlanPath(planPath, format, config) {
|
|
963
|
+
if (!planPath || planPath.trim() === "") {
|
|
964
|
+
return "Plan path cannot be empty";
|
|
965
|
+
}
|
|
966
|
+
if (format === "sqlite") {
|
|
967
|
+
const extension = getFormatExtension(format, config);
|
|
968
|
+
if (!planPath.endsWith(extension)) {
|
|
969
|
+
return `SQLite plan path must end with ${extension}`;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (format === "directory") {
|
|
973
|
+
const ext = extname(planPath);
|
|
974
|
+
if (ext && ext !== ".") {
|
|
975
|
+
return `Directory plan path should not have a file extension (got ${ext})`;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
function getPlanNameFromPath(planPath, format, config) {
|
|
981
|
+
let name = planPath;
|
|
982
|
+
if (format === "sqlite") {
|
|
983
|
+
const extension = getFormatExtension(format, config);
|
|
984
|
+
if (name.endsWith(extension)) {
|
|
985
|
+
name = name.slice(0, -extension.length);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const parts = name.split(/[/\\]/);
|
|
989
|
+
return parts[parts.length - 1] || name;
|
|
990
|
+
}
|
|
991
|
+
class DefaultStorageProviderFactory {
|
|
992
|
+
config;
|
|
993
|
+
constructor(config) {
|
|
994
|
+
this.config = mergeFormatConfig(config);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Create a storage provider for the given path
|
|
998
|
+
*
|
|
999
|
+
* @param planPath - Path to the plan
|
|
1000
|
+
* @param options - Optional creation options
|
|
1001
|
+
* @returns A storage provider instance
|
|
1002
|
+
*/
|
|
1003
|
+
createProvider(planPath, options) {
|
|
1004
|
+
const format = this.determineFormat(planPath, options?.format);
|
|
1005
|
+
if (format === "sqlite") {
|
|
1006
|
+
const finalPath = ensureFormatExtension(planPath, "sqlite", this.config);
|
|
1007
|
+
return new SqliteStorageProvider(finalPath);
|
|
1008
|
+
}
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
"Directory storage provider is not yet implemented in riotplan-format. Use the main riotplan package for directory-based plans."
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Check if this factory supports the given path
|
|
1015
|
+
*
|
|
1016
|
+
* @param planPath - Path to check
|
|
1017
|
+
* @returns True if a provider can be created for this path
|
|
1018
|
+
*/
|
|
1019
|
+
supportsPath(planPath) {
|
|
1020
|
+
const format = detectPlanFormat(planPath);
|
|
1021
|
+
if (format !== "unknown") {
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
return isSqlitePath(planPath, this.config) || isDirectoryPath(planPath);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Determine the format to use for a path
|
|
1028
|
+
*
|
|
1029
|
+
* @param planPath - The plan path
|
|
1030
|
+
* @param forcedFormat - Optional format override
|
|
1031
|
+
* @returns The format to use
|
|
1032
|
+
*/
|
|
1033
|
+
determineFormat(planPath, forcedFormat) {
|
|
1034
|
+
if (forcedFormat) {
|
|
1035
|
+
return forcedFormat;
|
|
1036
|
+
}
|
|
1037
|
+
const detected = detectPlanFormat(planPath);
|
|
1038
|
+
if (detected !== "unknown") {
|
|
1039
|
+
return detected;
|
|
1040
|
+
}
|
|
1041
|
+
return inferFormatFromPath(planPath, this.config);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Get the default format from configuration
|
|
1045
|
+
*/
|
|
1046
|
+
get defaultFormat() {
|
|
1047
|
+
return this.config.defaultFormat;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Get the SQLite configuration
|
|
1051
|
+
*/
|
|
1052
|
+
get sqliteConfig() {
|
|
1053
|
+
return this.config.sqlite;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get the directory configuration
|
|
1057
|
+
*/
|
|
1058
|
+
get directoryConfig() {
|
|
1059
|
+
return this.config.directory;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function createStorageFactory(config) {
|
|
1063
|
+
return new DefaultStorageProviderFactory(config);
|
|
1064
|
+
}
|
|
1065
|
+
function createProvider(planPath, options) {
|
|
1066
|
+
const factory = createStorageFactory(options?.config);
|
|
1067
|
+
return factory.createProvider(planPath, options);
|
|
1068
|
+
}
|
|
1069
|
+
class MigrationValidator {
|
|
1070
|
+
/**
|
|
1071
|
+
* Validate that target contains all data from source
|
|
1072
|
+
*/
|
|
1073
|
+
async validate(source, target) {
|
|
1074
|
+
const errors = [];
|
|
1075
|
+
const warnings = [];
|
|
1076
|
+
const stats = {
|
|
1077
|
+
stepsCompared: 0,
|
|
1078
|
+
filesCompared: 0,
|
|
1079
|
+
timelineEventsCompared: 0,
|
|
1080
|
+
evidenceCompared: 0,
|
|
1081
|
+
feedbackCompared: 0
|
|
1082
|
+
};
|
|
1083
|
+
await this.validateMetadata(source, target, errors);
|
|
1084
|
+
const stepsResult = await this.validateSteps(source, target, errors);
|
|
1085
|
+
stats.stepsCompared = stepsResult;
|
|
1086
|
+
const filesResult = await this.validateFiles(source, target, errors);
|
|
1087
|
+
stats.filesCompared = filesResult;
|
|
1088
|
+
const timelineResult = await this.validateTimeline(source, target, errors, warnings);
|
|
1089
|
+
stats.timelineEventsCompared = timelineResult;
|
|
1090
|
+
const evidenceResult = await this.validateEvidence(source, target, errors);
|
|
1091
|
+
stats.evidenceCompared = evidenceResult;
|
|
1092
|
+
const feedbackResult = await this.validateFeedback(source, target, errors);
|
|
1093
|
+
stats.feedbackCompared = feedbackResult;
|
|
1094
|
+
return {
|
|
1095
|
+
valid: errors.length === 0,
|
|
1096
|
+
errors,
|
|
1097
|
+
warnings,
|
|
1098
|
+
stats
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
async validateMetadata(source, target, errors) {
|
|
1102
|
+
const sourceResult = await source.getMetadata();
|
|
1103
|
+
const targetResult = await target.getMetadata();
|
|
1104
|
+
if (!sourceResult.success || !sourceResult.data) {
|
|
1105
|
+
errors.push({
|
|
1106
|
+
type: "metadata_difference",
|
|
1107
|
+
path: "metadata",
|
|
1108
|
+
expected: "valid metadata",
|
|
1109
|
+
actual: "failed to read source metadata",
|
|
1110
|
+
message: "Could not read source metadata"
|
|
1111
|
+
});
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (!targetResult.success || !targetResult.data) {
|
|
1115
|
+
errors.push({
|
|
1116
|
+
type: "metadata_difference",
|
|
1117
|
+
path: "metadata",
|
|
1118
|
+
expected: "valid metadata",
|
|
1119
|
+
actual: "failed to read target metadata",
|
|
1120
|
+
message: "Could not read target metadata"
|
|
1121
|
+
});
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const sourceData = sourceResult.data;
|
|
1125
|
+
const targetData = targetResult.data;
|
|
1126
|
+
if (sourceData.id !== targetData.id) {
|
|
1127
|
+
errors.push({
|
|
1128
|
+
type: "metadata_difference",
|
|
1129
|
+
path: "metadata.id",
|
|
1130
|
+
expected: sourceData.id,
|
|
1131
|
+
actual: targetData.id,
|
|
1132
|
+
message: `Plan ID mismatch: expected "${sourceData.id}", got "${targetData.id}"`
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
if (sourceData.name !== targetData.name) {
|
|
1136
|
+
errors.push({
|
|
1137
|
+
type: "metadata_difference",
|
|
1138
|
+
path: "metadata.name",
|
|
1139
|
+
expected: sourceData.name,
|
|
1140
|
+
actual: targetData.name,
|
|
1141
|
+
message: `Plan name mismatch: expected "${sourceData.name}", got "${targetData.name}"`
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
if (sourceData.stage !== targetData.stage) {
|
|
1145
|
+
errors.push({
|
|
1146
|
+
type: "metadata_difference",
|
|
1147
|
+
path: "metadata.stage",
|
|
1148
|
+
expected: sourceData.stage,
|
|
1149
|
+
actual: targetData.stage,
|
|
1150
|
+
message: `Plan stage mismatch: expected "${sourceData.stage}", got "${targetData.stage}"`
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
async validateSteps(source, target, errors) {
|
|
1155
|
+
const sourceResult = await source.getSteps();
|
|
1156
|
+
const targetResult = await target.getSteps();
|
|
1157
|
+
if (!sourceResult.success || !sourceResult.data) {
|
|
1158
|
+
return 0;
|
|
1159
|
+
}
|
|
1160
|
+
const sourceSteps = sourceResult.data;
|
|
1161
|
+
const targetSteps = targetResult.data || [];
|
|
1162
|
+
for (const sourceStep of sourceSteps) {
|
|
1163
|
+
const targetStep = targetSteps.find((s) => s.number === sourceStep.number);
|
|
1164
|
+
if (!targetStep) {
|
|
1165
|
+
errors.push({
|
|
1166
|
+
type: "missing_step",
|
|
1167
|
+
path: `steps[${sourceStep.number}]`,
|
|
1168
|
+
expected: sourceStep,
|
|
1169
|
+
actual: null,
|
|
1170
|
+
message: `Step ${sourceStep.number} is missing in target`
|
|
1171
|
+
});
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (sourceStep.title !== targetStep.title) {
|
|
1175
|
+
errors.push({
|
|
1176
|
+
type: "content_mismatch",
|
|
1177
|
+
path: `steps[${sourceStep.number}].title`,
|
|
1178
|
+
expected: sourceStep.title,
|
|
1179
|
+
actual: targetStep.title,
|
|
1180
|
+
message: `Step ${sourceStep.number} title mismatch`
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
if (sourceStep.status !== targetStep.status) {
|
|
1184
|
+
errors.push({
|
|
1185
|
+
type: "content_mismatch",
|
|
1186
|
+
path: `steps[${sourceStep.number}].status`,
|
|
1187
|
+
expected: sourceStep.status,
|
|
1188
|
+
actual: targetStep.status,
|
|
1189
|
+
message: `Step ${sourceStep.number} status mismatch`
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
const sourceContent = sourceStep.content.trim();
|
|
1193
|
+
const targetContent = targetStep.content.trim();
|
|
1194
|
+
if (sourceContent !== targetContent) {
|
|
1195
|
+
errors.push({
|
|
1196
|
+
type: "content_mismatch",
|
|
1197
|
+
path: `steps[${sourceStep.number}].content`,
|
|
1198
|
+
expected: `${sourceContent.length} chars`,
|
|
1199
|
+
actual: `${targetContent.length} chars`,
|
|
1200
|
+
message: `Step ${sourceStep.number} content mismatch`
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
for (const targetStep of targetSteps) {
|
|
1205
|
+
const sourceStep = sourceSteps.find((s) => s.number === targetStep.number);
|
|
1206
|
+
if (!sourceStep) {
|
|
1207
|
+
errors.push({
|
|
1208
|
+
type: "content_mismatch",
|
|
1209
|
+
path: `steps[${targetStep.number}]`,
|
|
1210
|
+
expected: null,
|
|
1211
|
+
actual: targetStep,
|
|
1212
|
+
message: `Unexpected step ${targetStep.number} in target`
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return sourceSteps.length;
|
|
1217
|
+
}
|
|
1218
|
+
async validateFiles(source, target, errors) {
|
|
1219
|
+
const sourceResult = await source.getFiles();
|
|
1220
|
+
const targetResult = await target.getFiles();
|
|
1221
|
+
if (!sourceResult.success || !sourceResult.data) {
|
|
1222
|
+
return 0;
|
|
1223
|
+
}
|
|
1224
|
+
const sourceFiles = sourceResult.data;
|
|
1225
|
+
const targetFiles = targetResult.data || [];
|
|
1226
|
+
for (const sourceFile of sourceFiles) {
|
|
1227
|
+
const targetFile = targetFiles.find(
|
|
1228
|
+
(f) => f.type === sourceFile.type && f.filename === sourceFile.filename
|
|
1229
|
+
);
|
|
1230
|
+
if (!targetFile) {
|
|
1231
|
+
errors.push({
|
|
1232
|
+
type: "missing_file",
|
|
1233
|
+
path: `files[${sourceFile.type}/${sourceFile.filename}]`,
|
|
1234
|
+
expected: sourceFile,
|
|
1235
|
+
actual: null,
|
|
1236
|
+
message: `File ${sourceFile.filename} (${sourceFile.type}) is missing in target`
|
|
1237
|
+
});
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
const sourceContent = sourceFile.content.trim();
|
|
1241
|
+
const targetContent = targetFile.content.trim();
|
|
1242
|
+
if (sourceContent !== targetContent) {
|
|
1243
|
+
errors.push({
|
|
1244
|
+
type: "content_mismatch",
|
|
1245
|
+
path: `files[${sourceFile.type}/${sourceFile.filename}].content`,
|
|
1246
|
+
expected: `${sourceContent.length} chars`,
|
|
1247
|
+
actual: `${targetContent.length} chars`,
|
|
1248
|
+
message: `File ${sourceFile.filename} content mismatch`
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return sourceFiles.length;
|
|
1253
|
+
}
|
|
1254
|
+
async validateTimeline(source, target, errors, warnings) {
|
|
1255
|
+
const sourceResult = await source.getTimelineEvents();
|
|
1256
|
+
const targetResult = await target.getTimelineEvents();
|
|
1257
|
+
if (!sourceResult.success || !sourceResult.data) {
|
|
1258
|
+
return 0;
|
|
1259
|
+
}
|
|
1260
|
+
const sourceEvents = sourceResult.data;
|
|
1261
|
+
const targetEvents = targetResult.data || [];
|
|
1262
|
+
for (const sourceEvent of sourceEvents) {
|
|
1263
|
+
const targetEvent = targetEvents.find(
|
|
1264
|
+
(e) => e.type === sourceEvent.type && e.timestamp === sourceEvent.timestamp
|
|
1265
|
+
);
|
|
1266
|
+
if (!targetEvent) {
|
|
1267
|
+
warnings.push(
|
|
1268
|
+
`Timeline event ${sourceEvent.type} at ${sourceEvent.timestamp} not found in target`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return sourceEvents.length;
|
|
1273
|
+
}
|
|
1274
|
+
async validateEvidence(source, target, errors) {
|
|
1275
|
+
const sourceResult = await source.getEvidence();
|
|
1276
|
+
const targetResult = await target.getEvidence();
|
|
1277
|
+
if (!sourceResult.success || !sourceResult.data) {
|
|
1278
|
+
return 0;
|
|
1279
|
+
}
|
|
1280
|
+
const sourceEvidence = sourceResult.data;
|
|
1281
|
+
const targetEvidence = targetResult.data || [];
|
|
1282
|
+
for (const sourceItem of sourceEvidence) {
|
|
1283
|
+
const targetItem = targetEvidence.find((e) => e.description === sourceItem.description);
|
|
1284
|
+
if (!targetItem) {
|
|
1285
|
+
errors.push({
|
|
1286
|
+
type: "missing_file",
|
|
1287
|
+
path: `evidence[${sourceItem.id}]`,
|
|
1288
|
+
expected: sourceItem,
|
|
1289
|
+
actual: null,
|
|
1290
|
+
message: `Evidence "${sourceItem.description}" is missing in target`
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return sourceEvidence.length;
|
|
1295
|
+
}
|
|
1296
|
+
async validateFeedback(source, target, errors) {
|
|
1297
|
+
const sourceResult = await source.getFeedback();
|
|
1298
|
+
const targetResult = await target.getFeedback();
|
|
1299
|
+
if (!sourceResult.success || !sourceResult.data) {
|
|
1300
|
+
return 0;
|
|
1301
|
+
}
|
|
1302
|
+
const sourceFeedback = sourceResult.data;
|
|
1303
|
+
const targetFeedback = targetResult.data || [];
|
|
1304
|
+
for (const sourceItem of sourceFeedback) {
|
|
1305
|
+
const targetItem = targetFeedback.find((f) => f.content === sourceItem.content);
|
|
1306
|
+
if (!targetItem) {
|
|
1307
|
+
errors.push({
|
|
1308
|
+
type: "missing_file",
|
|
1309
|
+
path: `feedback[${sourceItem.id}]`,
|
|
1310
|
+
expected: sourceItem,
|
|
1311
|
+
actual: null,
|
|
1312
|
+
message: `Feedback "${sourceItem.title || sourceItem.id}" is missing in target`
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return sourceFeedback.length;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function createValidator() {
|
|
1320
|
+
return new MigrationValidator();
|
|
1321
|
+
}
|
|
1322
|
+
class PlanMigrator {
|
|
1323
|
+
validator;
|
|
1324
|
+
constructor() {
|
|
1325
|
+
this.validator = new MigrationValidator();
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Migrate a plan from source to target format
|
|
1329
|
+
*
|
|
1330
|
+
* @param sourcePath - Path to source plan
|
|
1331
|
+
* @param targetPath - Path for target plan
|
|
1332
|
+
* @param sourceProvider - Provider for reading source
|
|
1333
|
+
* @param targetProvider - Provider for writing target
|
|
1334
|
+
* @param options - Migration options
|
|
1335
|
+
*/
|
|
1336
|
+
async migrate(sourcePath, targetPath, sourceProvider, targetProvider, options = {}) {
|
|
1337
|
+
const startTime = Date.now();
|
|
1338
|
+
const warnings = [];
|
|
1339
|
+
const stats = {
|
|
1340
|
+
stepsConverted: 0,
|
|
1341
|
+
filesConverted: 0,
|
|
1342
|
+
timelineEventsConverted: 0,
|
|
1343
|
+
evidenceConverted: 0,
|
|
1344
|
+
feedbackConverted: 0,
|
|
1345
|
+
checkpointsConverted: 0
|
|
1346
|
+
};
|
|
1347
|
+
try {
|
|
1348
|
+
const sourceFormat = sourceProvider.format;
|
|
1349
|
+
const targetFormat = targetProvider.format;
|
|
1350
|
+
const targetExists = await targetProvider.exists();
|
|
1351
|
+
if (targetExists && !options.force) {
|
|
1352
|
+
return {
|
|
1353
|
+
success: false,
|
|
1354
|
+
sourceFormat,
|
|
1355
|
+
targetFormat,
|
|
1356
|
+
sourcePath,
|
|
1357
|
+
targetPath,
|
|
1358
|
+
error: `Target already exists: ${targetPath}. Use force option to overwrite.`,
|
|
1359
|
+
warnings,
|
|
1360
|
+
stats,
|
|
1361
|
+
duration: Date.now() - startTime
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
this.reportProgress(options.onProgress, {
|
|
1365
|
+
phase: "reading",
|
|
1366
|
+
percentage: 0
|
|
1367
|
+
});
|
|
1368
|
+
const metadataResult = await sourceProvider.getMetadata();
|
|
1369
|
+
if (!metadataResult.success || !metadataResult.data) {
|
|
1370
|
+
return {
|
|
1371
|
+
success: false,
|
|
1372
|
+
sourceFormat,
|
|
1373
|
+
targetFormat,
|
|
1374
|
+
sourcePath,
|
|
1375
|
+
targetPath,
|
|
1376
|
+
error: `Failed to read source metadata: ${metadataResult.error}`,
|
|
1377
|
+
warnings,
|
|
1378
|
+
stats,
|
|
1379
|
+
duration: Date.now() - startTime
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
this.reportProgress(options.onProgress, {
|
|
1383
|
+
phase: "writing",
|
|
1384
|
+
percentage: 10,
|
|
1385
|
+
currentItem: "metadata"
|
|
1386
|
+
});
|
|
1387
|
+
const initResult = await targetProvider.initialize(metadataResult.data);
|
|
1388
|
+
if (!initResult.success) {
|
|
1389
|
+
return {
|
|
1390
|
+
success: false,
|
|
1391
|
+
sourceFormat,
|
|
1392
|
+
targetFormat,
|
|
1393
|
+
sourcePath,
|
|
1394
|
+
targetPath,
|
|
1395
|
+
error: `Failed to initialize target: ${initResult.error}`,
|
|
1396
|
+
warnings,
|
|
1397
|
+
stats,
|
|
1398
|
+
duration: Date.now() - startTime
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
this.reportProgress(options.onProgress, {
|
|
1402
|
+
phase: "converting",
|
|
1403
|
+
percentage: 20,
|
|
1404
|
+
currentItem: "steps"
|
|
1405
|
+
});
|
|
1406
|
+
stats.stepsConverted = await this.migrateSteps(sourceProvider, targetProvider);
|
|
1407
|
+
this.reportProgress(options.onProgress, {
|
|
1408
|
+
phase: "converting",
|
|
1409
|
+
percentage: 40,
|
|
1410
|
+
currentItem: "files"
|
|
1411
|
+
});
|
|
1412
|
+
stats.filesConverted = await this.migrateFiles(sourceProvider, targetProvider);
|
|
1413
|
+
this.reportProgress(options.onProgress, {
|
|
1414
|
+
phase: "converting",
|
|
1415
|
+
percentage: 60,
|
|
1416
|
+
currentItem: "timeline"
|
|
1417
|
+
});
|
|
1418
|
+
stats.timelineEventsConverted = await this.migrateTimeline(sourceProvider, targetProvider);
|
|
1419
|
+
this.reportProgress(options.onProgress, {
|
|
1420
|
+
phase: "converting",
|
|
1421
|
+
percentage: 70,
|
|
1422
|
+
currentItem: "evidence"
|
|
1423
|
+
});
|
|
1424
|
+
stats.evidenceConverted = await this.migrateEvidence(sourceProvider, targetProvider);
|
|
1425
|
+
this.reportProgress(options.onProgress, {
|
|
1426
|
+
phase: "converting",
|
|
1427
|
+
percentage: 80,
|
|
1428
|
+
currentItem: "feedback"
|
|
1429
|
+
});
|
|
1430
|
+
stats.feedbackConverted = await this.migrateFeedback(sourceProvider, targetProvider);
|
|
1431
|
+
this.reportProgress(options.onProgress, {
|
|
1432
|
+
phase: "converting",
|
|
1433
|
+
percentage: 90,
|
|
1434
|
+
currentItem: "checkpoints"
|
|
1435
|
+
});
|
|
1436
|
+
stats.checkpointsConverted = await this.migrateCheckpoints(sourceProvider, targetProvider);
|
|
1437
|
+
if (options.validate) {
|
|
1438
|
+
this.reportProgress(options.onProgress, {
|
|
1439
|
+
phase: "validating",
|
|
1440
|
+
percentage: 95
|
|
1441
|
+
});
|
|
1442
|
+
const validationResult = await this.validator.validate(sourceProvider, targetProvider);
|
|
1443
|
+
if (!validationResult.valid) {
|
|
1444
|
+
const errorMessages = validationResult.errors.map((e) => e.message).join("; ");
|
|
1445
|
+
return {
|
|
1446
|
+
success: false,
|
|
1447
|
+
sourceFormat,
|
|
1448
|
+
targetFormat,
|
|
1449
|
+
sourcePath,
|
|
1450
|
+
targetPath,
|
|
1451
|
+
error: `Validation failed: ${errorMessages}`,
|
|
1452
|
+
warnings: [...warnings, ...validationResult.warnings],
|
|
1453
|
+
stats,
|
|
1454
|
+
duration: Date.now() - startTime
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
warnings.push(...validationResult.warnings);
|
|
1458
|
+
}
|
|
1459
|
+
if (!options.keepSource) {
|
|
1460
|
+
await this.deleteSource(sourcePath, sourceFormat);
|
|
1461
|
+
}
|
|
1462
|
+
this.reportProgress(options.onProgress, {
|
|
1463
|
+
phase: "writing",
|
|
1464
|
+
percentage: 100
|
|
1465
|
+
});
|
|
1466
|
+
return {
|
|
1467
|
+
success: true,
|
|
1468
|
+
sourceFormat,
|
|
1469
|
+
targetFormat,
|
|
1470
|
+
sourcePath,
|
|
1471
|
+
targetPath,
|
|
1472
|
+
warnings,
|
|
1473
|
+
stats,
|
|
1474
|
+
duration: Date.now() - startTime
|
|
1475
|
+
};
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
return {
|
|
1478
|
+
success: false,
|
|
1479
|
+
sourceFormat: sourceProvider.format,
|
|
1480
|
+
targetFormat: targetProvider.format,
|
|
1481
|
+
sourcePath,
|
|
1482
|
+
targetPath,
|
|
1483
|
+
error: error instanceof Error ? error.message : "Unknown error during migration",
|
|
1484
|
+
warnings,
|
|
1485
|
+
stats,
|
|
1486
|
+
duration: Date.now() - startTime
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
async migrateSteps(source, target) {
|
|
1491
|
+
const result = await source.getSteps();
|
|
1492
|
+
if (!result.success || !result.data) {
|
|
1493
|
+
return 0;
|
|
1494
|
+
}
|
|
1495
|
+
for (const step of result.data) {
|
|
1496
|
+
await target.addStep(step);
|
|
1497
|
+
}
|
|
1498
|
+
return result.data.length;
|
|
1499
|
+
}
|
|
1500
|
+
async migrateFiles(source, target) {
|
|
1501
|
+
const result = await source.getFiles();
|
|
1502
|
+
if (!result.success || !result.data) {
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1505
|
+
for (const file of result.data) {
|
|
1506
|
+
await target.saveFile(file);
|
|
1507
|
+
}
|
|
1508
|
+
return result.data.length;
|
|
1509
|
+
}
|
|
1510
|
+
async migrateTimeline(source, target) {
|
|
1511
|
+
const result = await source.getTimelineEvents();
|
|
1512
|
+
if (!result.success || !result.data) {
|
|
1513
|
+
return 0;
|
|
1514
|
+
}
|
|
1515
|
+
for (const event of result.data) {
|
|
1516
|
+
await target.addTimelineEvent(event);
|
|
1517
|
+
}
|
|
1518
|
+
return result.data.length;
|
|
1519
|
+
}
|
|
1520
|
+
async migrateEvidence(source, target) {
|
|
1521
|
+
const result = await source.getEvidence();
|
|
1522
|
+
if (!result.success || !result.data) {
|
|
1523
|
+
return 0;
|
|
1524
|
+
}
|
|
1525
|
+
for (const evidence of result.data) {
|
|
1526
|
+
await target.addEvidence(evidence);
|
|
1527
|
+
}
|
|
1528
|
+
return result.data.length;
|
|
1529
|
+
}
|
|
1530
|
+
async migrateFeedback(source, target) {
|
|
1531
|
+
const result = await source.getFeedback();
|
|
1532
|
+
if (!result.success || !result.data) {
|
|
1533
|
+
return 0;
|
|
1534
|
+
}
|
|
1535
|
+
for (const feedback of result.data) {
|
|
1536
|
+
await target.addFeedback(feedback);
|
|
1537
|
+
}
|
|
1538
|
+
return result.data.length;
|
|
1539
|
+
}
|
|
1540
|
+
async migrateCheckpoints(source, target) {
|
|
1541
|
+
const result = await source.getCheckpoints();
|
|
1542
|
+
if (!result.success || !result.data) {
|
|
1543
|
+
return 0;
|
|
1544
|
+
}
|
|
1545
|
+
for (const checkpoint of result.data) {
|
|
1546
|
+
await target.createCheckpoint(checkpoint);
|
|
1547
|
+
}
|
|
1548
|
+
return result.data.length;
|
|
1549
|
+
}
|
|
1550
|
+
async deleteSource(sourcePath, format) {
|
|
1551
|
+
if (!existsSync(sourcePath)) {
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
if (format === "sqlite") {
|
|
1555
|
+
unlinkSync(sourcePath);
|
|
1556
|
+
const walPath = sourcePath + "-wal";
|
|
1557
|
+
const shmPath = sourcePath + "-shm";
|
|
1558
|
+
if (existsSync(walPath)) unlinkSync(walPath);
|
|
1559
|
+
if (existsSync(shmPath)) unlinkSync(shmPath);
|
|
1560
|
+
} else {
|
|
1561
|
+
rmSync(sourcePath, { recursive: true, force: true });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
reportProgress(callback, progress) {
|
|
1565
|
+
if (callback) {
|
|
1566
|
+
callback(progress);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function createMigrator() {
|
|
1571
|
+
return new PlanMigrator();
|
|
1572
|
+
}
|
|
1573
|
+
function generateTargetPath(sourcePath, sourceFormat, targetFormat) {
|
|
1574
|
+
if (targetFormat === "sqlite") {
|
|
1575
|
+
const basePath = sourcePath.replace(/\/$/, "");
|
|
1576
|
+
return ensureFormatExtension(basePath, "sqlite");
|
|
1577
|
+
} else {
|
|
1578
|
+
const basePath = sourcePath.replace(/\.plan$/, "");
|
|
1579
|
+
return basePath + "_migrated";
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function inferTargetFormat(sourceFormat) {
|
|
1583
|
+
return sourceFormat === "sqlite" ? "directory" : "sqlite";
|
|
1584
|
+
}
|
|
1585
|
+
async function renderPlanToMarkdown(provider, options = {}) {
|
|
1586
|
+
const result = {
|
|
1587
|
+
files: /* @__PURE__ */ new Map(),
|
|
1588
|
+
steps: /* @__PURE__ */ new Map(),
|
|
1589
|
+
evidence: /* @__PURE__ */ new Map(),
|
|
1590
|
+
feedback: /* @__PURE__ */ new Map()
|
|
1591
|
+
};
|
|
1592
|
+
const metadataResult = await provider.getMetadata();
|
|
1593
|
+
if (metadataResult.success && metadataResult.data) {
|
|
1594
|
+
result.files.set("SUMMARY.md", renderSummary(metadataResult.data, options));
|
|
1595
|
+
result.files.set("STATUS.md", await renderStatus(provider, metadataResult.data));
|
|
1596
|
+
}
|
|
1597
|
+
const filesResult = await provider.getFiles();
|
|
1598
|
+
if (filesResult.success && filesResult.data) {
|
|
1599
|
+
for (const file of filesResult.data) {
|
|
1600
|
+
result.files.set(file.filename, file.content);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const stepsResult = await provider.getSteps();
|
|
1604
|
+
if (stepsResult.success && stepsResult.data) {
|
|
1605
|
+
for (const step of stepsResult.data) {
|
|
1606
|
+
const filename = formatStepFilename(step);
|
|
1607
|
+
result.steps.set(filename, step.content);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if (options.includeEvidence !== false) {
|
|
1611
|
+
const evidenceResult = await provider.getEvidence();
|
|
1612
|
+
if (evidenceResult.success && evidenceResult.data) {
|
|
1613
|
+
for (const evidence of evidenceResult.data) {
|
|
1614
|
+
const filename = `${evidence.id}.md`;
|
|
1615
|
+
result.evidence.set(filename, renderEvidence(evidence));
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (options.includeFeedback !== false) {
|
|
1620
|
+
const feedbackResult = await provider.getFeedback();
|
|
1621
|
+
if (feedbackResult.success && feedbackResult.data) {
|
|
1622
|
+
for (const feedback of feedbackResult.data) {
|
|
1623
|
+
const filename = `${feedback.id}.md`;
|
|
1624
|
+
result.feedback.set(filename, renderFeedback(feedback));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return result;
|
|
1629
|
+
}
|
|
1630
|
+
function renderSummary(metadata, options) {
|
|
1631
|
+
const lines = [
|
|
1632
|
+
`# ${metadata.name}`,
|
|
1633
|
+
"",
|
|
1634
|
+
"## Overview",
|
|
1635
|
+
"",
|
|
1636
|
+
metadata.description || "_No description provided._",
|
|
1637
|
+
"",
|
|
1638
|
+
"## Metadata",
|
|
1639
|
+
"",
|
|
1640
|
+
`- **ID**: ${metadata.id}`,
|
|
1641
|
+
`- **Stage**: ${metadata.stage}`,
|
|
1642
|
+
`- **Created**: ${metadata.createdAt}`,
|
|
1643
|
+
`- **Updated**: ${metadata.updatedAt}`
|
|
1644
|
+
];
|
|
1645
|
+
if (options.includeSourceInfo) {
|
|
1646
|
+
lines.push(`- **Schema Version**: ${metadata.schemaVersion}`);
|
|
1647
|
+
}
|
|
1648
|
+
lines.push("", "---", "", `*Generated: ${(/* @__PURE__ */ new Date()).toISOString()}*`);
|
|
1649
|
+
return lines.join("\n");
|
|
1650
|
+
}
|
|
1651
|
+
async function renderStatus(provider, metadata) {
|
|
1652
|
+
const stepsResult = await provider.getSteps();
|
|
1653
|
+
const steps = stepsResult.data || [];
|
|
1654
|
+
const completed = steps.filter((s) => s.status === "completed").length;
|
|
1655
|
+
const inProgress = steps.filter((s) => s.status === "in_progress").length;
|
|
1656
|
+
const pending = steps.filter((s) => s.status === "pending").length;
|
|
1657
|
+
const total = steps.length;
|
|
1658
|
+
const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
1659
|
+
const statusEmoji = getStatusEmoji(metadata.stage, inProgress > 0);
|
|
1660
|
+
const lines = [
|
|
1661
|
+
`# ${metadata.name} Status`,
|
|
1662
|
+
"",
|
|
1663
|
+
"## Current State",
|
|
1664
|
+
"",
|
|
1665
|
+
"| Field | Value |",
|
|
1666
|
+
"|-------|-------|",
|
|
1667
|
+
`| **Status** | ${statusEmoji} ${metadata.stage.toUpperCase()} |`,
|
|
1668
|
+
`| **Progress** | ${percentage}% (${completed}/${total} steps) |`,
|
|
1669
|
+
`| **In Progress** | ${inProgress} |`,
|
|
1670
|
+
`| **Pending** | ${pending} |`,
|
|
1671
|
+
`| **Last Updated** | ${metadata.updatedAt.split("T")[0]} |`,
|
|
1672
|
+
"",
|
|
1673
|
+
"## Step Progress",
|
|
1674
|
+
"",
|
|
1675
|
+
"| Step | Name | Status | Started | Completed |",
|
|
1676
|
+
"|------|------|--------|---------|-----------|"
|
|
1677
|
+
];
|
|
1678
|
+
for (const step of steps) {
|
|
1679
|
+
const statusIcon = getStepStatusIcon(step.status);
|
|
1680
|
+
lines.push(
|
|
1681
|
+
`| ${String(step.number).padStart(2, "0")} | ${step.title} | ${statusIcon} | ${step.startedAt?.split("T")[0] || "-"} | ${step.completedAt?.split("T")[0] || "-"} |`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
lines.push("", "---", "", `*Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}*`);
|
|
1685
|
+
return lines.join("\n");
|
|
1686
|
+
}
|
|
1687
|
+
function formatStepFilename(step) {
|
|
1688
|
+
const num = String(step.number).padStart(2, "0");
|
|
1689
|
+
const code = step.code || step.title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
1690
|
+
return `${num}-${code}.md`;
|
|
1691
|
+
}
|
|
1692
|
+
function renderEvidence(evidence) {
|
|
1693
|
+
const lines = [
|
|
1694
|
+
"---",
|
|
1695
|
+
`id: ${evidence.id}`,
|
|
1696
|
+
`date: ${evidence.createdAt}`
|
|
1697
|
+
];
|
|
1698
|
+
if (evidence.source) {
|
|
1699
|
+
lines.push(`source: ${evidence.source}`);
|
|
1700
|
+
}
|
|
1701
|
+
if (evidence.sourceUrl) {
|
|
1702
|
+
lines.push(`url: ${evidence.sourceUrl}`);
|
|
1703
|
+
}
|
|
1704
|
+
if (evidence.gatheringMethod) {
|
|
1705
|
+
lines.push(`gathering_method: ${evidence.gatheringMethod}`);
|
|
1706
|
+
}
|
|
1707
|
+
lines.push("---", "", `# ${evidence.description}`, "");
|
|
1708
|
+
if (evidence.content) {
|
|
1709
|
+
lines.push(evidence.content);
|
|
1710
|
+
}
|
|
1711
|
+
return lines.join("\n");
|
|
1712
|
+
}
|
|
1713
|
+
function renderFeedback(feedback) {
|
|
1714
|
+
const lines = [
|
|
1715
|
+
"---",
|
|
1716
|
+
`id: ${feedback.id}`,
|
|
1717
|
+
`date: ${feedback.createdAt}`
|
|
1718
|
+
];
|
|
1719
|
+
if (feedback.title) {
|
|
1720
|
+
lines.push(`title: ${feedback.title}`);
|
|
1721
|
+
}
|
|
1722
|
+
if (feedback.platform) {
|
|
1723
|
+
lines.push(`platform: ${feedback.platform}`);
|
|
1724
|
+
}
|
|
1725
|
+
if (feedback.participants && feedback.participants.length > 0) {
|
|
1726
|
+
lines.push(`participants: [${feedback.participants.join(", ")}]`);
|
|
1727
|
+
}
|
|
1728
|
+
lines.push("---", "");
|
|
1729
|
+
if (feedback.title) {
|
|
1730
|
+
lines.push(`# ${feedback.title}`, "");
|
|
1731
|
+
}
|
|
1732
|
+
lines.push(feedback.content);
|
|
1733
|
+
return lines.join("\n");
|
|
1734
|
+
}
|
|
1735
|
+
function getStatusEmoji(stage, hasInProgress) {
|
|
1736
|
+
if (stage === "completed") return "✅";
|
|
1737
|
+
if (stage === "cancelled") return "❌";
|
|
1738
|
+
if (hasInProgress) return "🔄";
|
|
1739
|
+
if (stage === "executing") return "🔄";
|
|
1740
|
+
if (stage === "built") return "📋";
|
|
1741
|
+
if (stage === "shaping") return "🔧";
|
|
1742
|
+
return "⬜";
|
|
1743
|
+
}
|
|
1744
|
+
function getStepStatusIcon(status) {
|
|
1745
|
+
switch (status) {
|
|
1746
|
+
case "completed":
|
|
1747
|
+
return "✅";
|
|
1748
|
+
case "in_progress":
|
|
1749
|
+
return "🔄";
|
|
1750
|
+
case "skipped":
|
|
1751
|
+
return "⏭️";
|
|
1752
|
+
default:
|
|
1753
|
+
return "⬜";
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
const VERSION = "1.0.0-dev.0";
|
|
1757
|
+
const SCHEMA_VERSION = 1;
|
|
1758
|
+
const PLAN_FILE_EXTENSION = ".plan";
|
|
1759
|
+
export {
|
|
1760
|
+
DEFAULT_DIRECTORY_CONFIG,
|
|
1761
|
+
DEFAULT_FORMAT_CONFIG,
|
|
1762
|
+
DEFAULT_SQLITE_CONFIG,
|
|
1763
|
+
DefaultStorageProviderFactory,
|
|
1764
|
+
DirectoryStorageProvider,
|
|
1765
|
+
MigrationValidator,
|
|
1766
|
+
PLAN_FILE_EXTENSION,
|
|
1767
|
+
PlanMigrator,
|
|
1768
|
+
SCHEMA_VERSION,
|
|
1769
|
+
SqliteStorageProvider,
|
|
1770
|
+
VERSION,
|
|
1771
|
+
createDirectoryProvider,
|
|
1772
|
+
createMigrator,
|
|
1773
|
+
createProvider,
|
|
1774
|
+
createSqliteProvider,
|
|
1775
|
+
createStorageFactory,
|
|
1776
|
+
createValidator,
|
|
1777
|
+
detectPlanFormat,
|
|
1778
|
+
ensureFormatExtension,
|
|
1779
|
+
generateTargetPath,
|
|
1780
|
+
getFormatExtension,
|
|
1781
|
+
getPlanNameFromPath,
|
|
1782
|
+
hasSqliteHeader,
|
|
1783
|
+
inferFormatFromPath,
|
|
1784
|
+
inferTargetFormat,
|
|
1785
|
+
isDirectoryPath,
|
|
1786
|
+
isSqlitePath,
|
|
1787
|
+
mergeFormatConfig,
|
|
1788
|
+
renderPlanToMarkdown,
|
|
1789
|
+
validatePlanPath
|
|
1790
|
+
};
|
|
1791
|
+
//# sourceMappingURL=index.js.map
|