aifastdb 2.2.1 → 2.2.6

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.
@@ -89,13 +89,40 @@ function sectionImportance(section) {
89
89
  }
90
90
  /**
91
91
  * 获取默认的 DevPlan 存储基础路径
92
+ *
93
+ * 优先级:
94
+ * 1. AIFASTDB_DEVPLAN_PATH 环境变量(显式指定)
95
+ * 2. 项目内 .devplan/ 目录(天然跟随 Git 版本管理)
96
+ * 3. 回退到用户目录 ~/.aifastdb/dev-plans/(兜底)
92
97
  */
93
98
  function getDefaultBasePath() {
94
99
  if (process.env.AIFASTDB_DEVPLAN_PATH) {
95
100
  return process.env.AIFASTDB_DEVPLAN_PATH;
96
101
  }
102
+ // 尝试定位项目根目录(查找 .git 或 package.json 所在目录)
103
+ const projectRoot = findProjectRoot();
104
+ if (projectRoot) {
105
+ return path.join(projectRoot, '.devplan');
106
+ }
107
+ // 兜底:用户目录
97
108
  return path.join(os.homedir(), '.aifastdb', 'dev-plans');
98
109
  }
110
+ /**
111
+ * 从当前工作目录向上查找项目根目录
112
+ * 通过 .git 目录或 package.json 文件来判断
113
+ */
114
+ function findProjectRoot() {
115
+ const fs = require('fs');
116
+ let dir = process.cwd();
117
+ const root = path.parse(dir).root;
118
+ while (dir !== root) {
119
+ if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'package.json'))) {
120
+ return dir;
121
+ }
122
+ dir = path.dirname(dir);
123
+ }
124
+ return null;
125
+ }
99
126
  // ============================================================================
100
127
  // DevPlanStore Implementation
101
128
  // ============================================================================
@@ -111,6 +138,7 @@ class DevPlanStore {
111
138
  this.projectName = projectName;
112
139
  this.docStore = new native_1.EnhancedDocumentStore(config.documentPath, (0, document_store_1.documentStoreProductionConfig)());
113
140
  this.taskStore = new native_1.EnhancedDocumentStore(config.taskPath, (0, document_store_1.documentStoreProductionConfig)());
141
+ this.moduleStore = new native_1.EnhancedDocumentStore(config.modulePath, (0, document_store_1.documentStoreProductionConfig)());
114
142
  }
115
143
  // ==========================================================================
116
144
  // Document Section Operations
@@ -128,15 +156,20 @@ class DevPlanStore {
128
156
  }
129
157
  const version = input.version || '1.0.0';
130
158
  const now = Date.now();
159
+ const finalModuleId = input.moduleId || existing?.moduleId;
160
+ const tags = [
161
+ `plan:${this.projectName}`,
162
+ `section:${input.section}`,
163
+ ...(input.subSection ? [`sub:${input.subSection}`] : []),
164
+ `ver:${version}`,
165
+ ];
166
+ if (finalModuleId) {
167
+ tags.push(`module:${finalModuleId}`);
168
+ }
131
169
  const docInput = {
132
170
  content: input.content,
133
171
  contentType: native_2.ContentType.Text,
134
- tags: [
135
- `plan:${this.projectName}`,
136
- `section:${input.section}`,
137
- ...(input.subSection ? [`sub:${input.subSection}`] : []),
138
- `ver:${version}`,
139
- ],
172
+ tags,
140
173
  metadata: {
141
174
  projectName: this.projectName,
142
175
  section: input.section,
@@ -144,6 +177,7 @@ class DevPlanStore {
144
177
  version,
145
178
  subSection: input.subSection || null,
146
179
  relatedSections: input.relatedSections || [],
180
+ moduleId: finalModuleId || null,
147
181
  createdAt: existing?.createdAt || now,
148
182
  updatedAt: now,
149
183
  },
@@ -172,8 +206,8 @@ class DevPlanStore {
172
206
  }
173
207
  if (filtered.length === 0)
174
208
  return null;
175
- // 返回最新版本
176
- const latest = filtered.sort((a, b) => b.createdAt - a.createdAt)[0];
209
+ // 返回最新版本(以 metadata.updatedAt 为判定依据)
210
+ const latest = filtered.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
177
211
  return this.docToDevPlanDoc(latest);
178
212
  }
179
213
  /**
@@ -184,14 +218,14 @@ class DevPlanStore {
184
218
  listSections() {
185
219
  const planTag = `plan:${this.projectName}`;
186
220
  const docs = this.docStore.findByTag(planTag);
187
- // 按 section+subSection 去重,保留最新版本
221
+ // 按 section+subSection 去重,保留最新版本(以 metadata.updatedAt 判定)
188
222
  const latestMap = new Map();
189
223
  for (const doc of docs) {
190
224
  const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
191
225
  const subTag = doc.tags.find((t) => t.startsWith('sub:'));
192
226
  const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
193
227
  const existing = latestMap.get(key);
194
- if (!existing || doc.createdAt > existing.createdAt) {
228
+ if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
195
229
  latestMap.set(key, doc);
196
230
  }
197
231
  }
@@ -223,14 +257,14 @@ class DevPlanStore {
223
257
  searchSections(query, limit = 10) {
224
258
  const planTag = `plan:${this.projectName}`;
225
259
  const allDocs = this.docStore.findByTag(planTag);
226
- // 按 section+subSection 去重,保留最新版本
260
+ // 按 section+subSection 去重,保留最新版本(以 metadata.updatedAt 判定)
227
261
  const latestMap = new Map();
228
262
  for (const doc of allDocs) {
229
263
  const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
230
264
  const subTag = doc.tags.find((t) => t.startsWith('sub:'));
231
265
  const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
232
266
  const existing = latestMap.get(key);
233
- if (!existing || doc.createdAt > existing.createdAt) {
267
+ if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
234
268
  latestMap.set(key, doc);
235
269
  }
236
270
  }
@@ -275,20 +309,25 @@ class DevPlanStore {
275
309
  totalSubtasks: 0,
276
310
  completedSubtasks: 0,
277
311
  };
312
+ const tags = [
313
+ `plan:${this.projectName}`,
314
+ 'type:main-task',
315
+ `mtask:${input.taskId}`,
316
+ `priority:${input.priority}`,
317
+ 'status:pending',
318
+ ];
319
+ if (input.moduleId) {
320
+ tags.push(`module:${input.moduleId}`);
321
+ }
278
322
  const docInput = {
279
323
  content: JSON.stringify(taskData),
280
324
  contentType: native_2.ContentType.Text,
281
- tags: [
282
- `plan:${this.projectName}`,
283
- 'type:main-task',
284
- `mtask:${input.taskId}`,
285
- `priority:${input.priority}`,
286
- 'status:pending',
287
- ],
325
+ tags,
288
326
  metadata: {
289
327
  projectName: this.projectName,
290
328
  taskId: input.taskId,
291
329
  status: 'pending',
330
+ moduleId: input.moduleId || null,
292
331
  createdAt: now,
293
332
  updatedAt: now,
294
333
  completedAt: null,
@@ -301,17 +340,111 @@ class DevPlanStore {
301
340
  id,
302
341
  projectName: this.projectName,
303
342
  ...taskData,
343
+ moduleId: input.moduleId,
304
344
  status: 'pending',
305
345
  createdAt: now,
306
346
  updatedAt: now,
307
347
  completedAt: null,
308
348
  };
309
349
  }
350
+ /**
351
+ * 幂等导入主任务(Upsert)
352
+ *
353
+ * - 如果主任务不存在 → 创建新任务
354
+ * - 如果主任务已存在 → 更新标题/描述/优先级等字段,但保留已有的更高级状态
355
+ * (例如已完成的任务不会被重置为 pending)
356
+ * - updatedAt 保证严格递增,不会与历史版本重复
357
+ *
358
+ * @param input 主任务输入
359
+ * @param options.preserveStatus 若为 true(默认),则不覆盖已完成的状态
360
+ * @param options.status 导入时的目标状态(默认 pending)
361
+ * @returns 创建或更新后的主任务
362
+ */
363
+ upsertMainTask(input, options) {
364
+ const preserveStatus = options?.preserveStatus !== false; // 默认 true
365
+ const targetStatus = options?.status || 'pending';
366
+ const existing = this.getMainTask(input.taskId);
367
+ if (!existing) {
368
+ // 新建
369
+ const task = this.createMainTask(input);
370
+ // 如果目标状态不是 pending,更新状态
371
+ if (targetStatus !== 'pending') {
372
+ return this.updateMainTaskStatus(task.taskId, targetStatus) || task;
373
+ }
374
+ return task;
375
+ }
376
+ // 已存在 — 决定最终状态
377
+ let finalStatus = targetStatus;
378
+ if (preserveStatus) {
379
+ // 状态优先级: completed > in_progress > pending > cancelled
380
+ const statusPriority = {
381
+ cancelled: 0,
382
+ pending: 1,
383
+ in_progress: 2,
384
+ completed: 3,
385
+ };
386
+ if (statusPriority[existing.status] >= statusPriority[targetStatus]) {
387
+ finalStatus = existing.status; // 保留更高级状态
388
+ }
389
+ }
390
+ // 删除旧版本并确保时间戳递增
391
+ this.deleteAndEnsureTimestampAdvance(this.taskStore, existing.id);
392
+ const now = Date.now();
393
+ const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
394
+ const finalModuleId = input.moduleId || existing.moduleId;
395
+ const taskData = {
396
+ taskId: input.taskId,
397
+ title: input.title,
398
+ priority: input.priority,
399
+ description: input.description || existing.description || '',
400
+ estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
401
+ relatedSections: input.relatedSections || existing.relatedSections || [],
402
+ totalSubtasks: existing.totalSubtasks,
403
+ completedSubtasks: existing.completedSubtasks,
404
+ };
405
+ const tags = [
406
+ `plan:${this.projectName}`,
407
+ 'type:main-task',
408
+ `mtask:${input.taskId}`,
409
+ `priority:${input.priority}`,
410
+ `status:${finalStatus}`,
411
+ ];
412
+ if (finalModuleId) {
413
+ tags.push(`module:${finalModuleId}`);
414
+ }
415
+ const docInput = {
416
+ content: JSON.stringify(taskData),
417
+ contentType: native_2.ContentType.Text,
418
+ tags,
419
+ metadata: {
420
+ projectName: this.projectName,
421
+ taskId: input.taskId,
422
+ status: finalStatus,
423
+ moduleId: finalModuleId || null,
424
+ createdAt: existing.createdAt,
425
+ updatedAt: now,
426
+ completedAt,
427
+ },
428
+ importance: input.priority === 'P0' ? 0.95 : input.priority === 'P1' ? 0.8 : 0.6,
429
+ };
430
+ const id = this.taskStore.put(docInput);
431
+ this.taskStore.flush();
432
+ return {
433
+ ...taskData,
434
+ id,
435
+ projectName: this.projectName,
436
+ moduleId: finalModuleId,
437
+ status: finalStatus,
438
+ createdAt: existing.createdAt,
439
+ updatedAt: now,
440
+ completedAt,
441
+ };
442
+ }
310
443
  /**
311
444
  * 获取主任务
312
445
  *
313
446
  * 由于 JSONL append-only 存储会保留历史版本,
314
- * 需要按 created_at 降序取最新版本。
447
+ * 需要按 metadata.updatedAt 降序取最新版本。
315
448
  */
316
449
  getMainTask(taskId) {
317
450
  const tag = `mtask:${taskId}`;
@@ -319,8 +452,8 @@ class DevPlanStore {
319
452
  .filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
320
453
  if (docs.length === 0)
321
454
  return null;
322
- // 取最新版本(created_at 最大的)
323
- const latest = docs.sort((a, b) => b.createdAt - a.createdAt)[0];
455
+ // 取最新版本(以 metadata.updatedAt 为判定依据)
456
+ const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
324
457
  return this.docToMainTask(latest);
325
458
  }
326
459
  /**
@@ -341,6 +474,10 @@ class DevPlanStore {
341
474
  const priorityTag = `priority:${filter.priority}`;
342
475
  docs = docs.filter((doc) => doc.tags.includes(priorityTag));
343
476
  }
477
+ if (filter?.moduleId) {
478
+ const moduleTag = `module:${filter.moduleId}`;
479
+ docs = docs.filter((doc) => doc.tags.includes(moduleTag));
480
+ }
344
481
  return docs.map((doc) => this.docToMainTask(doc));
345
482
  }
346
483
  /**
@@ -363,20 +500,25 @@ class DevPlanStore {
363
500
  totalSubtasks: mainTask.totalSubtasks,
364
501
  completedSubtasks: mainTask.completedSubtasks,
365
502
  };
503
+ const tags = [
504
+ `plan:${this.projectName}`,
505
+ 'type:main-task',
506
+ `mtask:${mainTask.taskId}`,
507
+ `priority:${mainTask.priority}`,
508
+ `status:${status}`,
509
+ ];
510
+ if (mainTask.moduleId) {
511
+ tags.push(`module:${mainTask.moduleId}`);
512
+ }
366
513
  const docInput = {
367
514
  content: JSON.stringify(taskData),
368
515
  contentType: native_2.ContentType.Text,
369
- tags: [
370
- `plan:${this.projectName}`,
371
- 'type:main-task',
372
- `mtask:${mainTask.taskId}`,
373
- `priority:${mainTask.priority}`,
374
- `status:${status}`,
375
- ],
516
+ tags,
376
517
  metadata: {
377
518
  projectName: this.projectName,
378
519
  taskId: mainTask.taskId,
379
520
  status,
521
+ moduleId: mainTask.moduleId || null,
380
522
  createdAt: mainTask.createdAt,
381
523
  updatedAt: now,
382
524
  completedAt,
@@ -455,10 +597,146 @@ class DevPlanStore {
455
597
  completedAt: null,
456
598
  };
457
599
  }
600
+ /**
601
+ * 幂等导入子任务(Upsert)
602
+ *
603
+ * - 如果子任务不存在 → 创建新子任务
604
+ * - 如果子任务已存在 → 更新标题/描述等字段,但保留已有的更高级状态
605
+ * (例如已完成的任务不会被重置为 pending)
606
+ * - updatedAt 保证严格递增,不会与历史版本重复
607
+ *
608
+ * @param input 子任务输入
609
+ * @param options.preserveStatus 若为 true(默认),则不覆盖已完成的状态
610
+ * @param options.status 导入时的目标状态(默认 pending)
611
+ * @returns 创建或更新后的子任务
612
+ */
613
+ upsertSubTask(input, options) {
614
+ const preserveStatus = options?.preserveStatus !== false; // 默认 true
615
+ const targetStatus = options?.status || 'pending';
616
+ const existing = this.getSubTask(input.taskId);
617
+ if (!existing) {
618
+ // 新建(验证父任务存在)
619
+ const mainTask = this.getMainTask(input.parentTaskId);
620
+ if (!mainTask) {
621
+ throw new Error(`Parent main task "${input.parentTaskId}" not found for project "${this.projectName}"`);
622
+ }
623
+ const now = Date.now();
624
+ const taskData = {
625
+ taskId: input.taskId,
626
+ title: input.title,
627
+ estimatedHours: input.estimatedHours || 0,
628
+ relatedFiles: input.relatedFiles || [],
629
+ description: input.description || '',
630
+ };
631
+ const docInput = {
632
+ content: JSON.stringify(taskData),
633
+ contentType: native_2.ContentType.Text,
634
+ parentId: mainTask.id,
635
+ tags: [
636
+ `plan:${this.projectName}`,
637
+ 'type:sub-task',
638
+ `stask:${input.taskId}`,
639
+ `parent:${input.parentTaskId}`,
640
+ `status:${targetStatus}`,
641
+ ],
642
+ metadata: {
643
+ projectName: this.projectName,
644
+ taskId: input.taskId,
645
+ parentTaskId: input.parentTaskId,
646
+ status: targetStatus,
647
+ createdAt: now,
648
+ updatedAt: now,
649
+ completedAt: targetStatus === 'completed' ? now : null,
650
+ },
651
+ importance: 0.7,
652
+ };
653
+ const id = this.taskStore.put(docInput);
654
+ this.refreshMainTaskCounts(input.parentTaskId);
655
+ this.taskStore.flush();
656
+ return {
657
+ id,
658
+ projectName: this.projectName,
659
+ ...taskData,
660
+ parentTaskId: input.parentTaskId,
661
+ status: targetStatus,
662
+ createdAt: now,
663
+ updatedAt: now,
664
+ completedAt: targetStatus === 'completed' ? now : null,
665
+ };
666
+ }
667
+ // 已存在 — 决定最终状态
668
+ let finalStatus = targetStatus;
669
+ if (preserveStatus) {
670
+ const statusPriority = {
671
+ cancelled: 0,
672
+ pending: 1,
673
+ in_progress: 2,
674
+ completed: 3,
675
+ };
676
+ if (statusPriority[existing.status] >= statusPriority[targetStatus]) {
677
+ finalStatus = existing.status;
678
+ }
679
+ }
680
+ // 检查是否有实质性变化(避免无意义的更新,减少历史版本膨胀)
681
+ if (existing.title === input.title &&
682
+ existing.description === (input.description || '') &&
683
+ existing.status === finalStatus &&
684
+ existing.estimatedHours === (input.estimatedHours || 0)) {
685
+ // 无变化,直接返回
686
+ return existing;
687
+ }
688
+ // 删除旧版本并确保时间戳递增
689
+ this.deleteAndEnsureTimestampAdvance(this.taskStore, existing.id);
690
+ const mainTask = this.getMainTask(input.parentTaskId);
691
+ const now = Date.now();
692
+ const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
693
+ const taskData = {
694
+ taskId: input.taskId,
695
+ title: input.title,
696
+ estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
697
+ relatedFiles: input.relatedFiles || existing.relatedFiles || [],
698
+ description: input.description || existing.description || '',
699
+ };
700
+ const docInput = {
701
+ content: JSON.stringify(taskData),
702
+ contentType: native_2.ContentType.Text,
703
+ parentId: mainTask?.id || undefined,
704
+ tags: [
705
+ `plan:${this.projectName}`,
706
+ 'type:sub-task',
707
+ `stask:${input.taskId}`,
708
+ `parent:${input.parentTaskId}`,
709
+ `status:${finalStatus}`,
710
+ ],
711
+ metadata: {
712
+ projectName: this.projectName,
713
+ taskId: input.taskId,
714
+ parentTaskId: input.parentTaskId,
715
+ status: finalStatus,
716
+ createdAt: existing.createdAt,
717
+ updatedAt: now,
718
+ completedAt,
719
+ completedAtCommit: existing.completedAtCommit || null,
720
+ revertReason: existing.revertReason || null,
721
+ },
722
+ importance: 0.7,
723
+ };
724
+ const id = this.taskStore.put(docInput);
725
+ this.refreshMainTaskCounts(input.parentTaskId);
726
+ this.taskStore.flush();
727
+ return {
728
+ ...existing,
729
+ ...taskData,
730
+ id,
731
+ status: finalStatus,
732
+ updatedAt: now,
733
+ completedAt,
734
+ };
735
+ }
458
736
  /**
459
737
  * 获取子任务
460
738
  *
461
- * 取同一 taskId 的最新版本。
739
+ * 取同一 taskId 的最新版本(以 metadata.updatedAt 判定)。
462
740
  */
463
741
  getSubTask(taskId) {
464
742
  const tag = `stask:${taskId}`;
@@ -466,8 +744,8 @@ class DevPlanStore {
466
744
  .filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
467
745
  if (docs.length === 0)
468
746
  return null;
469
- // 取最新版本
470
- const latest = docs.sort((a, b) => b.createdAt - a.createdAt)[0];
747
+ // 取最新版本(以 metadata.updatedAt 为判定依据)
748
+ const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
471
749
  return this.docToSubTask(latest);
472
750
  }
473
751
  /**
@@ -490,8 +768,11 @@ class DevPlanStore {
490
768
  }
491
769
  /**
492
770
  * 更新子任务状态
771
+ *
772
+ * @param options.completedAtCommit - 完成时的 Git commit hash(仅 status=completed 时有效)
773
+ * @param options.revertReason - 回退原因(仅 status 从 completed 变为 pending 时有效)
493
774
  */
494
- updateSubTaskStatus(taskId, status) {
775
+ updateSubTaskStatus(taskId, status, options) {
495
776
  const subTask = this.getSubTask(taskId);
496
777
  if (!subTask)
497
778
  return null;
@@ -499,7 +780,11 @@ class DevPlanStore {
499
780
  const mainTask = this.getMainTask(subTask.parentTaskId);
500
781
  this.deleteAndEnsureTimestampAdvance(this.taskStore, subTask.id);
501
782
  const now = Date.now();
502
- const completedAt = status === 'completed' ? now : subTask.completedAt;
783
+ const completedAt = status === 'completed' ? now : (status === 'pending' ? null : subTask.completedAt);
784
+ const completedAtCommit = status === 'completed'
785
+ ? (options?.completedAtCommit || subTask.completedAtCommit)
786
+ : (status === 'pending' ? undefined : subTask.completedAtCommit);
787
+ const revertReason = options?.revertReason || (status === 'pending' ? undefined : subTask.revertReason);
503
788
  const taskData = {
504
789
  taskId: subTask.taskId,
505
790
  title: subTask.title,
@@ -526,6 +811,8 @@ class DevPlanStore {
526
811
  createdAt: subTask.createdAt,
527
812
  updatedAt: now,
528
813
  completedAt,
814
+ completedAtCommit: completedAtCommit || null,
815
+ revertReason: revertReason || null,
529
816
  },
530
817
  importance: 0.7,
531
818
  };
@@ -537,6 +824,8 @@ class DevPlanStore {
537
824
  status,
538
825
  updatedAt: now,
539
826
  completedAt,
827
+ completedAtCommit,
828
+ revertReason,
540
829
  };
541
830
  }
542
831
  // ==========================================================================
@@ -546,34 +835,40 @@ class DevPlanStore {
546
835
  * 完成子任务 — 核心自动化方法
547
836
  *
548
837
  * 自动处理:
549
- * 1. 更新子任务状态为 completed,写入 completedAt 时间戳
550
- * 2. 重新计算主任务的 completedSubtasks 计数
551
- * 3. 如果全部子任务完成,自动标记主任务为 completed
552
- * 4. 如果主任务完成,更新 milestones 文档
838
+ * 1. 获取当前 Git HEAD 的 short SHA 用于锚定
839
+ * 2. 更新子任务状态为 completed,写入 completedAt 时间戳和 completedAtCommit
840
+ * 3. 重新计算主任务的 completedSubtasks 计数
841
+ * 4. 如果全部子任务完成,自动标记主任务为 completed
842
+ * 5. 如果主任务完成,更新 milestones 文档
553
843
  */
554
844
  completeSubTask(taskId) {
555
- // 1. 更新子任务
556
- const updatedSubTask = this.updateSubTaskStatus(taskId, 'completed');
845
+ // 1. 获取当前 Git commit hash
846
+ const commitHash = this.getCurrentGitCommit();
847
+ // 2. 更新子任务(带 Git commit 锚定)
848
+ const updatedSubTask = this.updateSubTaskStatus(taskId, 'completed', {
849
+ completedAtCommit: commitHash,
850
+ });
557
851
  if (!updatedSubTask) {
558
852
  throw new Error(`Sub task "${taskId}" not found for project "${this.projectName}"`);
559
853
  }
560
- // 2. 刷新主任务计数
854
+ // 3. 刷新主任务计数
561
855
  const updatedMainTask = this.refreshMainTaskCounts(updatedSubTask.parentTaskId);
562
856
  if (!updatedMainTask) {
563
857
  throw new Error(`Parent main task "${updatedSubTask.parentTaskId}" not found`);
564
858
  }
565
- // 3. 检查主任务是否全部完成
859
+ // 4. 检查主任务是否全部完成
566
860
  const mainTaskCompleted = updatedMainTask.totalSubtasks > 0 &&
567
861
  updatedMainTask.completedSubtasks >= updatedMainTask.totalSubtasks;
568
862
  if (mainTaskCompleted && updatedMainTask.status !== 'completed') {
569
863
  const completedMain = this.updateMainTaskStatus(updatedSubTask.parentTaskId, 'completed');
570
864
  if (completedMain) {
571
- // 4. 更新 milestones 文档(如果存在)
865
+ // 5. 更新 milestones 文档(如果存在)
572
866
  this.autoUpdateMilestones(completedMain);
573
867
  return {
574
868
  subTask: updatedSubTask,
575
869
  mainTask: completedMain,
576
870
  mainTaskCompleted: true,
871
+ completedAtCommit: commitHash,
577
872
  };
578
873
  }
579
874
  }
@@ -581,6 +876,7 @@ class DevPlanStore {
581
876
  subTask: updatedSubTask,
582
877
  mainTask: updatedMainTask,
583
878
  mainTaskCompleted,
879
+ completedAtCommit: commitHash,
584
880
  };
585
881
  }
586
882
  /**
@@ -701,6 +997,196 @@ class DevPlanStore {
701
997
  return md;
702
998
  }
703
999
  // ==========================================================================
1000
+ // Module Operations
1001
+ // ==========================================================================
1002
+ /**
1003
+ * 创建功能模块
1004
+ */
1005
+ createModule(input) {
1006
+ const existing = this.getModule(input.moduleId);
1007
+ if (existing) {
1008
+ throw new Error(`Module "${input.moduleId}" already exists for project "${this.projectName}"`);
1009
+ }
1010
+ const now = Date.now();
1011
+ const status = input.status || 'active';
1012
+ const moduleData = {
1013
+ moduleId: input.moduleId,
1014
+ name: input.name,
1015
+ description: input.description || '',
1016
+ };
1017
+ const docInput = {
1018
+ content: JSON.stringify(moduleData),
1019
+ contentType: native_2.ContentType.Text,
1020
+ tags: [
1021
+ `plan:${this.projectName}`,
1022
+ 'type:module',
1023
+ `module:${input.moduleId}`,
1024
+ `status:${status}`,
1025
+ ],
1026
+ metadata: {
1027
+ projectName: this.projectName,
1028
+ moduleId: input.moduleId,
1029
+ status,
1030
+ createdAt: now,
1031
+ updatedAt: now,
1032
+ },
1033
+ importance: 0.85,
1034
+ };
1035
+ const id = this.moduleStore.put(docInput);
1036
+ this.moduleStore.flush();
1037
+ return {
1038
+ id,
1039
+ projectName: this.projectName,
1040
+ moduleId: input.moduleId,
1041
+ name: input.name,
1042
+ description: input.description,
1043
+ status,
1044
+ mainTaskCount: 0,
1045
+ subTaskCount: 0,
1046
+ completedSubTaskCount: 0,
1047
+ docCount: 0,
1048
+ createdAt: now,
1049
+ updatedAt: now,
1050
+ };
1051
+ }
1052
+ /**
1053
+ * 获取功能模块(含自动计算的 taskCount/docCount)
1054
+ */
1055
+ getModule(moduleId) {
1056
+ const tag = `module:${moduleId}`;
1057
+ const docs = this.moduleStore.findByTag(tag)
1058
+ .filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
1059
+ if (docs.length === 0)
1060
+ return null;
1061
+ const latest = docs.sort((a, b) => this.getDocUpdatedAt(b) - this.getDocUpdatedAt(a))[0];
1062
+ return this.docToModule(latest);
1063
+ }
1064
+ /**
1065
+ * 列出所有功能模块
1066
+ */
1067
+ listModules(filter) {
1068
+ let docs = this.moduleStore.findByTag(`plan:${this.projectName}`)
1069
+ .filter((doc) => doc.tags.includes('type:module'));
1070
+ // 按 moduleId 去重
1071
+ const latestMap = new Map();
1072
+ for (const doc of docs) {
1073
+ const data = JSON.parse(doc.content);
1074
+ const moduleId = data.moduleId;
1075
+ if (!moduleId)
1076
+ continue;
1077
+ const existing = latestMap.get(moduleId);
1078
+ if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
1079
+ latestMap.set(moduleId, doc);
1080
+ }
1081
+ }
1082
+ docs = Array.from(latestMap.values());
1083
+ if (filter?.status) {
1084
+ const statusTag = `status:${filter.status}`;
1085
+ docs = docs.filter((doc) => doc.tags.includes(statusTag));
1086
+ }
1087
+ return docs.map((doc) => this.docToModule(doc));
1088
+ }
1089
+ /**
1090
+ * 更新功能模块
1091
+ */
1092
+ updateModule(moduleId, updates) {
1093
+ const existing = this.getModule(moduleId);
1094
+ if (!existing)
1095
+ return null;
1096
+ this.deleteAndEnsureTimestampAdvance(this.moduleStore, existing.id);
1097
+ const now = Date.now();
1098
+ const newName = updates.name || existing.name;
1099
+ const newDescription = updates.description !== undefined ? updates.description : existing.description;
1100
+ const newStatus = updates.status || existing.status;
1101
+ const moduleData = {
1102
+ moduleId,
1103
+ name: newName,
1104
+ description: newDescription || '',
1105
+ };
1106
+ const docInput = {
1107
+ content: JSON.stringify(moduleData),
1108
+ contentType: native_2.ContentType.Text,
1109
+ tags: [
1110
+ `plan:${this.projectName}`,
1111
+ 'type:module',
1112
+ `module:${moduleId}`,
1113
+ `status:${newStatus}`,
1114
+ ],
1115
+ metadata: {
1116
+ projectName: this.projectName,
1117
+ moduleId,
1118
+ status: newStatus,
1119
+ createdAt: existing.createdAt,
1120
+ updatedAt: now,
1121
+ },
1122
+ importance: 0.85,
1123
+ };
1124
+ const id = this.moduleStore.put(docInput);
1125
+ this.moduleStore.flush();
1126
+ return {
1127
+ id,
1128
+ projectName: this.projectName,
1129
+ moduleId,
1130
+ name: newName,
1131
+ description: newDescription,
1132
+ status: newStatus,
1133
+ mainTaskCount: existing.mainTaskCount,
1134
+ subTaskCount: existing.subTaskCount,
1135
+ completedSubTaskCount: existing.completedSubTaskCount,
1136
+ docCount: existing.docCount,
1137
+ createdAt: existing.createdAt,
1138
+ updatedAt: now,
1139
+ };
1140
+ }
1141
+ /**
1142
+ * 删除功能模块
1143
+ */
1144
+ deleteModule(moduleId) {
1145
+ const existing = this.getModule(moduleId);
1146
+ if (!existing)
1147
+ return false;
1148
+ this.moduleStore.delete(existing.id);
1149
+ this.moduleStore.flush();
1150
+ return true;
1151
+ }
1152
+ /**
1153
+ * 获取模块详情 — 包含关联的任务和文档
1154
+ */
1155
+ getModuleDetail(moduleId) {
1156
+ const mod = this.getModule(moduleId);
1157
+ if (!mod)
1158
+ return null;
1159
+ // 获取关联的主任务
1160
+ const moduleTag = `module:${moduleId}`;
1161
+ let taskDocs = this.taskStore.findByTag(moduleTag)
1162
+ .filter((doc) => doc.tags.includes(`plan:${this.projectName}`) &&
1163
+ doc.tags.includes('type:main-task'));
1164
+ taskDocs = this.deduplicateByTaskId(taskDocs);
1165
+ const mainTasks = taskDocs.map((doc) => this.docToMainTask(doc));
1166
+ // 获取关联的所有子任务(通过主任务间接关联)
1167
+ const subTasks = [];
1168
+ for (const mt of mainTasks) {
1169
+ const subs = this.listSubTasks(mt.taskId);
1170
+ subTasks.push(...subs);
1171
+ }
1172
+ // 获取关联的文档
1173
+ let docDocs = this.docStore.findByTag(moduleTag)
1174
+ .filter((doc) => doc.tags.includes(`plan:${this.projectName}`));
1175
+ // 按 section+subSection 去重
1176
+ const latestDocMap = new Map();
1177
+ for (const doc of docDocs) {
1178
+ const sectionTag = doc.tags.find((t) => t.startsWith('section:'));
1179
+ const subTag = doc.tags.find((t) => t.startsWith('sub:'));
1180
+ const key = `${sectionTag || 'unknown'}|${subTag || ''}`;
1181
+ const ex = latestDocMap.get(key);
1182
+ if (!ex || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(ex)) {
1183
+ latestDocMap.set(key, doc);
1184
+ }
1185
+ }
1186
+ const documents = Array.from(latestDocMap.values()).map((doc) => this.docToDevPlanDoc(doc));
1187
+ return { module: mod, mainTasks, subTasks, documents };
1188
+ }
1189
+ // ==========================================================================
704
1190
  // Utility
705
1191
  // ==========================================================================
706
1192
  /**
@@ -709,6 +1195,7 @@ class DevPlanStore {
709
1195
  sync() {
710
1196
  this.docStore.flush();
711
1197
  this.taskStore.flush();
1198
+ this.moduleStore.flush();
712
1199
  }
713
1200
  /**
714
1201
  * 获取项目名称
@@ -719,16 +1206,29 @@ class DevPlanStore {
719
1206
  // ==========================================================================
720
1207
  // Private Helpers
721
1208
  // ==========================================================================
1209
+ /**
1210
+ * 获取文档的有效 updatedAt 时间戳。
1211
+ *
1212
+ * EnhancedDocumentStore 使用 append-only JSONL 存储,修改文档时实际上
1213
+ * 是 delete 旧文档 + put 新文档。因此同一逻辑文档可能存在多个物理版本。
1214
+ * 必须通过 metadata.updatedAt 来判断哪个是最新的"可用文档",
1215
+ * 其余的都是"历史文档"。
1216
+ *
1217
+ * 优先级:metadata.updatedAt > metadata.createdAt > doc.createdAt
1218
+ */
1219
+ getDocUpdatedAt(doc) {
1220
+ return doc.metadata?.updatedAt || doc.metadata?.createdAt || doc.createdAt;
1221
+ }
722
1222
  /**
723
1223
  * 确保当前时间戳严格大于参考时间戳。
724
1224
  *
725
1225
  * EnhancedDocumentStore 使用 append-only JSONL 存储,保留所有历史版本。
726
- * deduplicateByTaskId 通过 MemoryNode.created_at 选择最新版本。
727
- * 如果 delete+put 发生在同一毫秒内,新旧版本的 created_at 相同,
1226
+ * 版本选择通过 metadata.updatedAt 判定最新文档。
1227
+ * 如果 delete+put 发生在同一毫秒内,新旧版本的 updatedAt 相同,
728
1228
  * 会导致去重时可能选中旧版本(如 pending 状态),造成状态丢失。
729
1229
  *
730
1230
  * 本方法在 delete 之后、put 之前调用,自旋等待直到时间戳前进,
731
- * 从而保证新版本的 created_at 一定大于旧版本。
1231
+ * 从而保证新版本的 updatedAt 一定大于旧版本。
732
1232
  */
733
1233
  ensureTimestampAfter(referenceTimestamp) {
734
1234
  while (Date.now() <= referenceTimestamp) {
@@ -736,21 +1236,28 @@ class DevPlanStore {
736
1236
  }
737
1237
  }
738
1238
  /**
739
- * 删除文档并确保后续 put 的时间戳严格大于被删文档。
740
- * 返回被删文档的 created_at,若文档不存在返回 0。
1239
+ * 删除文档并确保后续 put 的 updatedAt 严格大于被删文档。
1240
+ *
1241
+ * 使用 metadata.updatedAt 作为参考时间戳(而非 doc.createdAt),
1242
+ * 因为版本选择是基于 metadata.updatedAt 进行的。
741
1243
  */
742
1244
  deleteAndEnsureTimestampAdvance(store, id) {
743
1245
  const deleted = store.delete(id);
744
1246
  if (deleted) {
745
- this.ensureTimestampAfter(deleted.createdAt);
1247
+ // 以 metadata.updatedAt 为基准,确保新文档的 updatedAt 严格递增
1248
+ const refTimestamp = this.getDocUpdatedAt(deleted);
1249
+ this.ensureTimestampAfter(refTimestamp);
746
1250
  }
747
1251
  }
748
1252
  /**
749
- * 对同一 taskId 的多个历史版本做去重,仅保留最新版(created_at 最大)。
1253
+ * 对同一 taskId 的多个历史版本做去重,仅保留最新版(metadata.updatedAt 最大)。
750
1254
  *
751
1255
  * 由于 EnhancedDocumentStore 使用 append-only JSONL 存储,
752
1256
  * delete+put 操作会在文件中保留历史版本。重新加载时所有版本都会出现,
753
1257
  * 因此需要在查询层面进行去重。
1258
+ *
1259
+ * 使用 metadata.updatedAt(而非 doc.createdAt)作为版本判定依据,
1260
+ * 确保"最近更新时间"的文档才是可用文档,其余为历史文档。
754
1261
  */
755
1262
  deduplicateByTaskId(docs) {
756
1263
  const latestMap = new Map();
@@ -760,7 +1267,7 @@ class DevPlanStore {
760
1267
  if (!taskId)
761
1268
  continue;
762
1269
  const existing = latestMap.get(taskId);
763
- if (!existing || doc.createdAt > existing.createdAt) {
1270
+ if (!existing || this.getDocUpdatedAt(doc) > this.getDocUpdatedAt(existing)) {
764
1271
  latestMap.set(taskId, doc);
765
1272
  }
766
1273
  }
@@ -771,6 +1278,8 @@ class DevPlanStore {
771
1278
  const section = (sectionTag?.replace('section:', '') || 'custom');
772
1279
  const subTag = doc.tags.find((t) => t.startsWith('sub:'));
773
1280
  const subSection = subTag?.replace('sub:', '');
1281
+ const moduleTag = doc.tags.find((t) => t.startsWith('module:'));
1282
+ const moduleId = moduleTag?.replace('module:', '') || undefined;
774
1283
  return {
775
1284
  id: doc.id,
776
1285
  projectName: this.projectName,
@@ -780,6 +1289,7 @@ class DevPlanStore {
780
1289
  version: doc.metadata?.version || '1.0.0',
781
1290
  subSection,
782
1291
  relatedSections: doc.metadata?.relatedSections || [],
1292
+ moduleId,
783
1293
  createdAt: doc.metadata?.createdAt || doc.createdAt,
784
1294
  updatedAt: doc.metadata?.updatedAt || doc.createdAt,
785
1295
  };
@@ -788,6 +1298,8 @@ class DevPlanStore {
788
1298
  const data = JSON.parse(doc.content);
789
1299
  const statusTag = doc.tags.find((t) => t.startsWith('status:'));
790
1300
  const status = (statusTag?.replace('status:', '') || 'pending');
1301
+ const moduleTag = doc.tags.find((t) => t.startsWith('module:'));
1302
+ const moduleId = moduleTag?.replace('module:', '') || undefined;
791
1303
  return {
792
1304
  id: doc.id,
793
1305
  projectName: this.projectName,
@@ -797,6 +1309,7 @@ class DevPlanStore {
797
1309
  description: data.description,
798
1310
  estimatedHours: data.estimatedHours,
799
1311
  relatedSections: data.relatedSections || [],
1312
+ moduleId,
800
1313
  totalSubtasks: data.totalSubtasks || 0,
801
1314
  completedSubtasks: data.completedSubtasks || 0,
802
1315
  status,
@@ -805,6 +1318,71 @@ class DevPlanStore {
805
1318
  completedAt: doc.metadata?.completedAt || null,
806
1319
  };
807
1320
  }
1321
+ docToModule(doc) {
1322
+ const data = JSON.parse(doc.content);
1323
+ const statusTag = doc.tags.find((t) => t.startsWith('status:'));
1324
+ const status = (statusTag?.replace('status:', '') || 'active');
1325
+ const moduleId = data.moduleId;
1326
+ // 计算关联的主任务数(去重)
1327
+ const moduleTag = `module:${moduleId}`;
1328
+ const taskDocs = this.taskStore.findByTag(moduleTag)
1329
+ .filter((d) => d.tags.includes(`plan:${this.projectName}`) &&
1330
+ d.tags.includes('type:main-task'));
1331
+ const uniqueTaskIds = new Set();
1332
+ for (const td of taskDocs) {
1333
+ try {
1334
+ uniqueTaskIds.add(JSON.parse(td.content).taskId);
1335
+ }
1336
+ catch { }
1337
+ }
1338
+ // 计算关联的子任务数(遍历关联主任务下的所有子任务)
1339
+ let subTaskCount = 0;
1340
+ let completedSubTaskCount = 0;
1341
+ for (const mainTaskId of uniqueTaskIds) {
1342
+ const subDocs = this.taskStore.findByTag(`parent:${mainTaskId}`)
1343
+ .filter((d) => d.tags.includes(`plan:${this.projectName}`) &&
1344
+ d.tags.includes('type:sub-task'));
1345
+ // 按 taskId 去重
1346
+ const seenSubIds = new Set();
1347
+ for (const sd of subDocs) {
1348
+ try {
1349
+ const subData = JSON.parse(sd.content);
1350
+ if (seenSubIds.has(subData.taskId))
1351
+ continue;
1352
+ seenSubIds.add(subData.taskId);
1353
+ subTaskCount++;
1354
+ const subStatusTag = sd.tags.find((t) => t.startsWith('status:'));
1355
+ if (subStatusTag === 'status:completed') {
1356
+ completedSubTaskCount++;
1357
+ }
1358
+ }
1359
+ catch { }
1360
+ }
1361
+ }
1362
+ // 计算关联的文档数(按 section+subSection 去重)
1363
+ const docDocs = this.docStore.findByTag(moduleTag)
1364
+ .filter((d) => d.tags.includes(`plan:${this.projectName}`));
1365
+ const uniqueDocKeys = new Set();
1366
+ for (const dd of docDocs) {
1367
+ const st = dd.tags.find((t) => t.startsWith('section:'));
1368
+ const sub = dd.tags.find((t) => t.startsWith('sub:'));
1369
+ uniqueDocKeys.add(`${st || ''}|${sub || ''}`);
1370
+ }
1371
+ return {
1372
+ id: doc.id,
1373
+ projectName: this.projectName,
1374
+ moduleId,
1375
+ name: data.name,
1376
+ description: data.description || undefined,
1377
+ status,
1378
+ mainTaskCount: uniqueTaskIds.size,
1379
+ subTaskCount,
1380
+ completedSubTaskCount,
1381
+ docCount: uniqueDocKeys.size,
1382
+ createdAt: doc.metadata?.createdAt || doc.createdAt,
1383
+ updatedAt: doc.metadata?.updatedAt || doc.createdAt,
1384
+ };
1385
+ }
808
1386
  docToSubTask(doc) {
809
1387
  const data = JSON.parse(doc.content);
810
1388
  const statusTag = doc.tags.find((t) => t.startsWith('status:'));
@@ -824,6 +1402,8 @@ class DevPlanStore {
824
1402
  createdAt: doc.metadata?.createdAt || doc.createdAt,
825
1403
  updatedAt: doc.metadata?.updatedAt || doc.createdAt,
826
1404
  completedAt: doc.metadata?.completedAt || null,
1405
+ completedAtCommit: doc.metadata?.completedAtCommit || undefined,
1406
+ revertReason: doc.metadata?.revertReason || undefined,
827
1407
  };
828
1408
  }
829
1409
  /**
@@ -851,20 +1431,25 @@ class DevPlanStore {
851
1431
  totalSubtasks: subs.length,
852
1432
  completedSubtasks: completedCount,
853
1433
  };
1434
+ const tags = [
1435
+ `plan:${this.projectName}`,
1436
+ 'type:main-task',
1437
+ `mtask:${mainTask.taskId}`,
1438
+ `priority:${mainTask.priority}`,
1439
+ `status:${mainTask.status}`,
1440
+ ];
1441
+ if (mainTask.moduleId) {
1442
+ tags.push(`module:${mainTask.moduleId}`);
1443
+ }
854
1444
  const docInput = {
855
1445
  content: JSON.stringify(taskData),
856
1446
  contentType: native_2.ContentType.Text,
857
- tags: [
858
- `plan:${this.projectName}`,
859
- 'type:main-task',
860
- `mtask:${mainTask.taskId}`,
861
- `priority:${mainTask.priority}`,
862
- `status:${mainTask.status}`,
863
- ],
1447
+ tags,
864
1448
  metadata: {
865
1449
  projectName: this.projectName,
866
1450
  taskId: mainTask.taskId,
867
1451
  status: mainTask.status,
1452
+ moduleId: mainTask.moduleId || null,
868
1453
  createdAt: mainTask.createdAt,
869
1454
  updatedAt: now,
870
1455
  completedAt: mainTask.completedAt,
@@ -901,6 +1486,118 @@ class DevPlanStore {
901
1486
  relatedSections: milestonesDoc.relatedSections,
902
1487
  });
903
1488
  }
1489
+ // ==========================================================================
1490
+ // Git Integration (Git Commit 锚定 + 同步检查)
1491
+ // ==========================================================================
1492
+ /**
1493
+ * 🆕 获取当前 Git HEAD 的 short SHA
1494
+ *
1495
+ * 在非 Git 仓库或 Git 不可用时返回 undefined,不阻断正常流程。
1496
+ */
1497
+ getCurrentGitCommit() {
1498
+ try {
1499
+ const { execSync } = require('child_process');
1500
+ return execSync('git rev-parse --short HEAD', {
1501
+ encoding: 'utf-8',
1502
+ timeout: 5000,
1503
+ stdio: ['pipe', 'pipe', 'pipe'], // 静默 stderr
1504
+ }).trim();
1505
+ }
1506
+ catch {
1507
+ return undefined; // 非 Git 仓库或 Git 不可用
1508
+ }
1509
+ }
1510
+ /**
1511
+ * 🆕 检查 commit 是否是 target 的祖先
1512
+ *
1513
+ * 使用 `git merge-base --is-ancestor` 命令。
1514
+ * 如果 commit 不存在或不可达,返回 false(视为需要回退)。
1515
+ */
1516
+ isAncestor(commit, target) {
1517
+ try {
1518
+ const { execSync } = require('child_process');
1519
+ execSync(`git merge-base --is-ancestor ${commit} ${target}`, {
1520
+ timeout: 5000,
1521
+ stdio: ['pipe', 'pipe', 'pipe'],
1522
+ });
1523
+ return true; // exit code 0 = is ancestor
1524
+ }
1525
+ catch {
1526
+ return false; // exit code 1 = not ancestor, or error
1527
+ }
1528
+ }
1529
+ /**
1530
+ * 🆕 回退子任务状态
1531
+ *
1532
+ * 将已完成的子任务回退为 pending,记录回退原因,
1533
+ * 清空 completedAtCommit 和 completedAt。
1534
+ * 同时刷新父主任务的计数。
1535
+ */
1536
+ revertSubTask(taskId, reason) {
1537
+ const result = this.updateSubTaskStatus(taskId, 'pending', {
1538
+ revertReason: reason,
1539
+ });
1540
+ if (result) {
1541
+ // 刷新父主任务计数
1542
+ this.refreshMainTaskCounts(result.parentTaskId);
1543
+ // 如果父主任务被标记为 completed,也需要回退
1544
+ const mainTask = this.getMainTask(result.parentTaskId);
1545
+ if (mainTask && mainTask.status === 'completed') {
1546
+ this.updateMainTaskStatus(result.parentTaskId, 'in_progress');
1547
+ }
1548
+ }
1549
+ return result;
1550
+ }
1551
+ /**
1552
+ * 🆕 同步检查所有已完成任务与 Git 历史的一致性
1553
+ *
1554
+ * 对每个 status=completed 且有 completedAtCommit 的子任务:
1555
+ * 1. 检查 completedAtCommit 是否是当前 HEAD 的祖先
1556
+ * 2. 如果不是(说明 Git 发生了回滚),回退任务状态为 pending
1557
+ * 3. 记录 revertReason
1558
+ *
1559
+ * @param dryRun 如果为 true,只返回哪些任务会被回退,不实际修改数据
1560
+ * @returns 同步结果,包含被回退的任务列表
1561
+ */
1562
+ syncWithGit(dryRun = false) {
1563
+ const currentHead = this.getCurrentGitCommit();
1564
+ if (!currentHead) {
1565
+ return {
1566
+ checked: 0,
1567
+ reverted: [],
1568
+ currentHead: 'unknown',
1569
+ error: 'Git not available or not in a Git repository',
1570
+ };
1571
+ }
1572
+ const mainTasks = this.listMainTasks();
1573
+ const reverted = [];
1574
+ let checked = 0;
1575
+ for (const mt of mainTasks) {
1576
+ const subs = this.listSubTasks(mt.taskId);
1577
+ for (const sub of subs) {
1578
+ if (sub.status !== 'completed' || !sub.completedAtCommit)
1579
+ continue;
1580
+ checked++;
1581
+ if (!this.isAncestor(sub.completedAtCommit, currentHead)) {
1582
+ const reason = `Commit ${sub.completedAtCommit} is not ancestor of HEAD ${currentHead}`;
1583
+ if (!dryRun) {
1584
+ this.revertSubTask(sub.taskId, reason);
1585
+ }
1586
+ reverted.push({
1587
+ taskId: sub.taskId,
1588
+ title: sub.title,
1589
+ parentTaskId: sub.parentTaskId,
1590
+ completedAtCommit: sub.completedAtCommit,
1591
+ reason: `Commit ${sub.completedAtCommit} not found in current branch (HEAD: ${currentHead})`,
1592
+ });
1593
+ }
1594
+ }
1595
+ }
1596
+ return { checked, reverted, currentHead };
1597
+ }
1598
+ // ==========================================================================
1599
+ // Utilities
1600
+ // ==========================================================================
904
1601
  /**
905
1602
  * 生成文本进度条
906
1603
  */
@@ -919,13 +1616,22 @@ exports.DevPlanStore = DevPlanStore;
919
1616
  * 为项目创建 DevPlanStore
920
1617
  *
921
1618
  * @param projectName - 项目名称
922
- * @param basePath - 存储基础路径(默认 ~/.aifastdb/dev-plans/)
1619
+ * @param basePath - 存储基础路径(默认优先使用项目内 .devplan/,回退到 ~/.aifastdb/dev-plans/)
1620
+ *
1621
+ * 存储路径解析优先级:
1622
+ * 1. 显式 basePath 参数
1623
+ * 2. AIFASTDB_DEVPLAN_PATH 环境变量
1624
+ * 3. 项目根目录/.devplan/(通过 .git 或 package.json 定位)
1625
+ * 4. ~/.aifastdb/dev-plans/(兜底)
1626
+ *
1627
+ * 最终路径:{basePath}/{projectName}/documents.jsonl + tasks.jsonl
923
1628
  */
924
1629
  function createDevPlan(projectName, basePath) {
925
1630
  const base = basePath || getDefaultBasePath();
926
1631
  return new DevPlanStore(projectName, {
927
1632
  documentPath: path.join(base, projectName, 'documents.jsonl'),
928
1633
  taskPath: path.join(base, projectName, 'tasks.jsonl'),
1634
+ modulePath: path.join(base, projectName, 'modules.jsonl'),
929
1635
  });
930
1636
  }
931
1637
  /**