aifastdb-devplan 1.6.1 → 1.6.3

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.
Files changed (97) hide show
  1. package/dist/dev-plan-document-store.d.ts +13 -1
  2. package/dist/dev-plan-document-store.d.ts.map +1 -1
  3. package/dist/dev-plan-document-store.js +119 -0
  4. package/dist/dev-plan-document-store.js.map +1 -1
  5. package/dist/dev-plan-factory.d.ts.map +1 -1
  6. package/dist/dev-plan-factory.js +3 -1
  7. package/dist/dev-plan-factory.js.map +1 -1
  8. package/dist/dev-plan-graph-store.d.ts +341 -9
  9. package/dist/dev-plan-graph-store.d.ts.map +1 -1
  10. package/dist/dev-plan-graph-store.js +2414 -210
  11. package/dist/dev-plan-graph-store.js.map +1 -1
  12. package/dist/dev-plan-interface.d.ts +119 -1
  13. package/dist/dev-plan-interface.d.ts.map +1 -1
  14. package/dist/dev-plan-migrate.d.ts +1 -0
  15. package/dist/dev-plan-migrate.d.ts.map +1 -1
  16. package/dist/dev-plan-migrate.js +28 -2
  17. package/dist/dev-plan-migrate.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp-server/index.js +652 -0
  22. package/dist/mcp-server/index.js.map +1 -1
  23. package/dist/shard-config.d.ts +64 -0
  24. package/dist/shard-config.d.ts.map +1 -0
  25. package/dist/shard-config.js +109 -0
  26. package/dist/shard-config.js.map +1 -0
  27. package/dist/types.d.ts +305 -2
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/types.js.map +1 -1
  30. package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -1
  31. package/dist/visualize/graph-canvas/api-compat.js +22 -12
  32. package/dist/visualize/graph-canvas/api-compat.js.map +1 -1
  33. package/dist/visualize/graph-canvas/core.d.ts.map +1 -1
  34. package/dist/visualize/graph-canvas/core.js +296 -4
  35. package/dist/visualize/graph-canvas/core.js.map +1 -1
  36. package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -1
  37. package/dist/visualize/graph-canvas/interaction.js +11 -0
  38. package/dist/visualize/graph-canvas/interaction.js.map +1 -1
  39. package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -1
  40. package/dist/visualize/graph-canvas/layout-worker.js +45 -9
  41. package/dist/visualize/graph-canvas/layout-worker.js.map +1 -1
  42. package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -1
  43. package/dist/visualize/graph-canvas/renderer.js +164 -33
  44. package/dist/visualize/graph-canvas/renderer.js.map +1 -1
  45. package/dist/visualize/graph-canvas/styles.d.ts.map +1 -1
  46. package/dist/visualize/graph-canvas/styles.js +146 -121
  47. package/dist/visualize/graph-canvas/styles.js.map +1 -1
  48. package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -1
  49. package/dist/visualize/graph-canvas/viewport.js +10 -0
  50. package/dist/visualize/graph-canvas/viewport.js.map +1 -1
  51. package/dist/visualize/server.js +371 -32
  52. package/dist/visualize/server.js.map +1 -1
  53. package/dist/visualize/template-core.d.ts +9 -0
  54. package/dist/visualize/template-core.d.ts.map +1 -0
  55. package/dist/visualize/template-core.js +721 -0
  56. package/dist/visualize/template-core.js.map +1 -0
  57. package/dist/visualize/template-data-loading.d.ts +7 -0
  58. package/dist/visualize/template-data-loading.d.ts.map +1 -0
  59. package/dist/visualize/template-data-loading.js +677 -0
  60. package/dist/visualize/template-data-loading.js.map +1 -0
  61. package/dist/visualize/template-detail-panel.d.ts +14 -0
  62. package/dist/visualize/template-detail-panel.d.ts.map +1 -0
  63. package/dist/visualize/template-detail-panel.js +624 -0
  64. package/dist/visualize/template-detail-panel.js.map +1 -0
  65. package/dist/visualize/template-graph-3d.d.ts +7 -0
  66. package/dist/visualize/template-graph-3d.d.ts.map +1 -0
  67. package/dist/visualize/template-graph-3d.js +1114 -0
  68. package/dist/visualize/template-graph-3d.js.map +1 -0
  69. package/dist/visualize/template-graph-vis.d.ts +8 -0
  70. package/dist/visualize/template-graph-vis.d.ts.map +1 -0
  71. package/dist/visualize/template-graph-vis.js +1215 -0
  72. package/dist/visualize/template-graph-vis.js.map +1 -0
  73. package/dist/visualize/template-html.d.ts +9 -0
  74. package/dist/visualize/template-html.d.ts.map +1 -0
  75. package/dist/visualize/template-html.js +635 -0
  76. package/dist/visualize/template-html.js.map +1 -0
  77. package/dist/visualize/template-md-viewer.d.ts +11 -0
  78. package/dist/visualize/template-md-viewer.d.ts.map +1 -0
  79. package/dist/visualize/template-md-viewer.js +806 -0
  80. package/dist/visualize/template-md-viewer.js.map +1 -0
  81. package/dist/visualize/template-pages.d.ts +7 -0
  82. package/dist/visualize/template-pages.d.ts.map +1 -0
  83. package/dist/visualize/template-pages.js +1892 -0
  84. package/dist/visualize/template-pages.js.map +1 -0
  85. package/dist/visualize/template-stats-modal.d.ts +7 -0
  86. package/dist/visualize/template-stats-modal.d.ts.map +1 -0
  87. package/dist/visualize/template-stats-modal.js +466 -0
  88. package/dist/visualize/template-stats-modal.js.map +1 -0
  89. package/dist/visualize/template-styles.d.ts +9 -0
  90. package/dist/visualize/template-styles.d.ts.map +1 -0
  91. package/dist/visualize/template-styles.js +623 -0
  92. package/dist/visualize/template-styles.js.map +1 -0
  93. package/dist/visualize/template.d.ts +15 -3
  94. package/dist/visualize/template.d.ts.map +1 -1
  95. package/dist/visualize/template.js +44 -3475
  96. package/dist/visualize/template.js.map +1 -1
  97. package/package.json +2 -2
@@ -53,6 +53,7 @@ exports.DevPlanGraphStore = void 0;
53
53
  const aifastdb_1 = require("aifastdb");
54
54
  const path = __importStar(require("path"));
55
55
  const fs = __importStar(require("fs"));
56
+ const shard_config_1 = require("./shard-config");
56
57
  // ============================================================================
57
58
  // Constants
58
59
  // ============================================================================
@@ -63,16 +64,32 @@ const ET = {
63
64
  MAIN_TASK: 'devplan-main-task',
64
65
  SUB_TASK: 'devplan-sub-task',
65
66
  MODULE: 'devplan-module',
67
+ PROMPT: 'devplan-prompt',
68
+ MEMORY: 'devplan-memory',
66
69
  };
67
70
  /** Relation 类型常量 */
68
71
  const RT = {
69
72
  HAS_DOCUMENT: 'has_document',
70
73
  HAS_MAIN_TASK: 'has_main_task',
71
74
  HAS_SUB_TASK: 'has_sub_task',
75
+ HAS_MODULE: 'has_module',
72
76
  MODULE_HAS_TASK: 'module_has_task',
73
77
  MODULE_HAS_DOC: 'module_has_doc',
74
78
  TASK_HAS_DOC: 'task_has_doc',
75
79
  DOC_HAS_CHILD: 'doc_has_child',
80
+ TASK_HAS_PROMPT: 'task_has_prompt',
81
+ HAS_PROMPT: 'has_prompt',
82
+ HAS_MEMORY: 'has_memory',
83
+ MEMORY_FROM_TASK: 'memory_from_task',
84
+ // ---- Phase-37: 记忆网络关系类型 ----
85
+ /** 记忆 ↔ 记忆 语义关联(双向,带 similarity score 权重) */
86
+ MEMORY_RELATES: 'memory_relates',
87
+ /** 文档 → 记忆 来源关系(从文档提取的记忆) */
88
+ MEMORY_FROM_DOC: 'memory_from_doc',
89
+ /** 模块 → 记忆 归属关系(模块级记忆) */
90
+ MODULE_MEMORY: 'module_memory',
91
+ /** 记忆 → 记忆 替代/演化关系(新记忆替代旧记忆) */
92
+ MEMORY_SUPERSEDES: 'memory_supersedes',
76
93
  };
77
94
  // ============================================================================
78
95
  // Helper
@@ -113,22 +130,15 @@ class DevPlanGraphStore {
113
130
  // 必须在 SocialGraphV2 构造之前执行,否则 recover() 会找不到旧数据
114
131
  DevPlanGraphStore.migrateWalDirNames(config.graphPath);
115
132
  // 构建 SocialGraphV2 配置
133
+ // shardCount 由 SocialGraphV2 自动从 shardNames.length 推导(aifastdb >= 2.7.0)
134
+ // 所有分片定义集中在 shard-config.ts(单一数据源)
116
135
  const graphConfig = {
117
136
  path: config.graphPath,
118
- shardCount: config.shardCount || 4,
119
137
  walEnabled: true,
120
138
  mode: 'balanced',
121
- shardNames: ['tasks', 'relations', 'docs', 'modules'],
122
- // 显式路由:DevPlan 实体类型 → 对应分片
123
- typeShardMapping: {
124
- 'devplan-project': 0, // 项目根节点 → tasks shard
125
- 'devplan-main-task': 0, // 主任务 → tasks shard
126
- 'devplan-sub-task': 0, // 子任务 → tasks shard
127
- 'devplan-doc': 2, // 文档片段 → docs shard
128
- 'devplan-module': 3, // 功能模块 → modules shard
129
- '_default': 0, // 未知类型 → tasks shard (fallback)
130
- },
131
- relationShardId: 1, // 所有关系 → relations shard
139
+ shardNames: shard_config_1.DEVPLAN_SHARD_NAMES,
140
+ typeShardMapping: shard_config_1.DEVPLAN_TYPE_SHARD_MAPPING,
141
+ relationShardId: shard_config_1.DEVPLAN_RELATION_SHARD_ID,
132
142
  };
133
143
  // 如果启用语义搜索,配置 SocialGraphV2 的向量搜索
134
144
  const dimension = config.embeddingDimension || 384;
@@ -200,21 +210,25 @@ class DevPlanGraphStore {
200
210
  // WAL Directory Migration (旧分片名 → 语义化新名)
201
211
  // ==========================================================================
202
212
  /**
203
- * 一次性迁移:将旧 WAL 目录名重命名为语义化新名。
213
+ * WAL 目录迁移 + 分片目录补全。
214
+ *
215
+ * 1. 旧名重命名(幂等):
216
+ * - shard_0_entities → shard_0_tasks
217
+ * - shard_2_index → shard_2_docs
218
+ * - shard_3_meta → shard_3_modules
204
219
  *
205
- * 映射表:
206
- * - shard_0_entities → shard_0_tasks
207
- * - shard_2_index → shard_2_docs
208
- * - shard_3_meta → shard_3_modules
209
- * - shard_1_relations 不变
220
+ * 2. 确保所有预期的分片目录存在(Phase-36: shard_5_memory):
221
+ * SocialGraphV2 (Rust) 根据磁盘上的目录数确定分片数量,
222
+ * 如果新增分片目录不存在会导致 index out of bounds panic。
223
+ * 因此必须在 SocialGraphV2 构造之前创建缺失的目录。
210
224
  *
211
225
  * 该方法必须在 `new SocialGraphV2(config)` 之前调用。
212
- * 幂等:如果旧目录不存在或新目录已存在则跳过。
213
226
  */
214
227
  static migrateWalDirNames(graphPath) {
215
228
  const walBase = path.join(graphPath, 'wal');
216
229
  if (!fs.existsSync(walBase))
217
230
  return; // 全新安装,无需迁移
231
+ // ── Step 1: 旧名 → 新名 重命名 ──
218
232
  const renames = [
219
233
  ['shard_0_entities', 'shard_0_tasks'],
220
234
  ['shard_2_index', 'shard_2_docs'],
@@ -233,23 +247,33 @@ class DevPlanGraphStore {
233
247
  }
234
248
  }
235
249
  }
250
+ // ── Step 2: 确保所有预期分片目录存在 ──
251
+ // 当新增分片时(如 shard_5_memory),旧项目缺少对应目录,
252
+ // 必须提前创建空目录,否则 Rust SocialGraphV2 会 panic。
253
+ // 分片列表从 shard-config.ts 自动推导,不再硬编码。
254
+ for (const shardName of shard_config_1.DEVPLAN_EXPECTED_SHARD_DIRS) {
255
+ const shardDir = path.join(walBase, shardName);
256
+ if (!fs.existsSync(shardDir)) {
257
+ try {
258
+ fs.mkdirSync(shardDir, { recursive: true });
259
+ console.error(`[DevPlan] Created missing shard dir: ${shardName}`);
260
+ }
261
+ catch (e) {
262
+ console.warn(`[DevPlan] Failed to create shard dir ${shardName}: ${e instanceof Error ? e.message : String(e)}`);
263
+ }
264
+ }
265
+ }
236
266
  }
237
267
  // ==========================================================================
238
268
  // Project Entity
239
269
  // ==========================================================================
240
270
  ensureProjectEntity() {
241
- const existing = this.findProjectEntity();
242
- if (existing) {
243
- this.projectEntityId = existing.id;
244
- }
245
- else {
246
- const entity = this.graph.addEntity(this.projectName, ET.PROJECT, {
247
- projectName: this.projectName,
248
- createdAt: Date.now(),
249
- });
250
- this.projectEntityId = entity.id;
251
- this.graph.flush();
252
- }
271
+ const entity = this.graph.upsertEntityByProp(ET.PROJECT, 'projectName', this.projectName, this.projectName, {
272
+ projectName: this.projectName,
273
+ createdAt: Date.now(),
274
+ });
275
+ this.projectEntityId = entity.id;
276
+ this.graph.flush();
253
277
  }
254
278
  findProjectEntity() {
255
279
  const entities = this.graph.listEntitiesByType(ET.PROJECT);
@@ -261,20 +285,104 @@ class DevPlanGraphStore {
261
285
  }
262
286
  return this.projectEntityId;
263
287
  }
264
- // ==========================================================================
265
- // Generic Entity Helpers
266
- // ==========================================================================
288
+ /**
289
+ * 通用 Entity 去重:按指定 property key 分组,每组只保留"最优"实体。
290
+ *
291
+ * 胜出规则:
292
+ * 1. status 优先级高者胜(completed > in_progress > pending > cancelled)
293
+ * 2. 同 status 时 updatedAt 最新者胜
294
+ *
295
+ * 适用于 mainTask(按 taskId 去重)、subTask(按 taskId 去重)、
296
+ * module(按 moduleId 去重)等场景。
297
+ */
298
+ deduplicateEntities(entities, propKey) {
299
+ const bestMap = new Map();
300
+ for (const e of entities) {
301
+ const key = e.properties?.[propKey];
302
+ if (!key) {
303
+ // 无 key 的 entity 直接保留
304
+ bestMap.set(e.id, e);
305
+ continue;
306
+ }
307
+ const existing = bestMap.get(key);
308
+ if (!existing) {
309
+ bestMap.set(key, e);
310
+ continue;
311
+ }
312
+ // 比较状态优先级
313
+ const existStatus = DevPlanGraphStore.STATUS_PRIORITY[existing.properties?.status] ?? 1;
314
+ const newStatus = DevPlanGraphStore.STATUS_PRIORITY[e.properties?.status] ?? 1;
315
+ if (newStatus > existStatus) {
316
+ bestMap.set(key, e);
317
+ }
318
+ else if (newStatus === existStatus) {
319
+ // 同状态:updatedAt 更大者胜
320
+ const existUpdated = Number(existing.properties?.updatedAt) || 0;
321
+ const newUpdated = Number(e.properties?.updatedAt) || 0;
322
+ if (newUpdated > existUpdated) {
323
+ bestMap.set(key, e);
324
+ }
325
+ }
326
+ }
327
+ return Array.from(bestMap.values());
328
+ }
329
+ /**
330
+ * 查找所有重复 Entity(按指定 property key 分组,返回非胜出者列表)。
331
+ * 用于清理 WAL 中的冗余记录。
332
+ */
333
+ findDuplicateEntities(entityType, propKey) {
334
+ const entities = this.findEntitiesByType(entityType);
335
+ const groups = new Map();
336
+ for (const e of entities) {
337
+ const key = e.properties?.[propKey];
338
+ if (!key)
339
+ continue;
340
+ if (!groups.has(key))
341
+ groups.set(key, []);
342
+ groups.get(key).push(e);
343
+ }
344
+ const duplicates = [];
345
+ for (const [, group] of groups) {
346
+ if (group.length <= 1)
347
+ continue;
348
+ // 找出胜者(deduplicateEntities 返回的唯一值)
349
+ const winner = this.deduplicateEntities(group, propKey)[0];
350
+ for (const e of group) {
351
+ if (e.id !== winner.id) {
352
+ duplicates.push(e);
353
+ }
354
+ }
355
+ }
356
+ return duplicates;
357
+ }
267
358
  /** 按 entityType 列出所有实体并按属性过滤 */
268
359
  findEntitiesByType(entityType) {
269
360
  return this.graph.listEntitiesByType(entityType).filter((e) => e.properties?.projectName === this.projectName);
270
361
  }
271
- /** 按属性在指定类型中查找唯一实体 */
362
+ /**
363
+ * 按属性在指定类型中查找唯一实体。
364
+ *
365
+ * Phase-21 改进:当同一 key+value 有多个 Entity(WAL 重复),
366
+ * 选择状态优先级最高 + updatedAt 最新的那个(而非随机第一个)。
367
+ */
272
368
  findEntityByProp(entityType, key, value) {
273
369
  const entities = this.findEntitiesByType(entityType);
274
- return entities.find((e) => e.properties?.[key] === value) || null;
370
+ const matches = entities.filter((e) => e.properties?.[key] === value);
371
+ if (matches.length === 0)
372
+ return null;
373
+ if (matches.length === 1)
374
+ return matches[0];
375
+ // 多个匹配 → 去重取最优
376
+ return this.deduplicateEntities(matches, key)[0] || null;
275
377
  }
276
378
  /** 获取实体的出向关系 */
277
379
  getOutRelations(entityId, relationType) {
380
+ // Phase-44: 优先使用 outgoingByType(直接查询分片,O(出度)),
381
+ // 避免 listRelations 的全量扫描 O(关系总数)
382
+ const g = this.graph;
383
+ if (relationType && typeof g.outgoingByType === 'function') {
384
+ return g.outgoingByType(entityId, relationType);
385
+ }
278
386
  const filter = { sourceId: entityId };
279
387
  if (relationType)
280
388
  filter.relationType = relationType;
@@ -282,6 +390,12 @@ class DevPlanGraphStore {
282
390
  }
283
391
  /** 获取实体的入向关系 */
284
392
  getInRelations(entityId, relationType) {
393
+ // Phase-44: 优先使用 incomingByType(直接查询分片,O(入度)),
394
+ // 避免 listRelations 的全量扫描 O(关系总数)
395
+ const g = this.graph;
396
+ if (relationType && typeof g.incomingByType === 'function') {
397
+ return g.incomingByType(entityId, relationType);
398
+ }
285
399
  const filter = { targetId: entityId };
286
400
  if (relationType)
287
401
  filter.relationType = relationType;
@@ -343,6 +457,7 @@ class DevPlanGraphStore {
343
457
  estimatedHours: p.estimatedHours || undefined,
344
458
  relatedSections: p.relatedSections || [],
345
459
  moduleId: p.moduleId || undefined,
460
+ relatedPromptIds: p.relatedPromptIds || [],
346
461
  totalSubtasks: p.totalSubtasks || 0,
347
462
  completedSubtasks: p.completedSubtasks || 0,
348
463
  status: p.status || 'pending',
@@ -453,10 +568,14 @@ class DevPlanGraphStore {
453
568
  this.graph.flush();
454
569
  return existing.id;
455
570
  }
456
- // 新建文档
457
- const entity = this.graph.addEntity(input.title, ET.DOC, {
571
+ // 新建文档 — upsert by sectionKey 防止重复
572
+ const sectionKey = input.subSection
573
+ ? `${input.section}|${input.subSection}`
574
+ : input.section;
575
+ const entity = this.graph.upsertEntityByProp(ET.DOC, 'sectionKey', sectionKey, input.title, {
458
576
  projectName: this.projectName,
459
577
  section: input.section,
578
+ sectionKey,
460
579
  title: input.title,
461
580
  content: input.content,
462
581
  version,
@@ -515,7 +634,9 @@ class DevPlanGraphStore {
515
634
  return null;
516
635
  }
517
636
  listSections() {
518
- return this.findEntitiesByType(ET.DOC).map((e) => this.entityToDevPlanDoc(e));
637
+ const entities = this.findEntitiesByType(ET.DOC);
638
+ // Phase-23: 引擎层 upsertEntityByProp(sectionKey) 已保证不会新增重复,无需上层去重
639
+ return entities.map((e) => this.entityToDevPlanDoc(e));
519
640
  }
520
641
  updateSection(section, content, subSection) {
521
642
  const existing = this.getSection(section, subSection);
@@ -672,12 +793,13 @@ class DevPlanGraphStore {
672
793
  createMainTask(input) {
673
794
  const existing = this.getMainTask(input.taskId);
674
795
  if (existing) {
675
- throw new Error(`Main task "${input.taskId}" already exists for project "${this.projectName}"`);
796
+ // 已存在 返回现有任务(幂等)
797
+ return existing;
676
798
  }
677
799
  const now = Date.now();
678
800
  // 如果未指定 order,自动分配为当前最大 order + 1
679
801
  const order = input.order != null ? input.order : this.getNextMainTaskOrder();
680
- const entity = this.graph.addEntity(input.title, ET.MAIN_TASK, {
802
+ const entity = this.graph.upsertEntityByProp(ET.MAIN_TASK, 'taskId', input.taskId, input.title, {
681
803
  projectName: this.projectName,
682
804
  taskId: input.taskId,
683
805
  title: input.title,
@@ -685,6 +807,7 @@ class DevPlanGraphStore {
685
807
  description: input.description || '',
686
808
  estimatedHours: input.estimatedHours || 0,
687
809
  relatedSections: input.relatedSections || [],
810
+ relatedPromptIds: input.relatedPromptIds || [],
688
811
  moduleId: input.moduleId || null,
689
812
  totalSubtasks: 0,
690
813
  completedSubtasks: 0,
@@ -713,6 +836,15 @@ class DevPlanGraphStore {
713
836
  }
714
837
  }
715
838
  }
839
+ // task -> prompt 关系(通过 relatedPromptIds 建立)
840
+ if (input.relatedPromptIds?.length) {
841
+ for (const promptId of input.relatedPromptIds) {
842
+ const promptEntity = this.graph.getEntity(promptId);
843
+ if (promptEntity && promptEntity.entity_type === ET.PROMPT) {
844
+ this.graph.putRelation(entity.id, promptEntity.id, RT.TASK_HAS_PROMPT);
845
+ }
846
+ }
847
+ }
716
848
  this.graph.flush();
717
849
  return this.entityToMainTask(entity);
718
850
  }
@@ -741,30 +873,35 @@ class DevPlanGraphStore {
741
873
  const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
742
874
  const finalModuleId = input.moduleId || existing.moduleId;
743
875
  const finalOrder = input.order != null ? input.order : existing.order;
744
- this.graph.updateEntity(existing.id, {
745
- name: input.title,
746
- properties: {
747
- title: input.title,
748
- priority: input.priority,
749
- description: input.description || existing.description || '',
750
- estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
751
- relatedSections: input.relatedSections || existing.relatedSections || [],
752
- moduleId: finalModuleId || null,
753
- status: finalStatus,
754
- order: finalOrder,
755
- updatedAt: now,
756
- completedAt,
757
- },
876
+ // Phase-45: 使用 upsertEntityByProp 代替 updateEntity(修复分片路由不一致)
877
+ const upsertedEntity = this.graph.upsertEntityByProp(ET.MAIN_TASK, 'taskId', input.taskId, input.title, {
878
+ projectName: this.projectName,
879
+ taskId: input.taskId,
880
+ title: input.title,
881
+ priority: input.priority,
882
+ description: input.description || existing.description || '',
883
+ estimatedHours: input.estimatedHours || existing.estimatedHours || 0,
884
+ relatedSections: input.relatedSections || existing.relatedSections || [],
885
+ relatedPromptIds: input.relatedPromptIds || existing.relatedPromptIds || [],
886
+ moduleId: finalModuleId || null,
887
+ totalSubtasks: existing.totalSubtasks,
888
+ completedSubtasks: existing.completedSubtasks,
889
+ status: finalStatus,
890
+ order: finalOrder,
891
+ createdAt: existing.createdAt,
892
+ updatedAt: now,
893
+ completedAt,
758
894
  });
895
+ const upsertedId = upsertedEntity?.id || existing.id;
759
896
  // 更新模块关系
760
897
  if (finalModuleId && finalModuleId !== existing.moduleId) {
761
- this.updateModuleTaskRelation(existing.id, existing.moduleId, finalModuleId);
898
+ this.updateModuleTaskRelation(upsertedId, existing.moduleId, finalModuleId);
762
899
  }
763
900
  // 更新 task -> doc 关系
764
901
  const newRelatedSections = input.relatedSections || existing.relatedSections || [];
765
902
  if (newRelatedSections.length) {
766
903
  // 删除旧的 TASK_HAS_DOC 关系
767
- const oldDocRels = this.getOutRelations(existing.id, RT.TASK_HAS_DOC);
904
+ const oldDocRels = this.getOutRelations(upsertedId, RT.TASK_HAS_DOC);
768
905
  for (const rel of oldDocRels) {
769
906
  this.graph.deleteRelation(rel.id);
770
907
  }
@@ -773,13 +910,12 @@ class DevPlanGraphStore {
773
910
  const [sec, sub] = sk.split('|');
774
911
  const docEntity = this.findDocEntityBySection(sec, sub);
775
912
  if (docEntity) {
776
- this.graph.putRelation(existing.id, docEntity.id, RT.TASK_HAS_DOC);
913
+ this.graph.putRelation(upsertedId, docEntity.id, RT.TASK_HAS_DOC);
777
914
  }
778
915
  }
779
916
  }
780
917
  this.graph.flush();
781
- const updated = this.graph.getEntity(existing.id);
782
- return updated ? this.entityToMainTask(updated) : existing;
918
+ return this.getMainTask(input.taskId) || existing;
783
919
  }
784
920
  getMainTask(taskId) {
785
921
  const entity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', taskId);
@@ -787,6 +923,7 @@ class DevPlanGraphStore {
787
923
  }
788
924
  listMainTasks(filter) {
789
925
  let entities = this.findEntitiesByType(ET.MAIN_TASK);
926
+ // Phase-23: 引擎层 upsertEntityByProp 已保证不会新增重复,无需上层去重
790
927
  if (filter?.status) {
791
928
  entities = entities.filter((e) => e.properties.status === filter.status);
792
929
  }
@@ -805,16 +942,29 @@ class DevPlanGraphStore {
805
942
  return null;
806
943
  const now = Date.now();
807
944
  const completedAt = status === 'completed' ? now : mainTask.completedAt;
808
- this.graph.updateEntity(mainTask.id, {
809
- properties: {
810
- status,
811
- updatedAt: now,
812
- completedAt,
813
- },
945
+ // Phase-45: 使用 upsertEntityByProp 代替 updateEntity
946
+ // 原因:updateEntity 依赖 UUID 直接查找,在分片路由变更后可能写入错误分片,
947
+ // 导致更新不可见。upsertEntityByProp 按属性查找实体,路由始终正确。
948
+ this.graph.upsertEntityByProp(ET.MAIN_TASK, 'taskId', taskId, mainTask.title, {
949
+ projectName: this.projectName,
950
+ taskId,
951
+ title: mainTask.title,
952
+ priority: mainTask.priority,
953
+ description: mainTask.description || '',
954
+ estimatedHours: mainTask.estimatedHours || 0,
955
+ relatedSections: mainTask.relatedSections || [],
956
+ relatedPromptIds: mainTask.relatedPromptIds || [],
957
+ moduleId: mainTask.moduleId || null,
958
+ totalSubtasks: mainTask.totalSubtasks,
959
+ completedSubtasks: mainTask.completedSubtasks,
960
+ status,
961
+ order: mainTask.order,
962
+ createdAt: mainTask.createdAt,
963
+ updatedAt: now,
964
+ completedAt,
814
965
  });
815
966
  this.graph.flush();
816
- const updated = this.graph.getEntity(mainTask.id);
817
- return updated ? this.entityToMainTask(updated) : null;
967
+ return this.getMainTask(taskId);
818
968
  }
819
969
  // ==========================================================================
820
970
  // Sub Task Operations
@@ -822,7 +972,8 @@ class DevPlanGraphStore {
822
972
  addSubTask(input) {
823
973
  const existing = this.getSubTask(input.taskId);
824
974
  if (existing) {
825
- throw new Error(`Sub task "${input.taskId}" already exists for project "${this.projectName}"`);
975
+ // 已存在 返回现有子任务(幂等)
976
+ return existing;
826
977
  }
827
978
  const mainTask = this.getMainTask(input.parentTaskId);
828
979
  if (!mainTask) {
@@ -831,7 +982,7 @@ class DevPlanGraphStore {
831
982
  const now = Date.now();
832
983
  // 如果未指定 order,自动分配为当前父任务下最大 order + 1
833
984
  const order = input.order != null ? input.order : this.getNextSubTaskOrder(input.parentTaskId);
834
- const entity = this.graph.addEntity(input.title, ET.SUB_TASK, {
985
+ const entity = this.graph.upsertEntityByProp(ET.SUB_TASK, 'taskId', input.taskId, input.title, {
835
986
  projectName: this.projectName,
836
987
  taskId: input.taskId,
837
988
  parentTaskId: input.parentTaskId,
@@ -863,7 +1014,7 @@ class DevPlanGraphStore {
863
1014
  }
864
1015
  const now = Date.now();
865
1016
  const order = input.order != null ? input.order : this.getNextSubTaskOrder(input.parentTaskId);
866
- const entity = this.graph.addEntity(input.title, ET.SUB_TASK, {
1017
+ const entity = this.graph.upsertEntityByProp(ET.SUB_TASK, 'taskId', input.taskId, input.title, {
867
1018
  projectName: this.projectName,
868
1019
  taskId: input.taskId,
869
1020
  parentTaskId: input.parentTaskId,
@@ -927,6 +1078,7 @@ class DevPlanGraphStore {
927
1078
  }
928
1079
  listSubTasks(parentTaskId, filter) {
929
1080
  let entities = this.findEntitiesByType(ET.SUB_TASK).filter((e) => e.properties.parentTaskId === parentTaskId);
1081
+ // Phase-23: 引擎层 upsertEntityByProp 已保证不会新增重复,无需上层去重
930
1082
  if (filter?.status) {
931
1083
  entities = entities.filter((e) => e.properties.status === filter.status);
932
1084
  }
@@ -943,18 +1095,25 @@ class DevPlanGraphStore {
943
1095
  ? (options?.completedAtCommit || subTask.completedAtCommit)
944
1096
  : (status === 'pending' ? null : subTask.completedAtCommit);
945
1097
  const revertReason = options?.revertReason || (status === 'pending' ? null : subTask.revertReason);
946
- this.graph.updateEntity(subTask.id, {
947
- properties: {
948
- status,
949
- updatedAt: now,
950
- completedAt,
951
- completedAtCommit: completedAtCommit || null,
952
- revertReason: revertReason || null,
953
- },
1098
+ // Phase-45: 使用 upsertEntityByProp 代替 updateEntity(修复分片路由不一致)
1099
+ this.graph.upsertEntityByProp(ET.SUB_TASK, 'taskId', taskId, subTask.title, {
1100
+ projectName: this.projectName,
1101
+ taskId,
1102
+ parentTaskId: subTask.parentTaskId,
1103
+ title: subTask.title,
1104
+ description: subTask.description || '',
1105
+ estimatedHours: subTask.estimatedHours || 0,
1106
+ relatedFiles: subTask.relatedFiles || [],
1107
+ status,
1108
+ order: subTask.order,
1109
+ createdAt: subTask.createdAt,
1110
+ updatedAt: now,
1111
+ completedAt,
1112
+ completedAtCommit: completedAtCommit || null,
1113
+ revertReason: revertReason || null,
954
1114
  });
955
1115
  this.graph.flush();
956
- const updated = this.graph.getEntity(subTask.id);
957
- return updated ? this.entityToSubTask(updated) : null;
1116
+ return this.getSubTask(taskId);
958
1117
  }
959
1118
  // ==========================================================================
960
1119
  // Completion Workflow
@@ -1100,11 +1259,12 @@ class DevPlanGraphStore {
1100
1259
  createModule(input) {
1101
1260
  const existing = this.getModule(input.moduleId);
1102
1261
  if (existing) {
1103
- throw new Error(`Module "${input.moduleId}" already exists for project "${this.projectName}"`);
1262
+ // 已存在 返回现有模块(幂等)
1263
+ return existing;
1104
1264
  }
1105
1265
  const now = Date.now();
1106
1266
  const status = input.status || 'active';
1107
- const entity = this.graph.addEntity(input.name, ET.MODULE, {
1267
+ const entity = this.graph.upsertEntityByProp(ET.MODULE, 'moduleId', input.moduleId, input.name, {
1108
1268
  projectName: this.projectName,
1109
1269
  moduleId: input.moduleId,
1110
1270
  name: input.name,
@@ -1136,6 +1296,7 @@ class DevPlanGraphStore {
1136
1296
  }
1137
1297
  listModules(filter) {
1138
1298
  let entities = this.findEntitiesByType(ET.MODULE);
1299
+ // Phase-23: 引擎层 upsertEntityByProp 已保证不会新增重复,无需上层去重
1139
1300
  if (filter?.status) {
1140
1301
  entities = entities.filter((e) => e.properties.status === filter.status);
1141
1302
  }
@@ -1274,143 +1435,1828 @@ class DevPlanGraphStore {
1274
1435
  };
1275
1436
  }
1276
1437
  // ==========================================================================
1277
- // Utility
1438
+ // Prompt Operations (用户 Prompt 日志)
1278
1439
  // ==========================================================================
1279
- sync() {
1440
+ /**
1441
+ * 保存一条 Prompt
1442
+ *
1443
+ * 自动分配 promptIndex (当天内自增序号)。
1444
+ * 如果指定了 relatedTaskId,自动建立 task_has_prompt 关系。
1445
+ */
1446
+ savePrompt(input) {
1447
+ const now = Date.now();
1448
+ const today = new Date(now).toISOString().slice(0, 10); // YYYY-MM-DD
1449
+ // 获取当天已有 prompt 数量,分配自增序号
1450
+ const existingToday = this.listPrompts({ date: today });
1451
+ const promptIndex = existingToday.length + 1;
1452
+ // 生成简短标签作为 entity name
1453
+ const namePreview = input.content.slice(0, 50).replace(/\n/g, ' ');
1454
+ const entityName = `Prompt #${promptIndex} (${today})`;
1455
+ const entity = this.graph.addEntity(entityName, ET.PROMPT, {
1456
+ projectName: this.projectName,
1457
+ promptIndex,
1458
+ content: input.content,
1459
+ aiInterpretation: input.aiInterpretation || '',
1460
+ summary: input.summary || '',
1461
+ relatedTaskId: input.relatedTaskId || null,
1462
+ tags: input.tags || [],
1463
+ date: today,
1464
+ createdAt: now,
1465
+ });
1466
+ // project -> prompt 关系
1467
+ this.graph.putRelation(this.getProjectId(), entity.id, RT.HAS_PROMPT);
1468
+ // task -> prompt 关系
1469
+ if (input.relatedTaskId) {
1470
+ const taskEntity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', input.relatedTaskId);
1471
+ if (taskEntity) {
1472
+ this.graph.putRelation(taskEntity.id, entity.id, RT.TASK_HAS_PROMPT);
1473
+ // 同时更新主任务的 relatedPromptIds 数组
1474
+ const taskProps = taskEntity.properties;
1475
+ const existingIds = taskProps.relatedPromptIds || [];
1476
+ if (!existingIds.includes(entity.id)) {
1477
+ this.graph.updateEntity(taskEntity.id, {
1478
+ properties: {
1479
+ ...taskProps,
1480
+ relatedPromptIds: [...existingIds, entity.id],
1481
+ updatedAt: now,
1482
+ },
1483
+ });
1484
+ }
1485
+ }
1486
+ }
1280
1487
  this.graph.flush();
1488
+ return this.entityToPrompt(entity);
1281
1489
  }
1282
- getProjectName() {
1283
- return this.projectName;
1284
- }
1285
- syncWithGit(dryRun = false) {
1286
- const currentHead = this.getCurrentGitCommit();
1287
- if (!currentHead) {
1288
- return {
1289
- checked: 0,
1290
- reverted: [],
1291
- currentHead: 'unknown',
1292
- error: 'Git not available or not in a Git repository',
1293
- };
1490
+ /**
1491
+ * 列出 Prompt(支持按日期、关联任务过滤)
1492
+ */
1493
+ listPrompts(filter) {
1494
+ // 如果按关联任务过滤,通过关系查询
1495
+ if (filter?.relatedTaskId) {
1496
+ return this.getTaskRelatedPrompts(filter.relatedTaskId);
1497
+ }
1498
+ const entities = this.findEntitiesByType(ET.PROMPT);
1499
+ let prompts = entities
1500
+ .filter((e) => {
1501
+ const p = e.properties;
1502
+ if (p.projectName !== this.projectName)
1503
+ return false;
1504
+ if (filter?.date && p.date !== filter.date)
1505
+ return false;
1506
+ return true;
1507
+ })
1508
+ .map((e) => this.entityToPrompt(e));
1509
+ // 按 createdAt 降序排列(最新的在前)
1510
+ prompts.sort((a, b) => b.createdAt - a.createdAt);
1511
+ if (filter?.limit && filter.limit > 0) {
1512
+ prompts = prompts.slice(0, filter.limit);
1294
1513
  }
1295
- const mainTasks = this.listMainTasks();
1296
- const reverted = [];
1297
- let checked = 0;
1298
- for (const mt of mainTasks) {
1299
- const subs = this.listSubTasks(mt.taskId);
1300
- for (const sub of subs) {
1301
- if (sub.status !== 'completed' || !sub.completedAtCommit)
1302
- continue;
1303
- checked++;
1304
- if (!this.isAncestor(sub.completedAtCommit, currentHead)) {
1305
- const reason = `Commit ${sub.completedAtCommit} not found in current branch (HEAD: ${currentHead})`;
1306
- if (!dryRun) {
1307
- this.updateSubTaskStatus(sub.taskId, 'pending', { revertReason: reason });
1308
- this.refreshMainTaskCounts(sub.parentTaskId);
1309
- const parentMain = this.getMainTask(sub.parentTaskId);
1310
- if (parentMain && parentMain.status === 'completed') {
1311
- this.updateMainTaskStatus(sub.parentTaskId, 'in_progress');
1312
- }
1313
- }
1314
- reverted.push({
1315
- taskId: sub.taskId,
1316
- title: sub.title,
1317
- parentTaskId: sub.parentTaskId,
1318
- completedAtCommit: sub.completedAtCommit,
1319
- reason,
1320
- });
1321
- }
1514
+ return prompts;
1515
+ }
1516
+ /**
1517
+ * 获取主任务关联的所有 Prompt
1518
+ */
1519
+ getTaskRelatedPrompts(taskId) {
1520
+ const taskEntity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', taskId);
1521
+ if (!taskEntity)
1522
+ return [];
1523
+ const rels = this.getOutRelations(taskEntity.id, RT.TASK_HAS_PROMPT);
1524
+ const prompts = [];
1525
+ for (const rel of rels) {
1526
+ const promptEntity = this.graph.getEntity(rel.target);
1527
+ if (promptEntity && promptEntity.entity_type === ET.PROMPT) {
1528
+ prompts.push(this.entityToPrompt(promptEntity));
1322
1529
  }
1323
1530
  }
1324
- return { checked, reverted, currentHead };
1531
+ // createdAt 升序排列
1532
+ prompts.sort((a, b) => a.createdAt - b.createdAt);
1533
+ return prompts;
1534
+ }
1535
+ /**
1536
+ * Entity → Prompt 转换
1537
+ */
1538
+ entityToPrompt(e) {
1539
+ const p = e.properties;
1540
+ return {
1541
+ id: e.id,
1542
+ projectName: this.projectName,
1543
+ promptIndex: p.promptIndex || 0,
1544
+ content: p.content || '',
1545
+ aiInterpretation: p.aiInterpretation || undefined,
1546
+ summary: p.summary || undefined,
1547
+ relatedTaskId: p.relatedTaskId || undefined,
1548
+ tags: p.tags || [],
1549
+ createdAt: p.createdAt || e.created_at,
1550
+ };
1325
1551
  }
1326
1552
  // ==========================================================================
1327
- // Graph Export (核心差异能力)
1553
+ // Memory Operations (Cursor 长期记忆)
1328
1554
  // ==========================================================================
1329
1555
  /**
1330
- * 导出 DevPlan 的图结构用于可视化
1556
+ * 保存一条记忆
1331
1557
  *
1332
- * 返回 vis-network 兼容的 { nodes, edges } 格式。
1558
+ * - 记忆存储为 devplan-memory 实体
1559
+ * - 如启用语义搜索,自动 embed content 并索引到 HNSW
1560
+ * - 关联 project → memory 和可选的 memory → task 关系
1333
1561
  */
1334
- exportGraph(options) {
1335
- const includeDocuments = options?.includeDocuments !== false;
1336
- const includeModules = options?.includeModules !== false;
1337
- const includeNodeDegree = options?.includeNodeDegree !== false;
1338
- const enableBackendDegreeFallback = options?.enableBackendDegreeFallback !== false;
1339
- const nodes = [];
1340
- const edges = [];
1341
- // 项目根节点
1342
- nodes.push({
1343
- id: this.getProjectId(),
1344
- label: this.projectName,
1345
- type: 'project',
1346
- properties: { entityType: ET.PROJECT },
1347
- });
1348
- // 主任务节点
1349
- const mainTasks = this.listMainTasks();
1350
- for (const mt of mainTasks) {
1351
- nodes.push({
1352
- id: mt.id,
1353
- label: mt.title,
1354
- type: 'main-task',
1562
+ saveMemory(input) {
1563
+ const now = Date.now();
1564
+ // ---- 去重检测 ----
1565
+ const existingDup = this.findDuplicateMemory(input);
1566
+ if (existingDup) {
1567
+ // 已存在相同记忆 更新而非新建
1568
+ const oldProps = existingDup.properties;
1569
+ this.graph.updateEntity(existingDup.id, {
1355
1570
  properties: {
1356
- taskId: mt.taskId,
1357
- priority: mt.priority,
1358
- status: mt.status,
1359
- totalSubtasks: mt.totalSubtasks,
1360
- completedSubtasks: mt.completedSubtasks,
1361
- completedAt: mt.completedAt || null,
1571
+ ...oldProps,
1572
+ content: input.content,
1573
+ memoryType: input.memoryType,
1574
+ tags: input.tags || oldProps.tags || [],
1575
+ importance: input.importance ?? oldProps.importance ?? 0.5,
1576
+ sourceId: input.sourceId || oldProps.sourceId || null,
1577
+ updatedAt: now,
1362
1578
  },
1363
1579
  });
1364
- edges.push({
1365
- from: this.getProjectId(),
1366
- to: mt.id,
1367
- label: RT.HAS_MAIN_TASK,
1368
- });
1369
- // 子任务节点
1370
- const subTasks = this.listSubTasks(mt.taskId);
1371
- for (const st of subTasks) {
1372
- nodes.push({
1373
- id: st.id,
1374
- label: st.title,
1375
- type: 'sub-task',
1376
- properties: {
1377
- taskId: st.taskId,
1378
- parentTaskId: st.parentTaskId,
1379
- status: st.status,
1380
- completedAt: st.completedAt || null,
1381
- },
1382
- });
1383
- edges.push({
1384
- from: mt.id,
1385
- to: st.id,
1386
- label: RT.HAS_SUB_TASK,
1387
- });
1580
+ // 重新索引向量
1581
+ if (this.semanticSearchReady && this.synapse) {
1582
+ try {
1583
+ const embedding = this.synapse.embed(input.content);
1584
+ this.graph.indexEntity(existingDup.id, embedding);
1585
+ }
1586
+ catch (e) {
1587
+ console.warn(`[DevPlan] Failed to re-index memory ${existingDup.id}: ${e instanceof Error ? e.message : String(e)}`);
1588
+ }
1388
1589
  }
1389
- // task -> doc 关系
1390
- const taskDocRels = this.getOutRelations(mt.id, RT.TASK_HAS_DOC);
1391
- for (const rel of taskDocRels) {
1392
- edges.push({
1393
- from: mt.id,
1394
- to: rel.target,
1395
- label: RT.TASK_HAS_DOC,
1396
- });
1590
+ this.graph.flush();
1591
+ const updated = this.graph.getEntity(existingDup.id);
1592
+ return this.entityToMemory(updated || existingDup);
1593
+ }
1594
+ // ---- 新建记忆 ----
1595
+ const entityName = `Memory: ${input.memoryType} — ${input.content.slice(0, 40)}`;
1596
+ const entity = this.graph.addEntity(entityName, ET.MEMORY, {
1597
+ projectName: this.projectName,
1598
+ memoryType: input.memoryType,
1599
+ content: input.content,
1600
+ tags: input.tags || [],
1601
+ relatedTaskId: input.relatedTaskId || null,
1602
+ sourceId: input.sourceId || null,
1603
+ importance: input.importance ?? 0.5,
1604
+ hitCount: 0,
1605
+ lastAccessedAt: null,
1606
+ createdAt: now,
1607
+ updatedAt: now,
1608
+ });
1609
+ // project → memory 关系
1610
+ this.graph.putRelation(this.getProjectId(), entity.id, RT.HAS_MEMORY);
1611
+ // memory → task 关系(可选)
1612
+ if (input.relatedTaskId) {
1613
+ const taskEntity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', input.relatedTaskId);
1614
+ if (taskEntity) {
1615
+ this.graph.putRelation(entity.id, taskEntity.id, RT.MEMORY_FROM_TASK);
1397
1616
  }
1398
1617
  }
1399
- // 文档节点
1400
- if (includeDocuments) {
1401
- const docs = this.listSections();
1402
- for (const doc of docs) {
1403
- nodes.push({
1404
- id: doc.id,
1405
- label: doc.title,
1406
- type: 'document',
1407
- properties: {
1408
- section: doc.section,
1409
- subSection: doc.subSection,
1410
- version: doc.version,
1411
- parentDoc: doc.parentDoc || null,
1412
- childDocs: doc.childDocs || [],
1413
- },
1618
+ // 向量索引(如启用语义搜索)
1619
+ let embedding = null;
1620
+ if (this.semanticSearchReady && this.synapse) {
1621
+ try {
1622
+ embedding = this.synapse.embed(input.content);
1623
+ this.graph.indexEntity(entity.id, embedding);
1624
+ }
1625
+ catch (e) {
1626
+ console.warn(`[DevPlan] Failed to index memory ${entity.id}: ${e instanceof Error ? e.message : String(e)}`);
1627
+ }
1628
+ }
1629
+ // ---- Phase-37: 自动建立记忆间关联(MEMORY_RELATES) ----
1630
+ if (embedding && this.semanticSearchReady) {
1631
+ this.autoLinkSimilarMemories(entity.id, embedding);
1632
+ }
1633
+ // ---- Phase-37: 从文档生成的记忆自动建立 MEMORY_FROM_DOC 关系 ----
1634
+ if (input.sourceId) {
1635
+ this.autoLinkMemoryToDoc(entity.id, input.sourceId);
1636
+ }
1637
+ // ---- Phase-37: 模块级记忆自动建立 MODULE_MEMORY 关系 ----
1638
+ if (input.moduleId) {
1639
+ this.autoLinkMemoryToModule(entity.id, input.moduleId);
1640
+ }
1641
+ this.graph.flush();
1642
+ return this.entityToMemory(entity);
1643
+ }
1644
+ /**
1645
+ * Phase-37→44: 自动建立记忆间语义关联
1646
+ *
1647
+ * 用向量搜索找到最相似的 N 条已有记忆(minScore >= 0.7),
1648
+ * 建立 MEMORY_RELATES 双向关系,权重为 similarity score。
1649
+ *
1650
+ * Phase-44: 使用 applyMutations 批量创建关系(单次 Rust 调用),
1651
+ * 替代逐条 putRelation 的多次跨层调用。
1652
+ */
1653
+ autoLinkSimilarMemories(newMemoryId, embedding, maxLinks = 3, minScore = 0.7) {
1654
+ try {
1655
+ const hits = this.graph.searchEntitiesByVector(embedding, maxLinks + 5, ET.MEMORY);
1656
+ // 收集需要创建的关系
1657
+ const relationsToCreate = [];
1658
+ for (const hit of hits) {
1659
+ if (relationsToCreate.length >= maxLinks)
1660
+ break;
1661
+ if (hit.entityId === newMemoryId)
1662
+ continue;
1663
+ if (hit.score < minScore)
1664
+ continue;
1665
+ const target = this.graph.getEntity(hit.entityId);
1666
+ if (!target)
1667
+ continue;
1668
+ const p = target.properties;
1669
+ if (p.projectName !== this.projectName)
1670
+ continue;
1671
+ relationsToCreate.push({ targetId: hit.entityId, score: hit.score });
1672
+ }
1673
+ if (relationsToCreate.length === 0)
1674
+ return;
1675
+ // Phase-44: 尝试使用 applyMutations 批量创建(单次 Rust 调用)
1676
+ const g = this.graph;
1677
+ if (typeof g.applyMutations === 'function') {
1678
+ const mutations = [];
1679
+ for (const { targetId, score } of relationsToCreate) {
1680
+ // 正向关系
1681
+ mutations.push({
1682
+ type: 'PutRelation',
1683
+ relation: {
1684
+ source: newMemoryId,
1685
+ target: targetId,
1686
+ relation_type: RT.MEMORY_RELATES,
1687
+ weight: score,
1688
+ },
1689
+ });
1690
+ // 反向关系(双向)
1691
+ mutations.push({
1692
+ type: 'PutRelation',
1693
+ relation: {
1694
+ source: targetId,
1695
+ target: newMemoryId,
1696
+ relation_type: RT.MEMORY_RELATES,
1697
+ weight: score,
1698
+ },
1699
+ });
1700
+ }
1701
+ g.applyMutations(mutations).catch((e) => {
1702
+ console.warn(`[DevPlan] applyMutations for memory links failed: ${e}`);
1703
+ });
1704
+ }
1705
+ else {
1706
+ // Fallback: 逐条创建
1707
+ for (const { targetId, score } of relationsToCreate) {
1708
+ this.graph.putRelation(newMemoryId, targetId, RT.MEMORY_RELATES, score, true);
1709
+ }
1710
+ }
1711
+ console.log(`[DevPlan] Memory ${newMemoryId.slice(0, 8)}... auto-linked to ${relationsToCreate.length} similar memories`);
1712
+ }
1713
+ catch (e) {
1714
+ console.warn(`[DevPlan] autoLinkSimilarMemories failed: ${e instanceof Error ? e.message : String(e)}`);
1715
+ }
1716
+ }
1717
+ /**
1718
+ * Phase-37: 从文档生成的记忆自动建立 MEMORY_FROM_DOC 关系
1719
+ *
1720
+ * 当 sourceId 格式为 "section" 或 "section|subSection" 时,
1721
+ * 找到对应的文档 Entity 并建立 MEMORY_FROM_DOC 关系。
1722
+ */
1723
+ autoLinkMemoryToDoc(memoryId, sourceId) {
1724
+ try {
1725
+ // sourceId 格式:"section" 或 "section|subSection"
1726
+ // 任务格式:"phase-7"(以 phase- 或 T 开头的不是文档)
1727
+ if (sourceId.startsWith('phase-') || sourceId.startsWith('T'))
1728
+ return;
1729
+ // 解析 section 和 subSection
1730
+ const parts = sourceId.split('|');
1731
+ const section = parts[0];
1732
+ const subSection = parts[1];
1733
+ // 查找文档 Entity
1734
+ const docs = this.findEntitiesByType(ET.DOC);
1735
+ const docEntity = docs.find((e) => {
1736
+ const p = e.properties;
1737
+ if (p.projectName !== this.projectName)
1738
+ return false;
1739
+ if (p.section !== section)
1740
+ return false;
1741
+ if (subSection && p.subSection !== subSection)
1742
+ return false;
1743
+ if (!subSection && p.subSection)
1744
+ return false;
1745
+ return true;
1746
+ });
1747
+ if (docEntity) {
1748
+ this.graph.putRelation(docEntity.id, memoryId, RT.MEMORY_FROM_DOC);
1749
+ }
1750
+ }
1751
+ catch (e) {
1752
+ console.warn(`[DevPlan] autoLinkMemoryToDoc failed: ${e instanceof Error ? e.message : String(e)}`);
1753
+ }
1754
+ }
1755
+ /**
1756
+ * Phase-37: 模块级记忆自动建立 MODULE_MEMORY 关系
1757
+ */
1758
+ autoLinkMemoryToModule(memoryId, moduleId) {
1759
+ try {
1760
+ const modEntity = this.findEntityByProp(ET.MODULE, 'moduleId', moduleId);
1761
+ if (modEntity) {
1762
+ this.graph.putRelation(modEntity.id, memoryId, RT.MODULE_MEMORY);
1763
+ }
1764
+ }
1765
+ catch (e) {
1766
+ console.warn(`[DevPlan] autoLinkMemoryToModule failed: ${e instanceof Error ? e.message : String(e)}`);
1767
+ }
1768
+ }
1769
+ /**
1770
+ * 查找重复记忆 — 按 relatedTaskId + memoryType 或内容指纹去重
1771
+ *
1772
+ * 匹配规则(任一命中即视为重复):
1773
+ * 1. 相同 relatedTaskId + 相同 memoryType
1774
+ * 2. 内容前 80 字符指纹相同 + 相同 memoryType
1775
+ */
1776
+ findDuplicateMemory(input) {
1777
+ const allMemories = this.findEntitiesByType(ET.MEMORY);
1778
+ const inputFingerprint = input.content.slice(0, 80).toLowerCase().trim();
1779
+ for (const e of allMemories) {
1780
+ const p = e.properties;
1781
+ if (p.projectName !== this.projectName)
1782
+ continue;
1783
+ if (p.memoryType !== input.memoryType)
1784
+ continue;
1785
+ // 规则 1: 相同 relatedTaskId
1786
+ if (input.relatedTaskId && p.relatedTaskId === input.relatedTaskId) {
1787
+ return e;
1788
+ }
1789
+ // 规则 2: 内容指纹相同
1790
+ const existingFingerprint = (p.content || '').slice(0, 80).toLowerCase().trim();
1791
+ if (inputFingerprint.length > 10 && existingFingerprint === inputFingerprint) {
1792
+ return e;
1793
+ }
1794
+ }
1795
+ return null;
1796
+ }
1797
+ /**
1798
+ * 智能召回记忆 — 语义向量搜索 + 图谱遍历扩展 + RRF 融合
1799
+ *
1800
+ * Phase-38 增强: 向量搜索结果沿 MEMORY_RELATES 关系扩展,发现关联记忆。
1801
+ * 如语义搜索不可用,退化为字面匹配。
1802
+ * 命中的记忆自动更新 hitCount 和 lastAccessedAt。
1803
+ */
1804
+ recallMemory(query, options) {
1805
+ const limit = options?.limit || 10;
1806
+ const minScore = options?.minScore || 0;
1807
+ const includeDocs = options?.includeDocs !== false; // 默认 true
1808
+ const graphExpand = options?.graphExpand !== false; // 默认 true
1809
+ const now = Date.now();
1810
+ // ---- 1. 记忆召回(向量搜索) ----
1811
+ let memoryResults = [];
1812
+ // 尝试语义搜索
1813
+ if (this.semanticSearchReady && this.synapse) {
1814
+ try {
1815
+ const embedding = this.synapse.embed(query);
1816
+ const hits = this.graph.searchEntitiesByVector(embedding, limit * 3, ET.MEMORY);
1817
+ for (const hit of hits) {
1818
+ if (minScore > 0 && hit.score < minScore)
1819
+ continue;
1820
+ const entity = this.graph.getEntity(hit.entityId);
1821
+ if (!entity)
1822
+ continue;
1823
+ const p = entity.properties;
1824
+ if (p.projectName !== this.projectName)
1825
+ continue;
1826
+ if (options?.memoryType && p.memoryType !== options.memoryType)
1827
+ continue;
1828
+ // 更新访问统计
1829
+ this.graph.updateEntity(entity.id, {
1830
+ properties: {
1831
+ ...p,
1832
+ hitCount: (p.hitCount || 0) + 1,
1833
+ lastAccessedAt: now,
1834
+ },
1835
+ });
1836
+ memoryResults.push({
1837
+ ...this.entityToMemory(entity),
1838
+ hitCount: (p.hitCount || 0) + 1,
1839
+ lastAccessedAt: now,
1840
+ score: hit.score,
1841
+ sourceKind: 'memory',
1842
+ });
1843
+ if (memoryResults.length >= limit)
1844
+ break;
1845
+ }
1846
+ if (memoryResults.length > 0) {
1847
+ this.graph.flush();
1848
+ }
1849
+ }
1850
+ catch (e) {
1851
+ console.warn(`[DevPlan] Semantic memory recall failed: ${e instanceof Error ? e.message : String(e)}`);
1852
+ }
1853
+ }
1854
+ // 如果语义搜索无结果,fallback 到字面匹配
1855
+ if (memoryResults.length === 0) {
1856
+ const entities = this.findEntitiesByType(ET.MEMORY);
1857
+ const queryLower = query.toLowerCase();
1858
+ for (const e of entities) {
1859
+ const p = e.properties;
1860
+ if (p.projectName !== this.projectName)
1861
+ continue;
1862
+ if (options?.memoryType && p.memoryType !== options.memoryType)
1863
+ continue;
1864
+ const content = (p.content || '').toLowerCase();
1865
+ const tags = (p.tags || []).join(' ').toLowerCase();
1866
+ if (content.includes(queryLower) || tags.includes(queryLower)) {
1867
+ this.graph.updateEntity(e.id, {
1868
+ properties: {
1869
+ ...p,
1870
+ hitCount: (p.hitCount || 0) + 1,
1871
+ lastAccessedAt: now,
1872
+ },
1873
+ });
1874
+ memoryResults.push({
1875
+ ...this.entityToMemory(e),
1876
+ hitCount: (p.hitCount || 0) + 1,
1877
+ lastAccessedAt: now,
1878
+ score: 0.5,
1879
+ sourceKind: 'memory',
1880
+ });
1881
+ }
1882
+ }
1883
+ memoryResults.sort((a, b) => b.importance - a.importance);
1884
+ memoryResults = memoryResults.slice(0, limit);
1885
+ if (memoryResults.length > 0) {
1886
+ this.graph.flush();
1887
+ }
1888
+ }
1889
+ // ---- 1.5 Phase-38: 图谱关联扩展 ----
1890
+ if (graphExpand && memoryResults.length > 0) {
1891
+ const graphExpanded = this.expandMemoriesByGraph(memoryResults, options?.memoryType, limit);
1892
+ if (graphExpanded.length > 0) {
1893
+ // 用 RRF 融合向量结果和图谱扩展结果
1894
+ memoryResults = this.rrfMergeMemories(memoryResults, graphExpanded, limit);
1895
+ }
1896
+ }
1897
+ // ---- 1.6 Phase-44: 赫布学习 — 共同激活的记忆自动增强连接 ----
1898
+ if (memoryResults.length >= 2) {
1899
+ this.hebbianStrengthen(memoryResults.filter(m => m.sourceKind === 'memory'));
1900
+ }
1901
+ // ---- 2. 文档召回(统一召回) ----
1902
+ if (!includeDocs || options?.memoryType) {
1903
+ // 如果不需要文档或指定了 memoryType 过滤,直接返回记忆结果
1904
+ return memoryResults;
1905
+ }
1906
+ let docResults = [];
1907
+ try {
1908
+ const docHits = this.searchSectionsAdvanced(query, {
1909
+ mode: this.semanticSearchReady ? 'hybrid' : 'literal',
1910
+ limit: Math.max(5, Math.floor(limit / 2)),
1911
+ minScore: minScore > 0 ? minScore : undefined,
1912
+ });
1913
+ for (const doc of docHits) {
1914
+ // 将文档结果包装为 ScoredMemory 格式(统一接口)
1915
+ const contentSnippet = (doc.content || '').substring(0, 300);
1916
+ docResults.push({
1917
+ id: `doc:${doc.section}${doc.subSection ? '|' + doc.subSection : ''}`,
1918
+ projectName: this.projectName,
1919
+ memoryType: this.docSectionToMemoryType(doc.section),
1920
+ content: contentSnippet + (doc.content && doc.content.length > 300 ? '...' : ''),
1921
+ tags: [doc.section, ...(doc.subSection ? [doc.subSection] : [])],
1922
+ relatedTaskId: undefined,
1923
+ importance: 0.6,
1924
+ hitCount: 0,
1925
+ lastAccessedAt: null,
1926
+ createdAt: doc.updatedAt || 0,
1927
+ updatedAt: doc.updatedAt || 0,
1928
+ score: doc.score || 0.4,
1929
+ sourceKind: 'doc',
1930
+ docSection: doc.section,
1931
+ docSubSection: doc.subSection || undefined,
1932
+ docTitle: doc.title,
1933
+ });
1934
+ }
1935
+ }
1936
+ catch (e) {
1937
+ console.warn(`[DevPlan] Document recall failed: ${e instanceof Error ? e.message : String(e)}`);
1938
+ }
1939
+ // ---- 3. RRF 融合排序 ----
1940
+ if (docResults.length === 0)
1941
+ return memoryResults;
1942
+ if (memoryResults.length === 0)
1943
+ return docResults.slice(0, limit);
1944
+ return this.rrfMergeResults(memoryResults, docResults, limit);
1945
+ }
1946
+ /**
1947
+ * 文档 section 类型 → 建议的 memoryType 映射
1948
+ */
1949
+ docSectionToMemoryType(section) {
1950
+ const mapping = {
1951
+ overview: 'summary',
1952
+ core_concepts: 'insight',
1953
+ api_design: 'pattern',
1954
+ technical_notes: 'insight',
1955
+ config: 'preference',
1956
+ examples: 'pattern',
1957
+ changelog: 'summary',
1958
+ milestones: 'summary',
1959
+ };
1960
+ return mapping[section] || 'insight';
1961
+ }
1962
+ /**
1963
+ * RRF (Reciprocal Rank Fusion) 合并记忆和文档结果
1964
+ *
1965
+ * 记忆权重略高(rank 常数更小),因为记忆是经过提炼的知识。
1966
+ */
1967
+ rrfMergeResults(memoryResults, docResults, limit) {
1968
+ const k_memory = 30; // 记忆 RRF 常数(越小权重越高)
1969
+ const k_doc = 50; // 文档 RRF 常数
1970
+ const scoreMap = new Map();
1971
+ for (let i = 0; i < memoryResults.length; i++) {
1972
+ const item = memoryResults[i];
1973
+ const rrfScore = 1.0 / (k_memory + i + 1);
1974
+ scoreMap.set(item.id, { item, rrfScore });
1975
+ }
1976
+ for (let i = 0; i < docResults.length; i++) {
1977
+ const item = docResults[i];
1978
+ const rrfScore = 1.0 / (k_doc + i + 1);
1979
+ const existing = scoreMap.get(item.id);
1980
+ if (existing) {
1981
+ existing.rrfScore += rrfScore;
1982
+ }
1983
+ else {
1984
+ scoreMap.set(item.id, { item, rrfScore });
1985
+ }
1986
+ }
1987
+ // Phase-39: 对记忆类结果施加热度加权(文档不受影响)
1988
+ const merged = Array.from(scoreMap.values())
1989
+ .map(({ item, rrfScore }) => {
1990
+ if (item.sourceKind === 'memory' || !item.sourceKind) {
1991
+ const hotness = this.computeMemoryHotness(item);
1992
+ const hotnessMultiplier = 0.5 + hotness; // [0.5, 1.5]
1993
+ return { item, rrfScore: rrfScore * hotnessMultiplier };
1994
+ }
1995
+ return { item, rrfScore };
1996
+ })
1997
+ .sort((a, b) => b.rrfScore - a.rrfScore)
1998
+ .slice(0, limit)
1999
+ .map(({ item, rrfScore }) => ({ ...item, score: rrfScore }));
2000
+ return merged;
2001
+ }
2002
+ /**
2003
+ * Phase-38→44: 沿 MEMORY_RELATES 图谱关系扩展发现关联记忆
2004
+ *
2005
+ * Phase-44 增强: 优先使用 extractSubgraph 在 Rust 层完成 N-hop 遍历,
2006
+ * 避免 JS 层逐跳手动查询的 N 次跨层调用开销。
2007
+ *
2008
+ * 从向量搜索命中的记忆出发,沿 MEMORY_RELATES 关系探索 1-2 跳,
2009
+ * 发现未被向量搜索直接命中但图谱关联的记忆。
2010
+ * 关联分数 = 原始记忆分数 × 关系权重^depth × 衰减因子
2011
+ */
2012
+ expandMemoriesByGraph(seedMemories, memoryType, limit = 10) {
2013
+ const g = this.graph;
2014
+ // Phase-44: 优先使用 extractSubgraph — 单次 Rust 调用完成 N-hop 遍历
2015
+ if (typeof g.extractSubgraph === 'function') {
2016
+ return this.expandMemoriesBySubgraph(seedMemories, memoryType, limit);
2017
+ }
2018
+ // Fallback: 手动逐跳遍历(旧版 aifastdb 兼容)
2019
+ return this.expandMemoriesByManualTraversal(seedMemories, memoryType, limit);
2020
+ }
2021
+ /**
2022
+ * Phase-44: 用 extractSubgraph 实现高效图谱扩展
2023
+ *
2024
+ * 对每个种子记忆调用 extractSubgraph(seedId, {max_hops:2, direction:'both',
2025
+ * relation_type_filter:['memory_relates'], entity_type_filter:['devplan-memory']}),
2026
+ * Rust 层在分片内一次性完成 BFS 遍历,返回子图的 entities + depth_map。
2027
+ */
2028
+ expandMemoriesBySubgraph(seedMemories, memoryType, limit = 10) {
2029
+ const g = this.graph;
2030
+ const seenIds = new Set(seedMemories.map((m) => m.id));
2031
+ const expanded = [];
2032
+ const hopDecay = 0.7;
2033
+ for (const seed of seedMemories.slice(0, 5)) {
2034
+ try {
2035
+ const subgraph = g.extractSubgraph(seed.id, {
2036
+ max_hops: 2,
2037
+ direction: 'both',
2038
+ relation_type_filter: [RT.MEMORY_RELATES],
2039
+ entity_type_filter: [ET.MEMORY],
2040
+ max_nodes: limit * 2,
2041
+ });
2042
+ if (!subgraph || !subgraph.entities || !subgraph.depth_map)
2043
+ continue;
2044
+ // 遍历子图中的实体,按 depth 计算衰减分数
2045
+ const entityEntries = Object.entries(subgraph.entities);
2046
+ for (const [entityId, entity] of entityEntries) {
2047
+ if (seenIds.has(entityId))
2048
+ continue;
2049
+ if (!entity)
2050
+ continue;
2051
+ const p = entity.properties || {};
2052
+ if (p.projectName !== this.projectName)
2053
+ continue;
2054
+ if (memoryType && p.memoryType !== memoryType)
2055
+ continue;
2056
+ seenIds.add(entityId);
2057
+ const depth = subgraph.depth_map[entityId] || 1;
2058
+ const expandedScore = seed.score * Math.pow(hopDecay, depth);
2059
+ expanded.push({
2060
+ ...this.entityToMemory(entity),
2061
+ score: expandedScore,
2062
+ sourceKind: 'memory',
2063
+ });
2064
+ if (expanded.length >= limit)
2065
+ break;
2066
+ }
2067
+ }
2068
+ catch (e) {
2069
+ console.warn(`[DevPlan] extractSubgraph failed for ${seed.id}: ${e instanceof Error ? e.message : String(e)}`);
2070
+ }
2071
+ if (expanded.length >= limit)
2072
+ break;
2073
+ }
2074
+ return expanded;
2075
+ }
2076
+ /**
2077
+ * Phase-38 原始实现: 手动逐跳遍历(Fallback)
2078
+ */
2079
+ expandMemoriesByManualTraversal(seedMemories, memoryType, limit = 10) {
2080
+ const seenIds = new Set(seedMemories.map((m) => m.id));
2081
+ const expanded = [];
2082
+ const hopDecay = 0.7;
2083
+ for (const seed of seedMemories.slice(0, 5)) {
2084
+ const outRels = this.getOutRelations(seed.id, RT.MEMORY_RELATES);
2085
+ const inRels = this.getInRelations(seed.id, RT.MEMORY_RELATES);
2086
+ const allRels = [...outRels, ...inRels];
2087
+ for (const rel of allRels) {
2088
+ const neighborId = rel.source === seed.id ? rel.target : rel.source;
2089
+ if (seenIds.has(neighborId))
2090
+ continue;
2091
+ const neighbor = this.graph.getEntity(neighborId);
2092
+ if (!neighbor)
2093
+ continue;
2094
+ const p = neighbor.properties;
2095
+ if (p.projectName !== this.projectName)
2096
+ continue;
2097
+ if (memoryType && p.memoryType !== memoryType)
2098
+ continue;
2099
+ seenIds.add(neighborId);
2100
+ const relWeight = rel.weight || 0.5;
2101
+ const expandedScore = seed.score * relWeight * hopDecay;
2102
+ expanded.push({
2103
+ ...this.entityToMemory(neighbor),
2104
+ score: expandedScore,
2105
+ sourceKind: 'memory',
2106
+ });
2107
+ if (expanded.length >= limit)
2108
+ break;
2109
+ // 2跳
2110
+ const hop2OutRels = this.getOutRelations(neighborId, RT.MEMORY_RELATES);
2111
+ const hop2InRels = this.getInRelations(neighborId, RT.MEMORY_RELATES);
2112
+ for (const hop2Rel of [...hop2OutRels, ...hop2InRels]) {
2113
+ const hop2Id = hop2Rel.source === neighborId ? hop2Rel.target : hop2Rel.source;
2114
+ if (seenIds.has(hop2Id))
2115
+ continue;
2116
+ const hop2Entity = this.graph.getEntity(hop2Id);
2117
+ if (!hop2Entity)
2118
+ continue;
2119
+ const hp = hop2Entity.properties;
2120
+ if (hp.projectName !== this.projectName)
2121
+ continue;
2122
+ if (memoryType && hp.memoryType !== memoryType)
2123
+ continue;
2124
+ seenIds.add(hop2Id);
2125
+ const hop2Weight = hop2Rel.weight || 0.5;
2126
+ const hop2Score = expandedScore * hop2Weight * hopDecay;
2127
+ expanded.push({
2128
+ ...this.entityToMemory(hop2Entity),
2129
+ score: hop2Score,
2130
+ sourceKind: 'memory',
2131
+ });
2132
+ if (expanded.length >= limit)
2133
+ break;
2134
+ }
2135
+ }
2136
+ if (expanded.length >= limit)
2137
+ break;
2138
+ }
2139
+ return expanded;
2140
+ }
2141
+ /**
2142
+ * Phase-44: 赫布学习 — 共同激活的记忆自动增强连接权重
2143
+ *
2144
+ * "Neurons that fire together, wire together."
2145
+ * 当多条记忆在同一次召回中被共同激活,增强它们之间 MEMORY_RELATES 关系的权重。
2146
+ *
2147
+ * - 使用 adjustRelationWeight(Rust 原子操作),delta = +0.05
2148
+ * - 仅增强已有 MEMORY_RELATES 连接(不创建新连接)
2149
+ * - 上限 top5 记忆的 C(5,2) = 10 对,避免大量关系更新
2150
+ */
2151
+ hebbianStrengthen(coActivatedMemories) {
2152
+ const g = this.graph;
2153
+ if (typeof g.adjustRelationWeight !== 'function')
2154
+ return;
2155
+ if (coActivatedMemories.length < 2)
2156
+ return;
2157
+ const HEBBIAN_DELTA = 0.05;
2158
+ const top = coActivatedMemories.slice(0, 5); // 最多取 top5
2159
+ // 遍历所有对,检查是否有 MEMORY_RELATES 连接
2160
+ for (let i = 0; i < top.length; i++) {
2161
+ for (let j = i + 1; j < top.length; j++) {
2162
+ const a = top[i].id;
2163
+ const b = top[j].id;
2164
+ // 检查 a→b 或 b→a 是否有 MEMORY_RELATES 关系
2165
+ const outRels = this.getOutRelations(a, RT.MEMORY_RELATES);
2166
+ const rel = outRels.find((r) => r.target === b);
2167
+ if (rel) {
2168
+ g.adjustRelationWeight(rel.id, a, HEBBIAN_DELTA).catch(() => { });
2169
+ }
2170
+ // 检查反向 b→a
2171
+ const reverseRels = this.getOutRelations(b, RT.MEMORY_RELATES);
2172
+ const reverseRel = reverseRels.find((r) => r.target === a);
2173
+ if (reverseRel) {
2174
+ g.adjustRelationWeight(reverseRel.id, b, HEBBIAN_DELTA).catch(() => { });
2175
+ }
2176
+ }
2177
+ }
2178
+ }
2179
+ /**
2180
+ * Phase-39: 计算记忆热度分数 — 模拟遗忘曲线
2181
+ *
2182
+ * 综合 hitCount + 时间衰减 + importance,输出 0~1 的热度值。
2183
+ * - 高频访问 + 近期访问 → 热度高
2184
+ * - 长期未访问 → 热度衰减(半衰期 7 天)
2185
+ * - importance 作为基线加成
2186
+ */
2187
+ computeMemoryHotness(memory) {
2188
+ const now = Date.now();
2189
+ const HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000; // 7天半衰期
2190
+ const LN2 = Math.LN2;
2191
+ // 时间衰减:基于最后访问时间(如无访问则用创建时间)
2192
+ const lastAccess = memory.lastAccessedAt || memory.createdAt;
2193
+ const elapsed = Math.max(0, now - lastAccess);
2194
+ const timeDecay = Math.exp(-(LN2 / HALF_LIFE_MS) * elapsed); // 0~1
2195
+ // 频率分数:hitCount 的对数缩放,避免过高值主导
2196
+ const freqScore = Math.min(1, Math.log2(1 + (memory.hitCount || 0)) / 5); // log2(32)/5 = 1.0
2197
+ // importance 基线 (0~1)
2198
+ const importanceBase = memory.importance || 0.5;
2199
+ // 综合: importance 40% + frequency 30% + recency 30%
2200
+ return importanceBase * 0.4 + freqScore * 0.3 + timeDecay * 0.3;
2201
+ }
2202
+ /**
2203
+ * Phase-38 + Phase-39: RRF 融合记忆向量搜索结果和图谱扩展结果
2204
+ *
2205
+ * Phase-39 增强:热度加权融合 — 每条记忆的 RRF 分数乘以热度系数(0.5~1.5),
2206
+ * 高频+近期记忆获得排序提升,冷门记忆自动下沉。
2207
+ */
2208
+ rrfMergeMemories(vectorResults, graphResults, limit) {
2209
+ const k_vector = 20; // 向量搜索 RRF 常数(权重更高)
2210
+ const k_graph = 40; // 图谱扩展 RRF 常数
2211
+ const scoreMap = new Map();
2212
+ for (let i = 0; i < vectorResults.length; i++) {
2213
+ const item = vectorResults[i];
2214
+ const rrfScore = 1.0 / (k_vector + i + 1);
2215
+ scoreMap.set(item.id, { item, rrfScore });
2216
+ }
2217
+ for (let i = 0; i < graphResults.length; i++) {
2218
+ const item = graphResults[i];
2219
+ const rrfScore = 1.0 / (k_graph + i + 1);
2220
+ const existing = scoreMap.get(item.id);
2221
+ if (existing) {
2222
+ existing.rrfScore += rrfScore;
2223
+ }
2224
+ else {
2225
+ scoreMap.set(item.id, { item, rrfScore });
2226
+ }
2227
+ }
2228
+ // Phase-39: 热度加权 — hotness 映射到 [0.5, 1.5] 的乘法系数
2229
+ return Array.from(scoreMap.values())
2230
+ .map(({ item, rrfScore }) => {
2231
+ const hotness = this.computeMemoryHotness(item);
2232
+ const hotnessMultiplier = 0.5 + hotness; // [0.5, 1.5]
2233
+ return { item, rrfScore: rrfScore * hotnessMultiplier };
2234
+ })
2235
+ .sort((a, b) => b.rrfScore - a.rrfScore)
2236
+ .slice(0, limit)
2237
+ .map(({ item, rrfScore }) => ({ ...item, score: rrfScore }));
2238
+ }
2239
+ /**
2240
+ * 列出记忆(支持过滤)
2241
+ */
2242
+ listMemories(filter) {
2243
+ // 如果按关联任务过滤,通过关系查询
2244
+ if (filter?.relatedTaskId) {
2245
+ return this.getTaskRelatedMemories(filter.relatedTaskId, filter);
2246
+ }
2247
+ const entities = this.findEntitiesByType(ET.MEMORY);
2248
+ let memories = entities
2249
+ .filter((e) => {
2250
+ const p = e.properties;
2251
+ if (p.projectName !== this.projectName)
2252
+ return false;
2253
+ if (filter?.memoryType && p.memoryType !== filter.memoryType)
2254
+ return false;
2255
+ return true;
2256
+ })
2257
+ .map((e) => this.entityToMemory(e));
2258
+ // 按 updatedAt 降序排列(最新的在前)
2259
+ memories.sort((a, b) => b.updatedAt - a.updatedAt);
2260
+ if (filter?.limit && filter.limit > 0) {
2261
+ memories = memories.slice(0, filter.limit);
2262
+ }
2263
+ return memories;
2264
+ }
2265
+ /**
2266
+ * 删除一条记忆
2267
+ */
2268
+ deleteMemory(memoryId) {
2269
+ try {
2270
+ const entity = this.graph.getEntity(memoryId);
2271
+ if (!entity || entity.entity_type !== ET.MEMORY)
2272
+ return false;
2273
+ const p = entity.properties;
2274
+ if (p.projectName !== this.projectName)
2275
+ return false;
2276
+ this.graph.deleteEntity(memoryId);
2277
+ this.graph.flush();
2278
+ return true;
2279
+ }
2280
+ catch {
2281
+ return false;
2282
+ }
2283
+ }
2284
+ /**
2285
+ * 批量清除当前项目的所有记忆
2286
+ *
2287
+ * @param memoryType - 可选:仅清除指定类型的记忆。省略则清除所有。
2288
+ * @returns { deleted: number } 实际删除的数量
2289
+ */
2290
+ clearAllMemories(memoryType) {
2291
+ const entities = this.findEntitiesByType(ET.MEMORY);
2292
+ const toDelete = entities.filter((e) => {
2293
+ const p = e.properties;
2294
+ if (p.projectName !== this.projectName)
2295
+ return false;
2296
+ if (memoryType && p.memoryType !== memoryType)
2297
+ return false;
2298
+ return true;
2299
+ });
2300
+ let deleted = 0;
2301
+ for (const e of toDelete) {
2302
+ try {
2303
+ this.graph.deleteEntity(e.id);
2304
+ deleted++;
2305
+ }
2306
+ catch {
2307
+ // 跳过单条删除失败
2308
+ }
2309
+ }
2310
+ if (deleted > 0) {
2311
+ this.graph.flush();
2312
+ }
2313
+ return { deleted };
2314
+ }
2315
+ /**
2316
+ * 获取新会话上下文 — 核心工具
2317
+ *
2318
+ * 聚合以下信息为 Cursor 提供全面的项目上下文:
2319
+ * 1. 最近的 in_progress / completed 主任务
2320
+ * 2. 与查询相关的记忆(语义召回)
2321
+ * 3. 所有 preference 类型记忆
2322
+ * 4. 最近的 decision 类型记忆
2323
+ */
2324
+ getMemoryContext(query, maxMemories) {
2325
+ const limit = maxMemories || 10;
2326
+ // 1. 最近任务
2327
+ const allTasks = this.listMainTasks();
2328
+ const recentTasks = allTasks
2329
+ .filter((t) => t.status === 'in_progress' || t.status === 'completed')
2330
+ .sort((a, b) => (b.completedAt || b.updatedAt) - (a.completedAt || a.updatedAt))
2331
+ .slice(0, 5)
2332
+ .map((t) => ({
2333
+ taskId: t.taskId,
2334
+ title: t.title,
2335
+ status: t.status,
2336
+ completedAt: t.completedAt,
2337
+ }));
2338
+ // 2. 相关记忆(统一召回:同时搜索记忆 + 文档)
2339
+ let relevantMemories = [];
2340
+ if (query) {
2341
+ relevantMemories = this.recallMemory(query, { limit, includeDocs: true });
2342
+ }
2343
+ // 3. 项目偏好
2344
+ const projectPreferences = this.listMemories({ memoryType: 'preference' });
2345
+ // 4. 最近决策
2346
+ const recentDecisions = this.listMemories({ memoryType: 'decision', limit: 5 });
2347
+ // 5. 总记忆数
2348
+ const allMemories = this.findEntitiesByType(ET.MEMORY);
2349
+ const totalMemories = allMemories.filter((e) => e.properties.projectName === this.projectName).length;
2350
+ // 6. 关键文档摘要(自动纳入 overview / core_concepts)
2351
+ const relatedDocs = [];
2352
+ const KEY_SECTIONS = ['overview', 'core_concepts'];
2353
+ const DOC_SUMMARY_MAX_LEN = 500;
2354
+ try {
2355
+ const allSections = this.listSections();
2356
+ for (const sec of allSections) {
2357
+ if (KEY_SECTIONS.includes(sec.section)) {
2358
+ const doc = this.getSection(sec.section, sec.subSection);
2359
+ if (doc && doc.content) {
2360
+ const summary = doc.content.length > DOC_SUMMARY_MAX_LEN
2361
+ ? doc.content.substring(0, DOC_SUMMARY_MAX_LEN) + '...'
2362
+ : doc.content;
2363
+ relatedDocs.push({
2364
+ section: doc.section,
2365
+ subSection: doc.subSection || undefined,
2366
+ title: doc.title,
2367
+ summary,
2368
+ });
2369
+ }
2370
+ }
2371
+ }
2372
+ }
2373
+ catch (e) {
2374
+ // 非关键路径,忽略错误
2375
+ }
2376
+ // 7. Phase-38: 模块级关联记忆 — 图谱遍历 in_progress 任务 → 模块 → MODULE_MEMORY
2377
+ const moduleMemories = this.getModuleMemoriesFromActiveTasks(recentTasks);
2378
+ // 8. Phase-40: 记忆主题集群概览(仅摘要,不含完整记忆)
2379
+ let memoryClusters;
2380
+ try {
2381
+ const clusters = this.getMemoryClusters();
2382
+ // 只保留有意义的集群(2条以上记忆,排除 uncategorized)
2383
+ const significantClusters = clusters
2384
+ .filter((c) => c.clusterId > 0 && c.memoryCount >= 2)
2385
+ .map((c) => ({
2386
+ clusterId: c.clusterId,
2387
+ theme: c.theme,
2388
+ memoryCount: c.memoryCount,
2389
+ topMemoryTypes: c.topMemoryTypes,
2390
+ }));
2391
+ if (significantClusters.length > 0) {
2392
+ memoryClusters = significantClusters;
2393
+ }
2394
+ }
2395
+ catch (_e) {
2396
+ // 非关键路径,忽略错误
2397
+ }
2398
+ return {
2399
+ projectName: this.projectName,
2400
+ recentTasks,
2401
+ relevantMemories,
2402
+ projectPreferences,
2403
+ recentDecisions,
2404
+ totalMemories,
2405
+ relatedDocs: relatedDocs.length > 0 ? relatedDocs : undefined,
2406
+ moduleMemories: moduleMemories.length > 0 ? moduleMemories : undefined,
2407
+ memoryClusters,
2408
+ };
2409
+ }
2410
+ /**
2411
+ * Phase-38: 从进行中的任务出发,通过图谱遍历获取模块级记忆
2412
+ *
2413
+ * 路径: in_progress task → MODULE_HAS_TASK(反向) → module → MODULE_MEMORY → memories
2414
+ */
2415
+ getModuleMemoriesFromActiveTasks(recentTasks) {
2416
+ const results = [];
2417
+ const seenModuleIds = new Set();
2418
+ // 只从 in_progress 任务出发
2419
+ const activeTasks = recentTasks.filter((t) => t.status === 'in_progress');
2420
+ for (const task of activeTasks) {
2421
+ const taskEntity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', task.taskId);
2422
+ if (!taskEntity)
2423
+ continue;
2424
+ // 查找该任务所属的模块(反向: module → MODULE_HAS_TASK → task)
2425
+ const inRels = this.getInRelations(taskEntity.id, RT.MODULE_HAS_TASK);
2426
+ for (const rel of inRels) {
2427
+ const moduleEntity = this.graph.getEntity(rel.source);
2428
+ if (!moduleEntity)
2429
+ continue;
2430
+ const mp = moduleEntity.properties;
2431
+ const moduleId = mp.moduleId;
2432
+ if (!moduleId || seenModuleIds.has(moduleId))
2433
+ continue;
2434
+ seenModuleIds.add(moduleId);
2435
+ // 获取模块下的所有记忆(module → MODULE_MEMORY → memory)
2436
+ const memRels = this.getOutRelations(moduleEntity.id, RT.MODULE_MEMORY);
2437
+ const memories = [];
2438
+ for (const memRel of memRels) {
2439
+ const memEntity = this.graph.getEntity(memRel.target);
2440
+ if (!memEntity)
2441
+ continue;
2442
+ const memP = memEntity.properties;
2443
+ if (memP.projectName !== this.projectName)
2444
+ continue;
2445
+ memories.push(this.entityToMemory(memEntity));
2446
+ }
2447
+ if (memories.length > 0) {
2448
+ results.push({
2449
+ moduleId,
2450
+ moduleName: mp.name || moduleId,
2451
+ memories: memories.slice(0, 10), // 每模块最多10条
2452
+ });
2453
+ }
2454
+ }
2455
+ }
2456
+ return results;
2457
+ }
2458
+ // ============================================================================
2459
+ // Phase-40: 记忆主题集群 — 基于 MEMORY_RELATES 连通分量聚类
2460
+ // ============================================================================
2461
+ /**
2462
+ * Phase-40: 获取记忆主题集群
2463
+ *
2464
+ * 基于 MEMORY_RELATES 关系图的连通分量检测,自动将语义关联的记忆
2465
+ * 聚合为主题集群。每个集群包含一个自动生成的主题标签和成员记忆列表。
2466
+ *
2467
+ * 算法:BFS 连通分量检测(适合中小规模记忆网络)
2468
+ */
2469
+ getMemoryClusters() {
2470
+ // 1. 获取所有本项目的记忆
2471
+ const allEntities = this.findEntitiesByType(ET.MEMORY);
2472
+ const projectMemories = allEntities.filter((e) => e.properties.projectName === this.projectName);
2473
+ if (projectMemories.length === 0)
2474
+ return [];
2475
+ // 2. 构建邻接表(基于 MEMORY_RELATES 关系)
2476
+ const adjacency = new Map();
2477
+ for (const mem of projectMemories) {
2478
+ adjacency.set(mem.id, new Set());
2479
+ }
2480
+ for (const mem of projectMemories) {
2481
+ const outRels = this.getOutRelations(mem.id, RT.MEMORY_RELATES);
2482
+ const inRels = this.getInRelations(mem.id, RT.MEMORY_RELATES);
2483
+ for (const rel of [...outRels, ...inRels]) {
2484
+ const neighborId = rel.source === mem.id ? rel.target : rel.source;
2485
+ if (adjacency.has(neighborId)) {
2486
+ adjacency.get(mem.id).add(neighborId);
2487
+ adjacency.get(neighborId).add(mem.id);
2488
+ }
2489
+ }
2490
+ }
2491
+ // 3. BFS 连通分量检测
2492
+ const visited = new Set();
2493
+ const components = [];
2494
+ for (const mem of projectMemories) {
2495
+ if (visited.has(mem.id))
2496
+ continue;
2497
+ const component = [];
2498
+ const queue = [mem.id];
2499
+ visited.add(mem.id);
2500
+ while (queue.length > 0) {
2501
+ const current = queue.shift();
2502
+ component.push(current);
2503
+ const neighbors = adjacency.get(current) || new Set();
2504
+ for (const neighbor of neighbors) {
2505
+ if (!visited.has(neighbor)) {
2506
+ visited.add(neighbor);
2507
+ queue.push(neighbor);
2508
+ }
2509
+ }
2510
+ }
2511
+ components.push(component);
2512
+ }
2513
+ // 4. 转化为集群结果(按大小降序排列,孤立记忆归入"未分类")
2514
+ const clusters = [];
2515
+ // 实体 ID → Memory 的快速查找
2516
+ const entityMap = new Map(projectMemories.map((e) => [e.id, e]));
2517
+ let clusterId = 0;
2518
+ const isolatedMemories = [];
2519
+ for (const component of components.sort((a, b) => b.length - a.length)) {
2520
+ const memories = component
2521
+ .map((id) => entityMap.get(id))
2522
+ .filter(Boolean)
2523
+ .map((e) => this.entityToMemory(e));
2524
+ if (component.length <= 1) {
2525
+ // 孤立记忆收集,后续归入"未分类"集群
2526
+ isolatedMemories.push(...memories);
2527
+ continue;
2528
+ }
2529
+ clusterId++;
2530
+ // 统计 memoryType 分布,取出现最多的类型
2531
+ const typeCounts = new Map();
2532
+ for (const m of memories) {
2533
+ typeCounts.set(m.memoryType, (typeCounts.get(m.memoryType) || 0) + 1);
2534
+ }
2535
+ const topMemoryTypes = Array.from(typeCounts.entries())
2536
+ .sort((a, b) => b[1] - a[1])
2537
+ .slice(0, 3)
2538
+ .map(([t]) => t);
2539
+ // 自动主题:取高频 tags + 高频 memoryType
2540
+ const tagCounts = new Map();
2541
+ for (const m of memories) {
2542
+ for (const tag of m.tags || []) {
2543
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
2544
+ }
2545
+ }
2546
+ const topTags = Array.from(tagCounts.entries())
2547
+ .sort((a, b) => b[1] - a[1])
2548
+ .slice(0, 3)
2549
+ .map(([t]) => t);
2550
+ const theme = topTags.length > 0
2551
+ ? topTags.join(' / ')
2552
+ : `${topMemoryTypes[0] || 'mixed'} cluster`;
2553
+ clusters.push({
2554
+ clusterId,
2555
+ theme,
2556
+ memoryCount: memories.length,
2557
+ topMemoryTypes,
2558
+ memories,
2559
+ });
2560
+ }
2561
+ // 孤立记忆归入"未分类"集群(如果有的话)
2562
+ if (isolatedMemories.length > 0) {
2563
+ clusters.push({
2564
+ clusterId: 0,
2565
+ theme: 'uncategorized',
2566
+ memoryCount: isolatedMemories.length,
2567
+ topMemoryTypes: ['mixed'],
2568
+ memories: isolatedMemories,
2569
+ });
2570
+ }
2571
+ return clusters;
2572
+ }
2573
+ /**
2574
+ * 获取任务关联的记忆
2575
+ */
2576
+ getTaskRelatedMemories(taskId, filter) {
2577
+ const taskEntity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', taskId);
2578
+ if (!taskEntity)
2579
+ return [];
2580
+ // 查找所有指向该任务的 memory_from_task 关系(反向查询)
2581
+ const allMemories = this.findEntitiesByType(ET.MEMORY);
2582
+ const memories = [];
2583
+ for (const e of allMemories) {
2584
+ const p = e.properties;
2585
+ if (p.projectName !== this.projectName)
2586
+ continue;
2587
+ if (p.relatedTaskId !== taskId)
2588
+ continue;
2589
+ if (filter?.memoryType && p.memoryType !== filter.memoryType)
2590
+ continue;
2591
+ memories.push(this.entityToMemory(e));
2592
+ }
2593
+ memories.sort((a, b) => b.updatedAt - a.updatedAt);
2594
+ return memories;
2595
+ }
2596
+ /**
2597
+ * Entity → Memory 转换
2598
+ */
2599
+ entityToMemory(e) {
2600
+ const p = e.properties;
2601
+ return {
2602
+ id: e.id,
2603
+ projectName: this.projectName,
2604
+ memoryType: p.memoryType || 'insight',
2605
+ content: p.content || '',
2606
+ tags: p.tags || [],
2607
+ relatedTaskId: p.relatedTaskId || undefined,
2608
+ sourceId: p.sourceId || undefined,
2609
+ importance: p.importance ?? 0.5,
2610
+ hitCount: p.hitCount || 0,
2611
+ lastAccessedAt: p.lastAccessedAt || null,
2612
+ createdAt: p.createdAt || e.created_at,
2613
+ updatedAt: p.updatedAt || p.createdAt || e.created_at,
2614
+ };
2615
+ }
2616
+ // ==========================================================================
2617
+ // Memory Generation (从文档/任务提取记忆候选项)
2618
+ // ==========================================================================
2619
+ /**
2620
+ * 记忆生成器 — 从已有文档和已完成任务中提取记忆候选项
2621
+ *
2622
+ * 聚合逻辑:
2623
+ * 1. 任务: 每个已完成阶段 → 1 条 summary 候选项(phase 标题 + 子任务列表)
2624
+ * 2. 文档: 每篇文档 → 1 条候选项(section 类型 → 建议 memoryType 映射)
2625
+ * 3. 去重: 检查已有记忆,已有记忆的候选项直接跳过不返回
2626
+ *
2627
+ * AI 收到候选项后,分析 content 并调用 devplan_memory_save 批量生成记忆。
2628
+ */
2629
+ // ============================================================================
2630
+ // Phase-44: 记忆生命周期管理 — DynamicNode promote/demote
2631
+ // ============================================================================
2632
+ /**
2633
+ * Phase-44: 运行记忆生命周期扫描 — 基于 DynamicNode 的 promote/demote
2634
+ *
2635
+ * 调用 SocialGraphV2 的 dynamicScan 对记忆实体执行生命周期管理:
2636
+ * - promote: 长期未访问的高热度记忆恢复活跃
2637
+ * - demote: 长期未召回的低价值记忆降级为 shadow 状态
2638
+ * - shadow 记忆不参与向量搜索,但仍保留在图谱中
2639
+ * - 被重新访问时通过 dynamicPromote 恢复
2640
+ *
2641
+ * @param config 配置选项
2642
+ * @returns 扫描报告(promoted/demoted/scanned 计数)
2643
+ */
2644
+ runMemoryLifecycle(config) {
2645
+ const g = this.graph;
2646
+ if (typeof g.dynamicScan !== 'function') {
2647
+ return null; // aifastdb 版本不支持
2648
+ }
2649
+ try {
2650
+ const result = g.dynamicScan({
2651
+ promote_hit_threshold: config?.promoteHitThreshold ?? 3,
2652
+ demote_idle_timeout_secs: config?.demoteIdleTimeoutSecs ?? 2592000, // 30 天
2653
+ scan_entity_types: [ET.MEMORY],
2654
+ preserve_entity_types: [ET.PROJECT, ET.DOC, ET.MAIN_TASK, ET.SUB_TASK, ET.MODULE],
2655
+ });
2656
+ if (result && result.demoted > 0) {
2657
+ // demoted 的记忆实体需要从向量索引中移除
2658
+ // 以防止 shadow 记忆出现在搜索结果中
2659
+ const allMemories = this.findEntitiesByType(ET.MEMORY);
2660
+ for (const mem of allMemories) {
2661
+ const p = mem.properties;
2662
+ if (p.projectName !== this.projectName)
2663
+ continue;
2664
+ if (typeof g.isShadowEntity === 'function' && g.isShadowEntity(mem.id)) {
2665
+ try {
2666
+ this.graph.removeEntityVector(mem.id);
2667
+ }
2668
+ catch { /* entity might not have vector */ }
2669
+ }
2670
+ }
2671
+ }
2672
+ return {
2673
+ promoted: result?.promoted ?? 0,
2674
+ demoted: result?.demoted ?? 0,
2675
+ scanned: result?.scanned ?? 0,
2676
+ durationMs: result?.duration_ms ?? 0,
2677
+ };
2678
+ }
2679
+ catch (e) {
2680
+ console.warn(`[DevPlan] runMemoryLifecycle failed: ${e instanceof Error ? e.message : String(e)}`);
2681
+ return null;
2682
+ }
2683
+ }
2684
+ generateMemoryCandidates(options) {
2685
+ const source = options?.source || 'both';
2686
+ const limit = options?.limit || 50;
2687
+ // 获取已有记忆,用于去重检查
2688
+ const existingMemories = this.listMemories ? this.listMemories() : [];
2689
+ const memoryByTaskId = new Set();
2690
+ const memoryBySourceId = new Set();
2691
+ const memoryContentSet = new Set();
2692
+ for (const m of existingMemories) {
2693
+ if (m.relatedTaskId)
2694
+ memoryByTaskId.add(m.relatedTaskId);
2695
+ // Phase-35: sourceId 去重 — 最可靠的匹配方式
2696
+ if (m.sourceId)
2697
+ memoryBySourceId.add(m.sourceId);
2698
+ // 用前 50 字符作内容指纹(fallback,对旧记忆仍有效)
2699
+ memoryContentSet.add(m.content.slice(0, 50).toLowerCase());
2700
+ }
2701
+ const allEligible = [];
2702
+ let totalCompletedPhases = 0;
2703
+ let totalDocuments = 0;
2704
+ let phasesWithMemory = 0;
2705
+ let docsWithMemory = 0;
2706
+ let skippedWithMemory = 0;
2707
+ // ========== 任务来源 ==========
2708
+ if (source === 'tasks' || source === 'both') {
2709
+ const allTasks = this.listMainTasks();
2710
+ const completedTasks = options?.taskId
2711
+ ? allTasks.filter((t) => t.taskId === options.taskId)
2712
+ : allTasks.filter((t) => t.status === 'completed');
2713
+ totalCompletedPhases = completedTasks.length;
2714
+ for (const task of completedTasks) {
2715
+ // Phase-35: 三重去重检查 — sourceId > relatedTaskId > tags
2716
+ const hasMemory = memoryBySourceId.has(task.taskId) || memoryByTaskId.has(task.taskId);
2717
+ if (hasMemory) {
2718
+ phasesWithMemory++;
2719
+ skippedWithMemory++;
2720
+ continue; // 已有记忆 → 跳过不列出
2721
+ }
2722
+ // 聚合子任务列表
2723
+ const subTasks = this.listSubTasks(task.taskId);
2724
+ const subTaskLines = subTasks
2725
+ .map((st) => ` - [${st.status === 'completed' ? '✅' : '⬜'}] ${st.taskId}: ${st.title}`)
2726
+ .join('\n');
2727
+ const content = [
2728
+ `## ${task.title}`,
2729
+ `- 状态: ${task.status}`,
2730
+ `- 优先级: ${task.priority}`,
2731
+ task.description ? `- 描述: ${task.description}` : '',
2732
+ task.estimatedHours ? `- 预计工时: ${task.estimatedHours}h` : '',
2733
+ task.completedAt ? `- 完成时间: ${new Date(task.completedAt).toISOString().slice(0, 10)}` : '',
2734
+ `- 子任务 (${task.completedSubtasks}/${task.totalSubtasks}):`,
2735
+ subTaskLines,
2736
+ ].filter(Boolean).join('\n');
2737
+ // 推断重要性:P0 高优先级 → 0.8, P1 → 0.6, P2 → 0.5
2738
+ const importanceMap = { P0: 0.8, P1: 0.6, P2: 0.5 };
2739
+ allEligible.push({
2740
+ sourceType: 'task',
2741
+ sourceId: task.taskId,
2742
+ sourceTitle: task.title,
2743
+ content,
2744
+ suggestedMemoryType: 'summary',
2745
+ suggestedImportance: importanceMap[task.priority] || 0.6,
2746
+ suggestedTags: [task.taskId, task.priority],
2747
+ hasExistingMemory: false,
2748
+ });
2749
+ }
2750
+ }
2751
+ // ========== 文档来源 ==========
2752
+ if (source === 'docs' || source === 'both') {
2753
+ let docs = this.listSections();
2754
+ // 可选过滤
2755
+ if (options?.section) {
2756
+ docs = docs.filter((d) => d.section === options.section);
2757
+ if (options?.subSection) {
2758
+ docs = docs.filter((d) => d.subSection === options.subSection);
2759
+ }
2760
+ }
2761
+ totalDocuments = docs.length;
2762
+ // section → suggestedMemoryType 映射
2763
+ const sectionToMemoryType = {
2764
+ overview: 'summary',
2765
+ core_concepts: 'pattern',
2766
+ api_design: 'pattern',
2767
+ file_structure: 'pattern',
2768
+ config: 'preference',
2769
+ examples: 'pattern',
2770
+ technical_notes: 'insight',
2771
+ api_endpoints: 'pattern',
2772
+ milestones: 'summary',
2773
+ changelog: 'summary',
2774
+ custom: 'insight',
2775
+ };
2776
+ // section → suggestedImportance 映射
2777
+ const sectionToImportance = {
2778
+ overview: 0.7,
2779
+ core_concepts: 0.8,
2780
+ api_design: 0.7,
2781
+ file_structure: 0.5,
2782
+ config: 0.5,
2783
+ examples: 0.4,
2784
+ technical_notes: 0.7,
2785
+ api_endpoints: 0.5,
2786
+ milestones: 0.6,
2787
+ changelog: 0.4,
2788
+ custom: 0.5,
2789
+ };
2790
+ for (const doc of docs) {
2791
+ const docKey = doc.subSection
2792
+ ? `${doc.section}|${doc.subSection}`
2793
+ : doc.section;
2794
+ // Phase-35: 三重去重检查 — sourceId(最可靠) > 内容指纹(fallback)
2795
+ const hasMemoryBySourceId = memoryBySourceId.has(docKey);
2796
+ const contentFingerprint = doc.content.slice(0, 50).toLowerCase();
2797
+ const hasMemoryByFingerprint = memoryContentSet.has(contentFingerprint);
2798
+ const hasMemory = hasMemoryBySourceId || hasMemoryByFingerprint;
2799
+ if (hasMemory) {
2800
+ docsWithMemory++;
2801
+ skippedWithMemory++;
2802
+ continue; // 已有记忆 → 跳过不列出
2803
+ }
2804
+ // 截取内容摘要(最多 800 字符,保留完整行)
2805
+ let contentPreview = doc.content;
2806
+ if (contentPreview.length > 800) {
2807
+ contentPreview = contentPreview.slice(0, 800);
2808
+ const lastNewline = contentPreview.lastIndexOf('\n');
2809
+ if (lastNewline > 400)
2810
+ contentPreview = contentPreview.slice(0, lastNewline);
2811
+ contentPreview += '\n... (内容截断,完整文档可通过 devplan_get_section 获取)';
2812
+ }
2813
+ const content = [
2814
+ `## ${doc.title}`,
2815
+ `- 文档类型: ${doc.section}${doc.subSection ? ' → ' + doc.subSection : ''}`,
2816
+ `- 版本: ${doc.version}`,
2817
+ doc.relatedTaskIds?.length
2818
+ ? `- 关联任务: ${doc.relatedTaskIds.join(', ')}`
2819
+ : '',
2820
+ `\n### 内容摘要\n`,
2821
+ contentPreview,
2822
+ ].filter(Boolean).join('\n');
2823
+ const suggestedType = sectionToMemoryType[doc.section] || 'insight';
2824
+ const suggestedImportance = sectionToImportance[doc.section] || 0.5;
2825
+ // 从标题推断更精确的类型
2826
+ const titleLower = doc.title.toLowerCase();
2827
+ let refinedType = suggestedType;
2828
+ if (titleLower.includes('决策') || titleLower.includes('decision') || titleLower.includes('选择'))
2829
+ refinedType = 'decision';
2830
+ else if (titleLower.includes('修复') || titleLower.includes('fix') || titleLower.includes('bug'))
2831
+ refinedType = 'bugfix';
2832
+ else if (titleLower.includes('优化') || titleLower.includes('性能') || titleLower.includes('performance'))
2833
+ refinedType = 'insight';
2834
+ const tags = [doc.section];
2835
+ if (doc.subSection)
2836
+ tags.push(doc.subSection);
2837
+ if (doc.relatedTaskIds)
2838
+ tags.push(...doc.relatedTaskIds);
2839
+ allEligible.push({
2840
+ sourceType: 'document',
2841
+ sourceId: docKey,
2842
+ sourceTitle: doc.title,
2843
+ content,
2844
+ suggestedMemoryType: refinedType,
2845
+ suggestedImportance: suggestedImportance,
2846
+ suggestedTags: tags,
2847
+ hasExistingMemory: false,
2848
+ });
2849
+ }
2850
+ }
2851
+ // ========== Phase-44: 构建 suggestedRelations ==========
2852
+ // 为候选项之间推断关联关系,帮助 AI 用 applyMutations 构建记忆子图
2853
+ const candidateIdSet = new Set(allEligible.map(c => c.sourceId));
2854
+ for (const candidate of allEligible) {
2855
+ const relations = [];
2856
+ if (candidate.sourceType === 'task') {
2857
+ // 任务 → 关联文档: DERIVED_FROM
2858
+ const taskEntity = this.listMainTasks().find(t => t.taskId === candidate.sourceId);
2859
+ if (taskEntity?.relatedSections) {
2860
+ for (const docRef of taskEntity.relatedSections) {
2861
+ if (candidateIdSet.has(docRef)) {
2862
+ relations.push({ targetSourceId: docRef, relationType: 'mem:DERIVED_FROM', weight: 0.7, reason: 'task references this document' });
2863
+ }
2864
+ }
2865
+ }
2866
+ // 任务 → 同模块任务: RELATES
2867
+ if (taskEntity?.moduleId) {
2868
+ for (const other of allEligible) {
2869
+ if (other.sourceId !== candidate.sourceId && other.sourceType === 'task') {
2870
+ const otherTask = this.listMainTasks().find(t => t.taskId === other.sourceId);
2871
+ if (otherTask?.moduleId === taskEntity.moduleId) {
2872
+ relations.push({ targetSourceId: other.sourceId, relationType: 'mem:RELATES', weight: 0.5, reason: `same module: ${taskEntity.moduleId}` });
2873
+ }
2874
+ }
2875
+ }
2876
+ }
2877
+ // 任务 → 相邻阶段: TEMPORAL_NEXT (phase-N → phase-N+1)
2878
+ const phaseMatch = candidate.sourceId.match(/^phase-(\d+)/);
2879
+ if (phaseMatch) {
2880
+ const nextPhaseId = `phase-${parseInt(phaseMatch[1]) + 1}`;
2881
+ if (candidateIdSet.has(nextPhaseId)) {
2882
+ relations.push({ targetSourceId: nextPhaseId, relationType: 'mem:TEMPORAL_NEXT', weight: 0.6, reason: 'consecutive phases' });
2883
+ }
2884
+ }
2885
+ }
2886
+ else if (candidate.sourceType === 'document') {
2887
+ // 文档 → 关联任务: DERIVED_FROM
2888
+ const docEntity = this.listSections().find(d => {
2889
+ const key = d.subSection ? `${d.section}|${d.subSection}` : d.section;
2890
+ return key === candidate.sourceId;
2891
+ });
2892
+ if (docEntity?.relatedTaskIds) {
2893
+ for (const taskId of docEntity.relatedTaskIds) {
2894
+ if (candidateIdSet.has(taskId)) {
2895
+ relations.push({ targetSourceId: taskId, relationType: 'mem:DERIVED_FROM', weight: 0.7, reason: 'document references this task' });
2896
+ }
2897
+ }
2898
+ }
2899
+ // 文档 → 父文档: CONTAINS (反向)
2900
+ if (docEntity?.parentDoc) {
2901
+ if (candidateIdSet.has(docEntity.parentDoc)) {
2902
+ relations.push({ targetSourceId: docEntity.parentDoc, relationType: 'mem:CONTAINS', weight: 0.8, reason: 'child document of parent' });
2903
+ }
2904
+ }
2905
+ // 文档 → 同section文档: RELATES
2906
+ if (docEntity) {
2907
+ for (const other of allEligible) {
2908
+ if (other.sourceId !== candidate.sourceId && other.sourceType === 'document') {
2909
+ const otherDoc = this.listSections().find(d => {
2910
+ const key = d.subSection ? `${d.section}|${d.subSection}` : d.section;
2911
+ return key === other.sourceId;
2912
+ });
2913
+ if (otherDoc && otherDoc.section === docEntity.section && otherDoc.subSection !== docEntity.subSection) {
2914
+ relations.push({ targetSourceId: other.sourceId, relationType: 'mem:RELATES', weight: 0.4, reason: `same section: ${docEntity.section}` });
2915
+ }
2916
+ }
2917
+ }
2918
+ }
2919
+ }
2920
+ if (relations.length > 0) {
2921
+ candidate.suggestedRelations = relations;
2922
+ }
2923
+ }
2924
+ // 按 limit 截取,计算 remaining
2925
+ const candidates = allEligible.slice(0, limit);
2926
+ const remaining = Math.max(0, allEligible.length - limit);
2927
+ return {
2928
+ candidates,
2929
+ stats: {
2930
+ totalCompletedPhases,
2931
+ totalDocuments,
2932
+ phasesWithMemory,
2933
+ docsWithMemory,
2934
+ skippedWithMemory,
2935
+ candidatesReturned: candidates.length,
2936
+ remaining,
2937
+ },
2938
+ };
2939
+ }
2940
+ // ==========================================================================
2941
+ // Utility
2942
+ // ==========================================================================
2943
+ /**
2944
+ * Phase-21: 清理 WAL 中的重复 Entity。
2945
+ *
2946
+ * 扫描所有实体类型,按业务键去重,删除多余(低优先级)的 Entity。
2947
+ * 返回被清理的 Entity 数量和详情。
2948
+ *
2949
+ * @param dryRun - 若为 true,仅报告而不实际删除
2950
+ */
2951
+ cleanupDuplicates(dryRun = false) {
2952
+ const details = [];
2953
+ // 定义各实体类型的去重键
2954
+ const typeKeyMap = [
2955
+ { entityType: ET.MAIN_TASK, propKey: 'taskId' },
2956
+ { entityType: ET.SUB_TASK, propKey: 'taskId' },
2957
+ { entityType: ET.MODULE, propKey: 'moduleId' },
2958
+ ];
2959
+ for (const { entityType, propKey } of typeKeyMap) {
2960
+ const duplicates = this.findDuplicateEntities(entityType, propKey);
2961
+ for (const dup of duplicates) {
2962
+ const propVal = dup.properties?.[propKey] || 'unknown';
2963
+ // 找出胜者
2964
+ const entities = this.findEntitiesByType(entityType).filter((e) => e.properties?.[propKey] === propVal);
2965
+ const winner = this.deduplicateEntities(entities, propKey)[0];
2966
+ details.push({
2967
+ entityType,
2968
+ propKey: `${propKey}=${propVal}`,
2969
+ duplicateId: dup.id,
2970
+ keptId: winner?.id || 'unknown',
2971
+ });
2972
+ if (!dryRun) {
2973
+ // 删除重复 Entity 的所有关系
2974
+ const relations = this.graph.getEntityRelations(dup.id);
2975
+ for (const rel of relations) {
2976
+ this.graph.deleteRelation(rel.id);
2977
+ }
2978
+ // 删除重复 Entity
2979
+ this.graph.deleteEntity(dup.id);
2980
+ }
2981
+ }
2982
+ }
2983
+ // 文档去重(按 section+subSection 组合键)
2984
+ const docEntities = this.findEntitiesByType(ET.DOC);
2985
+ const docGroups = new Map();
2986
+ for (const e of docEntities) {
2987
+ const p = e.properties;
2988
+ const key = sectionKey(p.section, p.subSection || undefined);
2989
+ if (!docGroups.has(key))
2990
+ docGroups.set(key, []);
2991
+ docGroups.get(key).push(e);
2992
+ }
2993
+ for (const [key, group] of docGroups) {
2994
+ if (group.length <= 1)
2995
+ continue;
2996
+ // 保留 updatedAt 最新的
2997
+ group.sort((a, b) => (Number(b.properties?.updatedAt) || 0) - (Number(a.properties?.updatedAt) || 0));
2998
+ const winner = group[0];
2999
+ for (let i = 1; i < group.length; i++) {
3000
+ details.push({
3001
+ entityType: ET.DOC,
3002
+ propKey: `section=${key}`,
3003
+ duplicateId: group[i].id,
3004
+ keptId: winner.id,
3005
+ });
3006
+ if (!dryRun) {
3007
+ const relations = this.graph.getEntityRelations(group[i].id);
3008
+ for (const rel of relations) {
3009
+ this.graph.deleteRelation(rel.id);
3010
+ }
3011
+ this.graph.deleteEntity(group[i].id);
3012
+ }
3013
+ }
3014
+ }
3015
+ if (!dryRun && details.length > 0) {
3016
+ this.graph.flush();
3017
+ }
3018
+ return { cleaned: details.length, details };
3019
+ }
3020
+ sync() {
3021
+ this.graph.flush();
3022
+ }
3023
+ getProjectName() {
3024
+ return this.projectName;
3025
+ }
3026
+ syncWithGit(dryRun = false) {
3027
+ const currentHead = this.getCurrentGitCommit();
3028
+ if (!currentHead) {
3029
+ return {
3030
+ checked: 0,
3031
+ reverted: [],
3032
+ currentHead: 'unknown',
3033
+ error: 'Git not available or not in a Git repository',
3034
+ };
3035
+ }
3036
+ const mainTasks = this.listMainTasks();
3037
+ const reverted = [];
3038
+ let checked = 0;
3039
+ for (const mt of mainTasks) {
3040
+ const subs = this.listSubTasks(mt.taskId);
3041
+ for (const sub of subs) {
3042
+ if (sub.status !== 'completed' || !sub.completedAtCommit)
3043
+ continue;
3044
+ checked++;
3045
+ if (!this.isAncestor(sub.completedAtCommit, currentHead)) {
3046
+ const reason = `Commit ${sub.completedAtCommit} not found in current branch (HEAD: ${currentHead})`;
3047
+ if (!dryRun) {
3048
+ this.updateSubTaskStatus(sub.taskId, 'pending', { revertReason: reason });
3049
+ this.refreshMainTaskCounts(sub.parentTaskId);
3050
+ const parentMain = this.getMainTask(sub.parentTaskId);
3051
+ if (parentMain && parentMain.status === 'completed') {
3052
+ this.updateMainTaskStatus(sub.parentTaskId, 'in_progress');
3053
+ }
3054
+ }
3055
+ reverted.push({
3056
+ taskId: sub.taskId,
3057
+ title: sub.title,
3058
+ parentTaskId: sub.parentTaskId,
3059
+ completedAtCommit: sub.completedAtCommit,
3060
+ reason,
3061
+ });
3062
+ }
3063
+ }
3064
+ }
3065
+ return { checked, reverted, currentHead };
3066
+ }
3067
+ // ==========================================================================
3068
+ // Graph Export (核心差异能力)
3069
+ // ==========================================================================
3070
+ /**
3071
+ * 导出 DevPlan 的图结构用于可视化
3072
+ *
3073
+ * 返回 vis-network 兼容的 { nodes, edges } 格式。
3074
+ */
3075
+ exportGraph(options) {
3076
+ const includeDocuments = options?.includeDocuments !== false;
3077
+ const includeModules = options?.includeModules !== false;
3078
+ const includeNodeDegree = options?.includeNodeDegree !== false;
3079
+ const enableBackendDegreeFallback = options?.enableBackendDegreeFallback !== false;
3080
+ const includePrompts = options?.includePrompts !== false;
3081
+ const nodes = [];
3082
+ const edges = [];
3083
+ // 项目根节点
3084
+ nodes.push({
3085
+ id: this.getProjectId(),
3086
+ label: this.projectName,
3087
+ type: 'project',
3088
+ properties: { entityType: ET.PROJECT },
3089
+ });
3090
+ // 主任务节点
3091
+ const mainTasks = this.listMainTasks();
3092
+ for (const mt of mainTasks) {
3093
+ nodes.push({
3094
+ id: mt.id,
3095
+ label: mt.title,
3096
+ type: 'main-task',
3097
+ properties: {
3098
+ taskId: mt.taskId,
3099
+ priority: mt.priority,
3100
+ status: mt.status,
3101
+ totalSubtasks: mt.totalSubtasks,
3102
+ completedSubtasks: mt.completedSubtasks,
3103
+ completedAt: mt.completedAt || null,
3104
+ },
3105
+ });
3106
+ edges.push({
3107
+ from: this.getProjectId(),
3108
+ to: mt.id,
3109
+ label: RT.HAS_MAIN_TASK,
3110
+ });
3111
+ // 子任务节点
3112
+ const subTasks = this.listSubTasks(mt.taskId);
3113
+ for (const st of subTasks) {
3114
+ nodes.push({
3115
+ id: st.id,
3116
+ label: st.title,
3117
+ type: 'sub-task',
3118
+ properties: {
3119
+ taskId: st.taskId,
3120
+ parentTaskId: st.parentTaskId,
3121
+ status: st.status,
3122
+ completedAt: st.completedAt || null,
3123
+ },
3124
+ });
3125
+ edges.push({
3126
+ from: mt.id,
3127
+ to: st.id,
3128
+ label: RT.HAS_SUB_TASK,
3129
+ });
3130
+ }
3131
+ // task -> doc 关系
3132
+ const taskDocRels = this.getOutRelations(mt.id, RT.TASK_HAS_DOC);
3133
+ for (const rel of taskDocRels) {
3134
+ edges.push({
3135
+ from: mt.id,
3136
+ to: rel.target,
3137
+ label: RT.TASK_HAS_DOC,
3138
+ });
3139
+ }
3140
+ // task -> prompt 关系
3141
+ if (includePrompts) {
3142
+ const taskPromptRels = this.getOutRelations(mt.id, RT.TASK_HAS_PROMPT);
3143
+ for (const rel of taskPromptRels) {
3144
+ edges.push({
3145
+ from: mt.id,
3146
+ to: rel.target,
3147
+ label: RT.TASK_HAS_PROMPT,
3148
+ });
3149
+ }
3150
+ }
3151
+ }
3152
+ // Prompt 节点(默认包含,可视化页面可传 includePrompts=false 排除)
3153
+ if (includePrompts) {
3154
+ const prompts = this.listPrompts();
3155
+ for (const prompt of prompts) {
3156
+ nodes.push({
3157
+ id: prompt.id,
3158
+ label: `Prompt #${prompt.promptIndex}`,
3159
+ type: 'prompt',
3160
+ properties: {
3161
+ promptIndex: prompt.promptIndex,
3162
+ summary: prompt.summary || '',
3163
+ relatedTaskId: prompt.relatedTaskId || null,
3164
+ createdAt: prompt.createdAt,
3165
+ },
3166
+ });
3167
+ edges.push({
3168
+ from: this.getProjectId(),
3169
+ to: prompt.id,
3170
+ label: RT.HAS_PROMPT,
3171
+ });
3172
+ }
3173
+ }
3174
+ // Memory 节点
3175
+ {
3176
+ const memories = this.listMemories ? this.listMemories() : [];
3177
+ const memoryIdSet = new Set();
3178
+ for (const mem of memories) {
3179
+ memoryIdSet.add(mem.id);
3180
+ const label = `${mem.memoryType}: ${mem.content.slice(0, 30)}...`;
3181
+ nodes.push({
3182
+ id: mem.id,
3183
+ label,
3184
+ type: 'memory',
3185
+ properties: {
3186
+ memoryType: mem.memoryType,
3187
+ importance: mem.importance,
3188
+ hitCount: mem.hitCount,
3189
+ tags: mem.tags || [],
3190
+ createdAt: mem.createdAt,
3191
+ },
3192
+ });
3193
+ edges.push({
3194
+ from: this.getProjectId(),
3195
+ to: mem.id,
3196
+ label: RT.HAS_MEMORY,
3197
+ });
3198
+ // memory → task 边
3199
+ if (mem.relatedTaskId) {
3200
+ const taskEntity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', mem.relatedTaskId);
3201
+ if (taskEntity) {
3202
+ edges.push({
3203
+ from: mem.id,
3204
+ to: taskEntity.id,
3205
+ label: RT.MEMORY_FROM_TASK,
3206
+ });
3207
+ }
3208
+ }
3209
+ }
3210
+ // Phase-37: 记忆网络关系 — MEMORY_RELATES / MEMORY_FROM_DOC / MODULE_MEMORY / MEMORY_SUPERSEDES
3211
+ const memoryRelTypes = [RT.MEMORY_RELATES, RT.MEMORY_FROM_DOC, RT.MODULE_MEMORY, RT.MEMORY_SUPERSEDES];
3212
+ const edgeDedup = new Set();
3213
+ for (const memId of memoryIdSet) {
3214
+ for (const relType of memoryRelTypes) {
3215
+ const rels = this.getOutRelations(memId, relType);
3216
+ for (const rel of rels) {
3217
+ const key = `${rel.source}-${rel.target}-${relType}`;
3218
+ if (edgeDedup.has(key))
3219
+ continue;
3220
+ edgeDedup.add(key);
3221
+ edges.push({
3222
+ from: rel.source,
3223
+ to: rel.target,
3224
+ label: relType,
3225
+ properties: rel.weight != null ? { weight: rel.weight } : undefined,
3226
+ });
3227
+ }
3228
+ // 也检查入边(双向关系如 MEMORY_RELATES 的另一方向)
3229
+ const inRels = this.getInRelations(memId, relType);
3230
+ for (const rel of inRels) {
3231
+ const key = `${rel.source}-${rel.target}-${relType}`;
3232
+ if (edgeDedup.has(key))
3233
+ continue;
3234
+ edgeDedup.add(key);
3235
+ edges.push({
3236
+ from: rel.source,
3237
+ to: rel.target,
3238
+ label: relType,
3239
+ properties: rel.weight != null ? { weight: rel.weight } : undefined,
3240
+ });
3241
+ }
3242
+ }
3243
+ }
3244
+ }
3245
+ // 文档节点
3246
+ if (includeDocuments) {
3247
+ const docs = this.listSections();
3248
+ for (const doc of docs) {
3249
+ nodes.push({
3250
+ id: doc.id,
3251
+ label: doc.title,
3252
+ type: 'document',
3253
+ properties: {
3254
+ section: doc.section,
3255
+ subSection: doc.subSection,
3256
+ version: doc.version,
3257
+ parentDoc: doc.parentDoc || null,
3258
+ childDocs: doc.childDocs || [],
3259
+ },
1414
3260
  });
1415
3261
  // 子文档不连接项目节点,仅通过 doc_has_child 连接父文档
1416
3262
  if (!doc.parentDoc) {
@@ -1450,6 +3296,12 @@ class DevPlanGraphStore {
1450
3296
  mainTaskCount: mod.mainTaskCount,
1451
3297
  },
1452
3298
  });
3299
+ // 项目→模块 关系(确保每个模块都与项目节点相连,不会成为孤立节点)
3300
+ edges.push({
3301
+ from: this.getProjectId(),
3302
+ to: mod.id,
3303
+ label: RT.HAS_MODULE,
3304
+ });
1453
3305
  // 模块→主任务 关系
1454
3306
  const moduleTasks = this.listMainTasks({ moduleId: mod.moduleId });
1455
3307
  for (const mt of moduleTasks) {
@@ -1461,6 +3313,83 @@ class DevPlanGraphStore {
1461
3313
  }
1462
3314
  }
1463
3315
  }
3316
+ // Phase-41: 记忆节点 + 全部记忆关系
3317
+ const allMemoryEntities = this.findEntitiesByType(ET.MEMORY)
3318
+ .filter((e) => e.properties.projectName === this.projectName);
3319
+ for (const memEntity of allMemoryEntities) {
3320
+ const mem = this.entityToMemory(memEntity);
3321
+ nodes.push({
3322
+ id: mem.id,
3323
+ label: `${mem.memoryType}: ${mem.content.slice(0, 30)}...`,
3324
+ type: 'memory',
3325
+ properties: {
3326
+ memoryType: mem.memoryType,
3327
+ content: mem.content.length > 120 ? mem.content.slice(0, 120) + '...' : mem.content,
3328
+ importance: mem.importance,
3329
+ hitCount: mem.hitCount,
3330
+ tags: mem.tags,
3331
+ relatedTaskId: mem.relatedTaskId || null,
3332
+ sourceId: mem.sourceId || null,
3333
+ },
3334
+ });
3335
+ // project → memory (HAS_MEMORY)
3336
+ edges.push({
3337
+ from: this.getProjectId(),
3338
+ to: mem.id,
3339
+ label: RT.HAS_MEMORY,
3340
+ });
3341
+ // memory → task (MEMORY_FROM_TASK)
3342
+ if (mem.relatedTaskId) {
3343
+ const taskEntity = this.findEntityByProp(ET.MAIN_TASK, 'taskId', mem.relatedTaskId);
3344
+ if (taskEntity) {
3345
+ edges.push({
3346
+ from: mem.id,
3347
+ to: taskEntity.id,
3348
+ label: RT.MEMORY_FROM_TASK,
3349
+ });
3350
+ }
3351
+ }
3352
+ // memory ↔ memory (MEMORY_RELATES,仅导出出向避免重复)
3353
+ const memRelates = this.getOutRelations(mem.id, RT.MEMORY_RELATES);
3354
+ for (const rel of memRelates) {
3355
+ // 仅导出 source < target 的一侧,避免双向重复
3356
+ if (mem.id < rel.target) {
3357
+ edges.push({
3358
+ from: mem.id,
3359
+ to: rel.target,
3360
+ label: RT.MEMORY_RELATES,
3361
+ properties: rel.weight != null ? { weight: rel.weight } : undefined,
3362
+ });
3363
+ }
3364
+ }
3365
+ // doc → memory (MEMORY_FROM_DOC,反向查找)
3366
+ const fromDocRels = this.getInRelations(mem.id, RT.MEMORY_FROM_DOC);
3367
+ for (const rel of fromDocRels) {
3368
+ edges.push({
3369
+ from: rel.source,
3370
+ to: mem.id,
3371
+ label: RT.MEMORY_FROM_DOC,
3372
+ });
3373
+ }
3374
+ // module → memory (MODULE_MEMORY,反向查找)
3375
+ const moduleMemRels = this.getInRelations(mem.id, RT.MODULE_MEMORY);
3376
+ for (const rel of moduleMemRels) {
3377
+ edges.push({
3378
+ from: rel.source,
3379
+ to: mem.id,
3380
+ label: RT.MODULE_MEMORY,
3381
+ });
3382
+ }
3383
+ // memory → memory (MEMORY_SUPERSEDES)
3384
+ const supersedesRels = this.getOutRelations(mem.id, RT.MEMORY_SUPERSEDES);
3385
+ for (const rel of supersedesRels) {
3386
+ edges.push({
3387
+ from: mem.id,
3388
+ to: rel.target,
3389
+ label: RT.MEMORY_SUPERSEDES,
3390
+ });
3391
+ }
3392
+ }
1464
3393
  if (includeNodeDegree) {
1465
3394
  // 优先走 SocialGraphV2 原生 exportGraph(includeNodeDegree) 的 degree 结果
1466
3395
  const nativeDegreeMap = {};
@@ -1506,6 +3435,197 @@ class DevPlanGraphStore {
1506
3435
  }
1507
3436
  return { nodes, edges };
1508
3437
  }
3438
+ /**
3439
+ * 分页导出图谱数据 (Phase-9 T9.1)
3440
+ *
3441
+ * 利用 Rust 层 SocialGraphV2 的 exportGraphPaginated() 分页 API,
3442
+ * 避免全量加载 + 内存切片。真正的分页下推到数据库层。
3443
+ *
3444
+ * @param offset 分页偏移量
3445
+ * @param limit 每页节点数
3446
+ * @param options 可选参数
3447
+ * @returns 分页图谱数据
3448
+ */
3449
+ exportGraphPaginated(offset, limit, options) {
3450
+ const includeDocuments = options?.includeDocuments !== false;
3451
+ const includeModules = options?.includeModules !== false;
3452
+ const includeNodeDegree = options?.includeNodeDegree !== false;
3453
+ // Build entity type filter for Rust layer
3454
+ const entityTypes = [];
3455
+ entityTypes.push(ET.PROJECT, ET.MAIN_TASK, ET.SUB_TASK);
3456
+ if (includeDocuments)
3457
+ entityTypes.push(ET.DOC);
3458
+ if (includeModules)
3459
+ entityTypes.push(ET.MODULE);
3460
+ if (options?.entityTypes?.length) {
3461
+ // Use user-specified filter if provided
3462
+ entityTypes.length = 0;
3463
+ entityTypes.push(...options.entityTypes);
3464
+ }
3465
+ try {
3466
+ // Call Rust SocialGraphV2.exportGraphPaginated via NAPI
3467
+ const result = this.graph.exportGraphPaginated({
3468
+ offset,
3469
+ limit,
3470
+ entityTypes,
3471
+ includeNodeDegree,
3472
+ includeEdgeMeta: false,
3473
+ });
3474
+ if (result && typeof result === 'object') {
3475
+ const rustNodes = result.nodes || [];
3476
+ const rustEdges = result.edges || [];
3477
+ // Transform Rust GraphNode -> DevPlanGraphNode
3478
+ // Rust GraphNode only carries `group: NodeGroup` (numeric 0/1/2) — all DevPlan
3479
+ // entities map to Tag(2), losing type information. We must resolve entity_type
3480
+ // via getEntity() for correct visualization (module=diamond, doc=box, etc.).
3481
+ const currentProjectId = this.getProjectId();
3482
+ const nodes = [];
3483
+ for (const n of rustNodes) {
3484
+ const data = n.data || {};
3485
+ // Try multiple sources for entity_type string
3486
+ let etStr = n.entity_type || n.entityType
3487
+ || data.entity_type || data.entityType;
3488
+ // If still not found, look up the actual entity (fast hash lookup)
3489
+ if (!etStr || typeof etStr !== 'string') {
3490
+ try {
3491
+ const entity = this.graph.getEntity(n.id);
3492
+ if (entity) {
3493
+ etStr = entity.entity_type || entity.entityType;
3494
+ }
3495
+ }
3496
+ catch { /* ignore — fall through to group */ }
3497
+ }
3498
+ const devPlanType = this.mapGroupToDevPlanType(etStr || n.group);
3499
+ // Filter out project nodes that don't belong to the current project.
3500
+ // The Rust layer returns ALL entities matching the type filter, which may
3501
+ // include stale project nodes from other projects (e.g. "devplan-db").
3502
+ if (devPlanType === 'project' && n.id !== currentProjectId) {
3503
+ continue;
3504
+ }
3505
+ nodes.push({
3506
+ id: n.id,
3507
+ label: n.label || n.id,
3508
+ type: devPlanType,
3509
+ degree: includeNodeDegree ? (n.degree ?? 0) : undefined,
3510
+ properties: data,
3511
+ });
3512
+ }
3513
+ // Transform Rust GraphEdge -> DevPlanGraphEdge
3514
+ const edges = rustEdges.map((e) => ({
3515
+ from: e.from,
3516
+ to: e.to,
3517
+ label: e.label || e.relation_type || '',
3518
+ }));
3519
+ return {
3520
+ nodes,
3521
+ edges,
3522
+ totalNodes: result.total_nodes ?? result.totalNodes ?? nodes.length,
3523
+ totalEdges: result.total_edges ?? result.totalEdges ?? edges.length,
3524
+ offset: result.offset ?? offset,
3525
+ limit: result.limit ?? limit,
3526
+ hasMore: result.has_more ?? result.hasMore ?? false,
3527
+ };
3528
+ }
3529
+ }
3530
+ catch {
3531
+ // If NAPI method not available, fall back to in-memory pagination
3532
+ }
3533
+ // Fallback: full load + in-memory pagination
3534
+ const fullGraph = this.exportGraph({
3535
+ includeDocuments,
3536
+ includeModules,
3537
+ includeNodeDegree,
3538
+ enableBackendDegreeFallback: true,
3539
+ });
3540
+ const allNodes = fullGraph.nodes;
3541
+ const pageNodes = allNodes.slice(offset, offset + limit);
3542
+ const pageNodeIds = new Set(pageNodes.map((n) => n.id));
3543
+ const pageEdges = fullGraph.edges.filter((e) => pageNodeIds.has(e.from) && pageNodeIds.has(e.to));
3544
+ return {
3545
+ nodes: pageNodes,
3546
+ edges: pageEdges,
3547
+ totalNodes: allNodes.length,
3548
+ totalEdges: fullGraph.edges.length,
3549
+ offset,
3550
+ limit,
3551
+ hasMore: offset + limit < allNodes.length,
3552
+ };
3553
+ }
3554
+ /**
3555
+ * 导出紧凑二进制格式 (Phase-9 T9.3)
3556
+ *
3557
+ * 利用 Rust 层 CompactGraphExport 格式,返回 Buffer。
3558
+ * 比 JSON 小 5x+,解析速度接近零。
3559
+ *
3560
+ * @returns Node.js Buffer 或 null (如果 NAPI 方法不可用)
3561
+ */
3562
+ exportGraphCompact() {
3563
+ try {
3564
+ const buf = this.graph.exportGraphCompact({
3565
+ maxNodes: 1000000,
3566
+ includeTags: true,
3567
+ includeCompanies: true,
3568
+ includeNodeDegree: true,
3569
+ includeEdgeMeta: false,
3570
+ });
3571
+ if (buf && buf.length > 16) {
3572
+ return buf;
3573
+ }
3574
+ }
3575
+ catch {
3576
+ // NAPI method not available
3577
+ }
3578
+ return null;
3579
+ }
3580
+ /**
3581
+ * 获取实体组聚合摘要 (Phase-9 T9.4)
3582
+ *
3583
+ * 利用 Rust 层 group_entities_summary() 返回按类型分组的统计信息。
3584
+ * 远比加载全部实体开销小,适合低缩放级别的集群视图。
3585
+ *
3586
+ * @returns 聚合结果或 null (如果 NAPI 方法不可用)
3587
+ */
3588
+ getEntityGroupSummary() {
3589
+ try {
3590
+ const result = this.graph.getEntityGroupSummary();
3591
+ if (result && typeof result === 'object') {
3592
+ return {
3593
+ groups: (result.groups || []).map((g) => ({
3594
+ entityType: g.entity_type ?? g.entityType ?? '',
3595
+ count: g.count ?? 0,
3596
+ sampleIds: g.sample_ids ?? g.sampleIds ?? [],
3597
+ })),
3598
+ totalEntities: result.total_entities ?? result.totalEntities ?? 0,
3599
+ totalRelations: result.total_relations ?? result.totalRelations ?? 0,
3600
+ };
3601
+ }
3602
+ }
3603
+ catch {
3604
+ // NAPI method not available
3605
+ }
3606
+ return null;
3607
+ }
3608
+ /**
3609
+ * Map Rust NodeGroup name to DevPlan node type
3610
+ */
3611
+ mapGroupToDevPlanType(group) {
3612
+ // Rust NodeGroup: 0=Person, 1=Company, 2=Tag
3613
+ // But in DevPlan context, entity_type matters more
3614
+ // We rely on the entity's entity_type property
3615
+ if (typeof group === 'string') {
3616
+ if (group.includes('project'))
3617
+ return 'project';
3618
+ if (group.includes('main-task') || group.includes('main_task'))
3619
+ return 'main-task';
3620
+ if (group.includes('sub-task') || group.includes('sub_task'))
3621
+ return 'sub-task';
3622
+ if (group.includes('doc'))
3623
+ return 'document';
3624
+ if (group.includes('module'))
3625
+ return 'module';
3626
+ }
3627
+ return 'sub-task'; // default fallback
3628
+ }
1509
3629
  // ==========================================================================
1510
3630
  // Private Helpers
1511
3631
  // ==========================================================================
@@ -1558,16 +3678,90 @@ class DevPlanGraphStore {
1558
3678
  if (mainTask.totalSubtasks === subs.length && mainTask.completedSubtasks === completedCount) {
1559
3679
  return mainTask;
1560
3680
  }
1561
- this.graph.updateEntity(mainTask.id, {
1562
- properties: {
1563
- totalSubtasks: subs.length,
1564
- completedSubtasks: completedCount,
1565
- updatedAt: Date.now(),
1566
- },
3681
+ const now = Date.now();
3682
+ // Phase-45: 使用 upsertEntityByProp 代替 updateEntity(修复分片路由不一致)
3683
+ this.graph.upsertEntityByProp(ET.MAIN_TASK, 'taskId', mainTaskId, mainTask.title, {
3684
+ projectName: this.projectName,
3685
+ taskId: mainTaskId,
3686
+ title: mainTask.title,
3687
+ priority: mainTask.priority,
3688
+ description: mainTask.description || '',
3689
+ estimatedHours: mainTask.estimatedHours || 0,
3690
+ relatedSections: mainTask.relatedSections || [],
3691
+ relatedPromptIds: mainTask.relatedPromptIds || [],
3692
+ moduleId: mainTask.moduleId || null,
3693
+ totalSubtasks: subs.length,
3694
+ completedSubtasks: completedCount,
3695
+ status: mainTask.status,
3696
+ order: mainTask.order,
3697
+ createdAt: mainTask.createdAt,
3698
+ updatedAt: now,
3699
+ completedAt: mainTask.completedAt,
1567
3700
  });
1568
3701
  this.graph.flush();
1569
- const updated = this.graph.getEntity(mainTask.id);
1570
- return updated ? this.entityToMainTask(updated) : mainTask;
3702
+ return this.getMainTask(mainTaskId) || mainTask;
3703
+ }
3704
+ /**
3705
+ * Phase-45: 修复存量数据 — 遍历所有主任务,重新计算子任务计数和自动完成状态。
3706
+ * 用于修复因 updateEntity 路由不一致导致的 completedSubtasks=0 和 status=in_progress 的 Bug。
3707
+ * @returns 修复报告
3708
+ */
3709
+ repairAllMainTaskCounts() {
3710
+ const allMainTasks = this.listMainTasks();
3711
+ const report = { repaired: 0, autoCompleted: 0, details: [] };
3712
+ for (const mt of allMainTasks) {
3713
+ if (mt.status === 'cancelled')
3714
+ continue;
3715
+ const subs = this.listSubTasks(mt.taskId);
3716
+ const completedCount = subs.filter((s) => s.status === 'completed').length;
3717
+ const totalCount = subs.length;
3718
+ let needsRepair = false;
3719
+ let needsAutoComplete = false;
3720
+ // 检查计数是否正确
3721
+ if (mt.totalSubtasks !== totalCount || mt.completedSubtasks !== completedCount) {
3722
+ needsRepair = true;
3723
+ }
3724
+ // 检查是否应自动完成
3725
+ if (totalCount > 0 && completedCount >= totalCount && mt.status !== 'completed') {
3726
+ needsAutoComplete = true;
3727
+ needsRepair = true;
3728
+ }
3729
+ if (!needsRepair)
3730
+ continue;
3731
+ const now = Date.now();
3732
+ const finalStatus = needsAutoComplete ? 'completed' : mt.status;
3733
+ const finalCompletedAt = needsAutoComplete ? now : mt.completedAt;
3734
+ this.graph.upsertEntityByProp(ET.MAIN_TASK, 'taskId', mt.taskId, mt.title, {
3735
+ projectName: this.projectName,
3736
+ taskId: mt.taskId,
3737
+ title: mt.title,
3738
+ priority: mt.priority,
3739
+ description: mt.description || '',
3740
+ estimatedHours: mt.estimatedHours || 0,
3741
+ relatedSections: mt.relatedSections || [],
3742
+ relatedPromptIds: mt.relatedPromptIds || [],
3743
+ moduleId: mt.moduleId || null,
3744
+ totalSubtasks: totalCount,
3745
+ completedSubtasks: completedCount,
3746
+ status: finalStatus,
3747
+ order: mt.order,
3748
+ createdAt: mt.createdAt,
3749
+ updatedAt: now,
3750
+ completedAt: finalCompletedAt,
3751
+ });
3752
+ report.repaired++;
3753
+ if (needsAutoComplete) {
3754
+ report.autoCompleted++;
3755
+ report.details.push({ taskId: mt.taskId, action: `auto-completed (${completedCount}/${totalCount} subtasks done)` });
3756
+ }
3757
+ else {
3758
+ report.details.push({ taskId: mt.taskId, action: `counts fixed: ${mt.completedSubtasks}→${completedCount}/${totalCount}` });
3759
+ }
3760
+ }
3761
+ if (report.repaired > 0) {
3762
+ this.graph.flush();
3763
+ }
3764
+ return report;
1571
3765
  }
1572
3766
  autoUpdateMilestones(completedMainTask) {
1573
3767
  const milestonesDoc = this.getSection('milestones');
@@ -1771,4 +3965,14 @@ class DevPlanGraphStore {
1771
3965
  }
1772
3966
  }
1773
3967
  exports.DevPlanGraphStore = DevPlanGraphStore;
3968
+ // ==========================================================================
3969
+ // Generic Entity Helpers
3970
+ // ==========================================================================
3971
+ /** 任务状态优先级映射(用于去重时选择"胜出"实体) */
3972
+ DevPlanGraphStore.STATUS_PRIORITY = {
3973
+ cancelled: 0,
3974
+ pending: 1,
3975
+ in_progress: 2,
3976
+ completed: 3,
3977
+ };
1774
3978
  //# sourceMappingURL=dev-plan-graph-store.js.map