@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/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