aifastdb-devplan 1.2.6 → 1.3.8

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.
@@ -15,9 +15,43 @@
15
15
  * - Entity 类型: devplan-project, devplan-doc, devplan-main-task, devplan-sub-task, devplan-module
16
16
  * - Relation 类型: has_document, has_main_task, has_sub_task, module_has_task, module_has_doc
17
17
  */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
18
51
  Object.defineProperty(exports, "__esModule", { value: true });
19
52
  exports.DevPlanGraphStore = void 0;
20
53
  const aifastdb_1 = require("aifastdb");
54
+ const path = __importStar(require("path"));
21
55
  // ============================================================================
22
56
  // Constants
23
57
  // ============================================================================
@@ -37,6 +71,7 @@ const RT = {
37
71
  MODULE_HAS_TASK: 'module_has_task',
38
72
  MODULE_HAS_DOC: 'module_has_doc',
39
73
  TASK_HAS_DOC: 'task_has_doc',
74
+ DOC_HAS_CHILD: 'doc_has_child',
40
75
  };
41
76
  // ============================================================================
42
77
  // Helper
@@ -67,19 +102,85 @@ class DevPlanGraphStore {
67
102
  constructor(projectName, config) {
68
103
  /** 缓存的项目根实体 ID */
69
104
  this.projectEntityId = null;
105
+ /** VibeSynapse 实例(用于 Embedding 生成),仅启用语义搜索时可用 */
106
+ this.synapse = null;
107
+ /** 语义搜索是否成功初始化 */
108
+ this.semanticSearchReady = false;
70
109
  this.projectName = projectName;
71
- this.graph = new aifastdb_1.SocialGraphV2({
110
+ // 构建 SocialGraphV2 配置
111
+ const graphConfig = {
72
112
  path: config.graphPath,
73
113
  shardCount: config.shardCount || 4,
74
114
  walEnabled: true,
75
115
  mode: 'balanced',
76
116
  shardNames: ['entities', 'relations', 'index', 'meta'],
77
- });
78
- // 恢复 WAL 数据
117
+ };
118
+ // 如果启用语义搜索,配置 SocialGraphV2 的向量搜索
119
+ const dimension = config.embeddingDimension || 384;
120
+ if (config.enableSemanticSearch) {
121
+ graphConfig.vectorSearch = {
122
+ dimension,
123
+ m: 16,
124
+ efConstruction: 200,
125
+ efSearch: 50,
126
+ maxElements: 100000,
127
+ shardCount: 1,
128
+ };
129
+ }
130
+ this.graph = new aifastdb_1.SocialGraphV2(graphConfig);
131
+ // 恢复 WAL 数据(包括向量 WAL)
79
132
  this.graph.recover();
133
+ // 初始化 VibeSynapse(用于 Embedding 生成)
134
+ if (config.enableSemanticSearch) {
135
+ this.initSynapse(config.graphPath, dimension);
136
+ }
80
137
  // 确保项目根实体存在
81
138
  this.ensureProjectEntity();
82
139
  }
140
+ /**
141
+ * 初始化 VibeSynapse Embedding 引擎
142
+ *
143
+ * 使用 Candle MiniLM (384维) 作为默认模型,支持零配置离线使用。
144
+ * 初始化失败时降级为纯字面搜索(graceful degradation)。
145
+ */
146
+ initSynapse(graphPath, dimension) {
147
+ try {
148
+ const synapsePath = path.resolve(graphPath, '..', 'synapse-data');
149
+ this.synapse = new aifastdb_1.VibeSynapse({
150
+ storage: synapsePath,
151
+ dimension,
152
+ perception: {
153
+ engineType: 'candle',
154
+ modelId: 'sentence-transformers/all-MiniLM-L6-v2',
155
+ autoDownload: true,
156
+ },
157
+ });
158
+ // 验证 perception engine 是否真正可用
159
+ if (!this.synapse.hasPerception) {
160
+ console.warn('[DevPlan] VibeSynapse created but perception engine not available. ' +
161
+ 'Candle MiniLM may not be installed. Falling back to literal search.');
162
+ this.synapse = null;
163
+ this.semanticSearchReady = false;
164
+ return;
165
+ }
166
+ // 测试 embed 是否可用(dry run)
167
+ try {
168
+ this.synapse.embed('test');
169
+ this.semanticSearchReady = true;
170
+ console.log('[DevPlan] Semantic search initialized (Candle MiniLM)');
171
+ }
172
+ catch {
173
+ console.warn('[DevPlan] VibeSynapse embed() dry-run failed. Falling back to literal search.');
174
+ this.synapse = null;
175
+ this.semanticSearchReady = false;
176
+ }
177
+ }
178
+ catch (e) {
179
+ console.warn(`[DevPlan] Failed to initialize VibeSynapse for semantic search: ${e instanceof Error ? e.message : String(e)}. Falling back to literal search.`);
180
+ this.synapse = null;
181
+ this.semanticSearchReady = false;
182
+ }
183
+ }
83
184
  // ==========================================================================
84
185
  // Project Entity
85
186
  // ==========================================================================
@@ -147,6 +248,19 @@ class DevPlanGraphStore {
147
248
  // ==========================================================================
148
249
  entityToDevPlanDoc(e) {
149
250
  const p = e.properties;
251
+ // 获取 parentDoc:从属性读取
252
+ const parentDoc = p.parentDoc || undefined;
253
+ // 获取 childDocs:通过 DOC_HAS_CHILD 出向关系查询
254
+ const childDocRels = this.getOutRelations(e.id, RT.DOC_HAS_CHILD);
255
+ const childDocs = childDocRels.length > 0
256
+ ? childDocRels.map((rel) => {
257
+ const childEntity = this.graph.getEntity(rel.target);
258
+ if (!childEntity)
259
+ return undefined;
260
+ const cp = childEntity.properties;
261
+ return sectionKey(cp.section, cp.subSection || undefined);
262
+ }).filter((k) => k !== undefined)
263
+ : undefined;
150
264
  return {
151
265
  id: e.id,
152
266
  projectName: this.projectName,
@@ -158,6 +272,8 @@ class DevPlanGraphStore {
158
272
  relatedSections: p.relatedSections || [],
159
273
  moduleId: p.moduleId || undefined,
160
274
  relatedTaskIds: p.relatedTaskIds || [],
275
+ parentDoc,
276
+ childDocs,
161
277
  createdAt: p.createdAt || e.created_at,
162
278
  updatedAt: p.updatedAt || e.updated_at,
163
279
  };
@@ -177,6 +293,7 @@ class DevPlanGraphStore {
177
293
  totalSubtasks: p.totalSubtasks || 0,
178
294
  completedSubtasks: p.completedSubtasks || 0,
179
295
  status: p.status || 'pending',
296
+ order: p.order != null ? p.order : undefined,
180
297
  createdAt: p.createdAt || e.created_at,
181
298
  updatedAt: p.updatedAt || e.updated_at,
182
299
  completedAt: p.completedAt || null,
@@ -194,6 +311,7 @@ class DevPlanGraphStore {
194
311
  relatedFiles: p.relatedFiles || [],
195
312
  description: p.description || undefined,
196
313
  status: p.status || 'pending',
314
+ order: p.order != null ? p.order : undefined,
197
315
  createdAt: p.createdAt || e.created_at,
198
316
  updatedAt: p.updatedAt || e.updated_at,
199
317
  completedAt: p.completedAt || null,
@@ -238,6 +356,8 @@ class DevPlanGraphStore {
238
356
  const now = Date.now();
239
357
  const version = input.version || '1.0.0';
240
358
  const finalModuleId = input.moduleId || existing?.moduleId;
359
+ // 确定最终的 parentDoc 值(显式传入 > 已有值)
360
+ const finalParentDoc = input.parentDoc !== undefined ? input.parentDoc : existing?.parentDoc;
241
361
  if (existing) {
242
362
  // 更新已有文档
243
363
  const finalRelatedTaskIds = input.relatedTaskIds || existing.relatedTaskIds || [];
@@ -250,6 +370,7 @@ class DevPlanGraphStore {
250
370
  relatedSections: input.relatedSections || [],
251
371
  relatedTaskIds: finalRelatedTaskIds,
252
372
  moduleId: finalModuleId || null,
373
+ parentDoc: finalParentDoc || null,
253
374
  updatedAt: now,
254
375
  },
255
376
  });
@@ -257,6 +378,8 @@ class DevPlanGraphStore {
257
378
  if (finalModuleId && finalModuleId !== existing.moduleId) {
258
379
  this.updateModuleDocRelation(existing.id, existing.moduleId, finalModuleId);
259
380
  }
381
+ // 更新 parentDoc 关系(DOC_HAS_CHILD)
382
+ this.updateParentDocRelation(existing.id, existing.parentDoc, finalParentDoc);
260
383
  // 更新 task -> doc 关系
261
384
  if (finalRelatedTaskIds.length) {
262
385
  // 删除旧的 TASK_HAS_DOC 入向关系(指向本文档的)
@@ -272,6 +395,8 @@ class DevPlanGraphStore {
272
395
  }
273
396
  }
274
397
  }
398
+ // 语义搜索:自动为更新的文档生成 Embedding 并索引
399
+ this.autoIndexDocument(existing.id, input.title, input.content);
275
400
  this.graph.flush();
276
401
  return existing.id;
277
402
  }
@@ -286,11 +411,23 @@ class DevPlanGraphStore {
286
411
  relatedSections: input.relatedSections || [],
287
412
  relatedTaskIds: input.relatedTaskIds || [],
288
413
  moduleId: finalModuleId || null,
414
+ parentDoc: finalParentDoc || null,
289
415
  createdAt: now,
290
416
  updatedAt: now,
291
417
  });
292
- // 创建 project -> doc 关系
293
- this.graph.putRelation(this.getProjectId(), entity.id, RT.HAS_DOCUMENT);
418
+ // 子文档不直接连接项目节点,仅通过 doc_has_child 连接父文档
419
+ if (finalParentDoc) {
420
+ // 有 parentDoc → 创建 DOC_HAS_CHILD 关系(parent -> child),不创建 project -> doc
421
+ const [parentSection, parentSubSection] = finalParentDoc.split('|');
422
+ const parentEntity = this.findDocEntityBySection(parentSection, parentSubSection || undefined);
423
+ if (parentEntity) {
424
+ this.graph.putRelation(parentEntity.id, entity.id, RT.DOC_HAS_CHILD);
425
+ }
426
+ }
427
+ else {
428
+ // 无 parentDoc → 创建 project -> doc 关系(顶级文档)
429
+ this.graph.putRelation(this.getProjectId(), entity.id, RT.HAS_DOCUMENT);
430
+ }
294
431
  // 如果有模块关联,创建 module -> doc 关系
295
432
  if (finalModuleId) {
296
433
  const modEntity = this.findEntityByProp(ET.MODULE, 'moduleId', finalModuleId);
@@ -307,6 +444,8 @@ class DevPlanGraphStore {
307
444
  }
308
445
  }
309
446
  }
447
+ // 语义搜索:自动为新文档生成 Embedding 并索引
448
+ this.autoIndexDocument(entity.id, input.title, input.content);
310
449
  this.graph.flush();
311
450
  return entity.id;
312
451
  }
@@ -341,16 +480,135 @@ class DevPlanGraphStore {
341
480
  });
342
481
  }
343
482
  searchSections(query, limit = 10) {
344
- const queryLower = query.toLowerCase();
345
- return this.listSections()
346
- .filter((doc) => doc.content.toLowerCase().includes(queryLower) ||
347
- doc.title.toLowerCase().includes(queryLower))
348
- .slice(0, limit);
483
+ // 默认使用 hybrid 模式(语义+字面),无语义搜索时回退字面
484
+ const mode = this.semanticSearchReady ? 'hybrid' : 'literal';
485
+ return this.searchSectionsAdvanced(query, { mode, limit }).map(({ score, ...doc }) => doc);
486
+ }
487
+ /**
488
+ * 高级搜索:支持 literal / semantic / hybrid 三种模式
489
+ *
490
+ * - literal: 纯字面匹配(标题+内容包含查询词)
491
+ * - semantic: 纯语义搜索(embed(query) → searchEntitiesByVector)
492
+ * - hybrid: 字面+语义 RRF 融合排序
493
+ *
494
+ * 当 VibeSynapse 不可用时,semantic/hybrid 模式自动降级为 literal。
495
+ */
496
+ searchSectionsAdvanced(query, options) {
497
+ const mode = options?.mode || 'hybrid';
498
+ const limit = options?.limit || 10;
499
+ const minScore = options?.minScore || 0;
500
+ // ---- Literal Search ----
501
+ const literalResults = this.literalSearch(query);
502
+ // ---- If no semantic search or literal-only mode ----
503
+ if (mode === 'literal' || !this.semanticSearchReady || !this.synapse) {
504
+ return literalResults.slice(0, limit).map((doc) => ({ ...doc, score: undefined }));
505
+ }
506
+ // ---- Semantic Search ----
507
+ let semanticHits = [];
508
+ try {
509
+ const embedding = this.synapse.embed(query);
510
+ semanticHits = this.graph.searchEntitiesByVector(embedding, limit * 2, ET.DOC);
511
+ }
512
+ catch (e) {
513
+ console.warn(`[DevPlan] Semantic search failed: ${e instanceof Error ? e.message : String(e)}`);
514
+ // 降级为字面搜索
515
+ return literalResults.slice(0, limit).map((doc) => ({ ...doc, score: undefined }));
516
+ }
517
+ if (mode === 'semantic') {
518
+ // 纯语义模式:直接返回语义搜索结果
519
+ const docs = [];
520
+ for (const hit of semanticHits) {
521
+ if (minScore > 0 && hit.score < minScore)
522
+ continue;
523
+ const entity = this.graph.getEntity(hit.entityId);
524
+ if (entity && entity.properties?.projectName === this.projectName) {
525
+ docs.push({ ...this.entityToDevPlanDoc(entity), score: hit.score });
526
+ }
527
+ if (docs.length >= limit)
528
+ break;
529
+ }
530
+ return docs;
531
+ }
532
+ // ---- Hybrid Mode: RRF Fusion ----
533
+ return this.rrfFusion(semanticHits, literalResults, limit, minScore);
534
+ }
535
+ /**
536
+ * 重建所有文档的向量索引
537
+ *
538
+ * 适用于:首次启用语义搜索、模型切换、索引损坏修复。
539
+ */
540
+ rebuildIndex() {
541
+ const startTime = Date.now();
542
+ const docs = this.listSections();
543
+ let indexed = 0;
544
+ let failed = 0;
545
+ const failedDocIds = [];
546
+ if (!this.semanticSearchReady || !this.synapse) {
547
+ return {
548
+ total: docs.length,
549
+ indexed: 0,
550
+ failed: docs.length,
551
+ durationMs: Date.now() - startTime,
552
+ failedDocIds: docs.map((d) => d.id),
553
+ };
554
+ }
555
+ for (const doc of docs) {
556
+ try {
557
+ const text = `${doc.title}\n${doc.content}`;
558
+ const embedding = this.synapse.embed(text);
559
+ this.graph.indexEntity(doc.id, embedding);
560
+ indexed++;
561
+ }
562
+ catch (e) {
563
+ failed++;
564
+ failedDocIds.push(doc.id);
565
+ }
566
+ }
567
+ this.graph.flush();
568
+ return {
569
+ total: docs.length,
570
+ indexed,
571
+ failed,
572
+ durationMs: Date.now() - startTime,
573
+ failedDocIds: failedDocIds.length > 0 ? failedDocIds : undefined,
574
+ };
575
+ }
576
+ /**
577
+ * 检查语义搜索是否可用
578
+ */
579
+ isSemanticSearchEnabled() {
580
+ return this.semanticSearchReady;
349
581
  }
350
582
  deleteSection(section, subSection) {
351
583
  const existing = this.getSection(section, subSection);
352
584
  if (!existing)
353
585
  return false;
586
+ // 断开 DOC_HAS_CHILD 入向关系(从父文档指向本文档的)
587
+ const parentRels = this.getInRelations(existing.id, RT.DOC_HAS_CHILD);
588
+ for (const rel of parentRels) {
589
+ this.graph.deleteRelation(rel.id);
590
+ }
591
+ // 断开 DOC_HAS_CHILD 出向关系(本文档指向子文档的),子文档的 parentDoc 属性清空
592
+ const childRels = this.getOutRelations(existing.id, RT.DOC_HAS_CHILD);
593
+ for (const rel of childRels) {
594
+ this.graph.deleteRelation(rel.id);
595
+ // 清除子文档的 parentDoc 属性
596
+ const childEntity = this.graph.getEntity(rel.target);
597
+ if (childEntity) {
598
+ this.graph.updateEntity(childEntity.id, {
599
+ properties: { parentDoc: null },
600
+ });
601
+ }
602
+ }
603
+ // 语义搜索:删除文档对应的向量索引
604
+ if (this.semanticSearchReady) {
605
+ try {
606
+ this.graph.removeEntityVector(existing.id);
607
+ }
608
+ catch {
609
+ // 向量可能不存在,忽略错误
610
+ }
611
+ }
354
612
  this.graph.deleteEntity(existing.id);
355
613
  this.graph.flush();
356
614
  return true;
@@ -364,6 +622,8 @@ class DevPlanGraphStore {
364
622
  throw new Error(`Main task "${input.taskId}" already exists for project "${this.projectName}"`);
365
623
  }
366
624
  const now = Date.now();
625
+ // 如果未指定 order,自动分配为当前最大 order + 1
626
+ const order = input.order != null ? input.order : this.getNextMainTaskOrder();
367
627
  const entity = this.graph.addEntity(input.title, ET.MAIN_TASK, {
368
628
  projectName: this.projectName,
369
629
  taskId: input.taskId,
@@ -376,6 +636,7 @@ class DevPlanGraphStore {
376
636
  totalSubtasks: 0,
377
637
  completedSubtasks: 0,
378
638
  status: 'pending',
639
+ order,
379
640
  createdAt: now,
380
641
  updatedAt: now,
381
642
  completedAt: null,
@@ -426,6 +687,7 @@ class DevPlanGraphStore {
426
687
  const now = Date.now();
427
688
  const completedAt = finalStatus === 'completed' ? (existing.completedAt || now) : null;
428
689
  const finalModuleId = input.moduleId || existing.moduleId;
690
+ const finalOrder = input.order != null ? input.order : existing.order;
429
691
  this.graph.updateEntity(existing.id, {
430
692
  name: input.title,
431
693
  properties: {
@@ -436,6 +698,7 @@ class DevPlanGraphStore {
436
698
  relatedSections: input.relatedSections || existing.relatedSections || [],
437
699
  moduleId: finalModuleId || null,
438
700
  status: finalStatus,
701
+ order: finalOrder,
439
702
  updatedAt: now,
440
703
  completedAt,
441
704
  },
@@ -480,7 +743,8 @@ class DevPlanGraphStore {
480
743
  if (filter?.moduleId) {
481
744
  entities = entities.filter((e) => e.properties.moduleId === filter.moduleId);
482
745
  }
483
- return entities.map((e) => this.entityToMainTask(e));
746
+ const tasks = entities.map((e) => this.entityToMainTask(e));
747
+ return this.sortByOrder(tasks);
484
748
  }
485
749
  updateMainTaskStatus(taskId, status) {
486
750
  const mainTask = this.getMainTask(taskId);
@@ -512,6 +776,8 @@ class DevPlanGraphStore {
512
776
  throw new Error(`Parent main task "${input.parentTaskId}" not found for project "${this.projectName}"`);
513
777
  }
514
778
  const now = Date.now();
779
+ // 如果未指定 order,自动分配为当前父任务下最大 order + 1
780
+ const order = input.order != null ? input.order : this.getNextSubTaskOrder(input.parentTaskId);
515
781
  const entity = this.graph.addEntity(input.title, ET.SUB_TASK, {
516
782
  projectName: this.projectName,
517
783
  taskId: input.taskId,
@@ -521,6 +787,7 @@ class DevPlanGraphStore {
521
787
  relatedFiles: input.relatedFiles || [],
522
788
  description: input.description || '',
523
789
  status: 'pending',
790
+ order,
524
791
  createdAt: now,
525
792
  updatedAt: now,
526
793
  completedAt: null,
@@ -542,6 +809,7 @@ class DevPlanGraphStore {
542
809
  throw new Error(`Parent main task "${input.parentTaskId}" not found for project "${this.projectName}"`);
543
810
  }
544
811
  const now = Date.now();
812
+ const order = input.order != null ? input.order : this.getNextSubTaskOrder(input.parentTaskId);
545
813
  const entity = this.graph.addEntity(input.title, ET.SUB_TASK, {
546
814
  projectName: this.projectName,
547
815
  taskId: input.taskId,
@@ -551,6 +819,7 @@ class DevPlanGraphStore {
551
819
  relatedFiles: input.relatedFiles || [],
552
820
  description: input.description || '',
553
821
  status: targetStatus,
822
+ order,
554
823
  createdAt: now,
555
824
  updatedAt: now,
556
825
  completedAt: targetStatus === 'completed' ? now : null,
@@ -571,10 +840,12 @@ class DevPlanGraphStore {
571
840
  }
572
841
  }
573
842
  // 检查是否有实质变化
843
+ const newOrder = input.order != null ? input.order : existing.order;
574
844
  if (existing.title === input.title &&
575
845
  existing.description === (input.description || '') &&
576
846
  existing.status === finalStatus &&
577
- existing.estimatedHours === (input.estimatedHours || 0)) {
847
+ existing.estimatedHours === (input.estimatedHours || 0) &&
848
+ existing.order === newOrder) {
578
849
  return existing;
579
850
  }
580
851
  const now = Date.now();
@@ -587,6 +858,7 @@ class DevPlanGraphStore {
587
858
  relatedFiles: input.relatedFiles || existing.relatedFiles || [],
588
859
  description: input.description || existing.description || '',
589
860
  status: finalStatus,
861
+ order: newOrder,
590
862
  updatedAt: now,
591
863
  completedAt,
592
864
  },
@@ -605,7 +877,8 @@ class DevPlanGraphStore {
605
877
  if (filter?.status) {
606
878
  entities = entities.filter((e) => e.properties.status === filter.status);
607
879
  }
608
- return entities.map((e) => this.entityToSubTask(e));
880
+ const tasks = entities.map((e) => this.entityToSubTask(e));
881
+ return this.sortByOrder(tasks);
609
882
  }
610
883
  updateSubTaskStatus(taskId, status, options) {
611
884
  const subTask = this.getSubTask(taskId);
@@ -693,6 +966,7 @@ class DevPlanGraphStore {
693
966
  title: mt.title,
694
967
  priority: mt.priority,
695
968
  status: mt.status,
969
+ order: mt.order,
696
970
  total: subs.length,
697
971
  completed: subCompleted,
698
972
  percent: subs.length > 0 ? Math.round((subCompleted / subs.length) * 100) : 0,
@@ -908,6 +1182,45 @@ class DevPlanGraphStore {
908
1182
  return tasks;
909
1183
  }
910
1184
  // ==========================================================================
1185
+ // Document Hierarchy (文档层级关系)
1186
+ // ==========================================================================
1187
+ /**
1188
+ * 获取文档的直接子文档列表(通过 DOC_HAS_CHILD 出向关系)
1189
+ */
1190
+ getChildDocs(section, subSection) {
1191
+ const docEntity = this.findDocEntityBySection(section, subSection);
1192
+ if (!docEntity)
1193
+ return [];
1194
+ const rels = this.getOutRelations(docEntity.id, RT.DOC_HAS_CHILD);
1195
+ const children = [];
1196
+ for (const rel of rels) {
1197
+ const childEntity = this.graph.getEntity(rel.target);
1198
+ if (childEntity) {
1199
+ children.push(this.entityToDevPlanDoc(childEntity));
1200
+ }
1201
+ }
1202
+ return children;
1203
+ }
1204
+ /**
1205
+ * 获取文档树(递归,含所有后代文档)
1206
+ */
1207
+ getDocTree(section, subSection) {
1208
+ const doc = this.getSection(section, subSection);
1209
+ if (!doc)
1210
+ return null;
1211
+ return this.buildDocTree(doc);
1212
+ }
1213
+ /**
1214
+ * 递归构建文档树
1215
+ */
1216
+ buildDocTree(doc) {
1217
+ const children = this.getChildDocs(doc.section, doc.subSection);
1218
+ return {
1219
+ doc,
1220
+ children: children.map((child) => this.buildDocTree(child)),
1221
+ };
1222
+ }
1223
+ // ==========================================================================
911
1224
  // Utility
912
1225
  // ==========================================================================
913
1226
  sync() {
@@ -1042,13 +1355,32 @@ class DevPlanGraphStore {
1042
1355
  section: doc.section,
1043
1356
  subSection: doc.subSection,
1044
1357
  version: doc.version,
1358
+ parentDoc: doc.parentDoc || null,
1359
+ childDocs: doc.childDocs || [],
1045
1360
  },
1046
1361
  });
1047
- edges.push({
1048
- from: this.getProjectId(),
1049
- to: doc.id,
1050
- label: RT.HAS_DOCUMENT,
1051
- });
1362
+ // 子文档不连接项目节点,仅通过 doc_has_child 连接父文档
1363
+ if (!doc.parentDoc) {
1364
+ edges.push({
1365
+ from: this.getProjectId(),
1366
+ to: doc.id,
1367
+ label: RT.HAS_DOCUMENT,
1368
+ });
1369
+ }
1370
+ // doc_has_child 关系(文档 → 子文档)
1371
+ if (doc.childDocs?.length) {
1372
+ const docEntity = this.findDocEntityBySection(doc.section, doc.subSection);
1373
+ if (docEntity) {
1374
+ const childRels = this.getOutRelations(docEntity.id, RT.DOC_HAS_CHILD);
1375
+ for (const rel of childRels) {
1376
+ edges.push({
1377
+ from: doc.id,
1378
+ to: rel.target,
1379
+ label: RT.DOC_HAS_CHILD,
1380
+ });
1381
+ }
1382
+ }
1383
+ }
1052
1384
  }
1053
1385
  }
1054
1386
  // 模块节点
@@ -1124,6 +1456,46 @@ class DevPlanGraphStore {
1124
1456
  // ==========================================================================
1125
1457
  // Private Helpers
1126
1458
  // ==========================================================================
1459
+ /**
1460
+ * 获取下一个主任务的 order 值(当前最大 order + 1)
1461
+ */
1462
+ getNextMainTaskOrder() {
1463
+ const entities = this.findEntitiesByType(ET.MAIN_TASK);
1464
+ let maxOrder = 0;
1465
+ for (const e of entities) {
1466
+ const o = e.properties.order;
1467
+ if (typeof o === 'number' && o > maxOrder) {
1468
+ maxOrder = o;
1469
+ }
1470
+ }
1471
+ return maxOrder + 1;
1472
+ }
1473
+ /**
1474
+ * 获取下一个子任务的 order 值(当前父任务下最大 order + 1)
1475
+ */
1476
+ getNextSubTaskOrder(parentTaskId) {
1477
+ const entities = this.findEntitiesByType(ET.SUB_TASK).filter((e) => e.properties.parentTaskId === parentTaskId);
1478
+ let maxOrder = 0;
1479
+ for (const e of entities) {
1480
+ const o = e.properties.order;
1481
+ if (typeof o === 'number' && o > maxOrder) {
1482
+ maxOrder = o;
1483
+ }
1484
+ }
1485
+ return maxOrder + 1;
1486
+ }
1487
+ /**
1488
+ * 按 order 字段排序(order 为空的排到最后,order 相同则按 createdAt 排)
1489
+ */
1490
+ sortByOrder(items) {
1491
+ return items.sort((a, b) => {
1492
+ const oa = a.order != null ? a.order : Number.MAX_SAFE_INTEGER;
1493
+ const ob = b.order != null ? b.order : Number.MAX_SAFE_INTEGER;
1494
+ if (oa !== ob)
1495
+ return oa - ob;
1496
+ return a.createdAt - b.createdAt;
1497
+ });
1498
+ }
1127
1499
  refreshMainTaskCounts(mainTaskId) {
1128
1500
  const mainTask = this.getMainTask(mainTaskId);
1129
1501
  if (!mainTask)
@@ -1194,6 +1566,45 @@ class DevPlanGraphStore {
1194
1566
  }
1195
1567
  }
1196
1568
  }
1569
+ /**
1570
+ * 更新文档的父文档关系(DOC_HAS_CHILD)
1571
+ *
1572
+ * 移除旧的父文档关系,建立新的父文档关系。
1573
+ */
1574
+ updateParentDocRelation(docEntityId, oldParentDoc, newParentDoc) {
1575
+ const projectId = this.getProjectId();
1576
+ // 移除旧的父文档关系(入向 DOC_HAS_CHILD)
1577
+ if (oldParentDoc) {
1578
+ const [oldSection, oldSub] = oldParentDoc.split('|');
1579
+ const oldParentEntity = this.findDocEntityBySection(oldSection, oldSub || undefined);
1580
+ if (oldParentEntity) {
1581
+ const rel = this.graph.getRelationBetween(oldParentEntity.id, docEntityId);
1582
+ if (rel)
1583
+ this.graph.deleteRelation(rel.id);
1584
+ }
1585
+ }
1586
+ // 建立新的父文档关系
1587
+ if (newParentDoc) {
1588
+ const [newSection, newSub] = newParentDoc.split('|');
1589
+ const newParentEntity = this.findDocEntityBySection(newSection, newSub || undefined);
1590
+ if (newParentEntity) {
1591
+ this.graph.putRelation(newParentEntity.id, docEntityId, RT.DOC_HAS_CHILD);
1592
+ }
1593
+ // 从顶级变为子文档 → 移除 project -> doc 的 has_document 关系
1594
+ if (!oldParentDoc) {
1595
+ const hasDocRels = this.getInRelations(docEntityId, RT.HAS_DOCUMENT);
1596
+ for (const rel of hasDocRels) {
1597
+ if (rel.source === projectId) {
1598
+ this.graph.deleteRelation(rel.id);
1599
+ }
1600
+ }
1601
+ }
1602
+ }
1603
+ else if (oldParentDoc && !newParentDoc) {
1604
+ // 从子文档变为顶级 → 添加 project -> doc 关系
1605
+ this.graph.putRelation(projectId, docEntityId, RT.HAS_DOCUMENT);
1606
+ }
1607
+ }
1197
1608
  getCurrentGitCommit() {
1198
1609
  try {
1199
1610
  const { execSync } = require('child_process');
@@ -1226,6 +1637,83 @@ class DevPlanGraphStore {
1226
1637
  const empty = total - filled;
1227
1638
  return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
1228
1639
  }
1640
+ // ==========================================================================
1641
+ // Semantic Search Helpers
1642
+ // ==========================================================================
1643
+ /**
1644
+ * 自动为文档生成 Embedding 并索引到 SocialGraphV2 向量搜索层
1645
+ *
1646
+ * 将 title + content 拼接后生成 Embedding,以 entity.id 为 key 存入 HNSW 索引。
1647
+ * 失败时仅输出警告,不影响文档保存。
1648
+ */
1649
+ autoIndexDocument(entityId, title, content) {
1650
+ if (!this.semanticSearchReady || !this.synapse)
1651
+ return;
1652
+ try {
1653
+ const text = `${title}\n${content}`;
1654
+ const embedding = this.synapse.embed(text);
1655
+ this.graph.indexEntity(entityId, embedding);
1656
+ }
1657
+ catch (e) {
1658
+ console.warn(`[DevPlan] Failed to index document ${entityId}: ${e instanceof Error ? e.message : String(e)}`);
1659
+ }
1660
+ }
1661
+ /**
1662
+ * 字面搜索(标题/内容包含查询词)
1663
+ */
1664
+ literalSearch(query) {
1665
+ const queryLower = query.toLowerCase();
1666
+ return this.listSections().filter((doc) => doc.content.toLowerCase().includes(queryLower) ||
1667
+ doc.title.toLowerCase().includes(queryLower));
1668
+ }
1669
+ /**
1670
+ * RRF (Reciprocal Rank Fusion) 融合排序
1671
+ *
1672
+ * 将语义搜索和字面搜索的结果通过 RRF 公式融合:
1673
+ * score(d) = Σ 1/(k + rank_i(d))
1674
+ * 其中 k=60 是标准 RRF 常数。
1675
+ */
1676
+ rrfFusion(semanticHits, literalResults, limit, minScore) {
1677
+ const RRF_K = 60;
1678
+ const rrfScores = new Map();
1679
+ const docMap = new Map();
1680
+ // 语义搜索结果贡献
1681
+ for (let i = 0; i < semanticHits.length; i++) {
1682
+ const hit = semanticHits[i];
1683
+ const rrf = 1 / (RRF_K + i + 1);
1684
+ rrfScores.set(hit.entityId, (rrfScores.get(hit.entityId) || 0) + rrf);
1685
+ }
1686
+ // 字面搜索结果贡献
1687
+ for (let i = 0; i < literalResults.length; i++) {
1688
+ const doc = literalResults[i];
1689
+ const rrf = 1 / (RRF_K + i + 1);
1690
+ rrfScores.set(doc.id, (rrfScores.get(doc.id) || 0) + rrf);
1691
+ docMap.set(doc.id, doc);
1692
+ }
1693
+ // 按 RRF 评分排序
1694
+ const sorted = Array.from(rrfScores.entries())
1695
+ .sort((a, b) => b[1] - a[1]);
1696
+ // 组装结果
1697
+ const results = [];
1698
+ for (const [id, score] of sorted) {
1699
+ if (minScore > 0 && score < minScore)
1700
+ continue;
1701
+ if (results.length >= limit)
1702
+ break;
1703
+ // 优先从 docMap 获取(字面搜索已解析过的),否则从图中获取
1704
+ let doc = docMap.get(id);
1705
+ if (!doc) {
1706
+ const entity = this.graph.getEntity(id);
1707
+ if (entity && entity.properties?.projectName === this.projectName) {
1708
+ doc = this.entityToDevPlanDoc(entity);
1709
+ }
1710
+ }
1711
+ if (doc) {
1712
+ results.push({ ...doc, score });
1713
+ }
1714
+ }
1715
+ return results;
1716
+ }
1229
1717
  }
1230
1718
  exports.DevPlanGraphStore = DevPlanGraphStore;
1231
1719
  //# sourceMappingURL=dev-plan-graph-store.js.map