autosnippet 3.2.7 → 3.2.9

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 (147) hide show
  1. package/bin/cli.js +13 -5
  2. package/dashboard/dist/assets/index-BTAsOZv2.js +128 -0
  3. package/dashboard/dist/assets/index-C_72Ct98.css +1 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/lib/cli/AiScanService.js +26 -29
  6. package/lib/cli/SetupService.js +1 -1
  7. package/lib/core/AstAnalyzer.js +27 -5
  8. package/lib/core/analysis/CallEdgeResolver.js +402 -0
  9. package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
  10. package/lib/core/analysis/CallSiteExtractor.js +629 -0
  11. package/lib/core/analysis/DataFlowInferrer.js +57 -0
  12. package/lib/core/analysis/ImportPathResolver.js +189 -0
  13. package/lib/core/analysis/ImportRecord.js +105 -0
  14. package/lib/core/analysis/SymbolTableBuilder.js +211 -0
  15. package/lib/core/ast/ProjectGraph.js +8 -0
  16. package/lib/core/ast/lang-dart.js +352 -5
  17. package/lib/core/ast/lang-go.js +212 -10
  18. package/lib/core/ast/lang-java.js +205 -1
  19. package/lib/core/ast/lang-kotlin.js +330 -1
  20. package/lib/core/ast/lang-python.js +31 -2
  21. package/lib/core/ast/lang-rust.js +284 -3
  22. package/lib/core/ast/lang-swift.js +180 -1
  23. package/lib/core/ast/lang-typescript.js +290 -1
  24. package/lib/core/discovery/index.js +2 -2
  25. package/lib/external/ai/AiProvider.js +66 -172
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +23 -1
  27. package/lib/external/mcp/McpServer.js +1 -0
  28. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +1 -1
  29. package/lib/external/mcp/handlers/bootstrap/ExternalSubmissionTracker.js +3 -3
  30. package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +22 -1
  31. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +1 -1
  32. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
  33. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +8 -8
  34. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +1 -1
  35. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +311 -162
  36. package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +102 -7
  37. package/lib/external/mcp/handlers/bootstrap/shared/dimension-sop.js +1 -1
  38. package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
  39. package/lib/external/mcp/handlers/bootstrap-internal.js +19 -8
  40. package/lib/external/mcp/handlers/consolidated.js +9 -0
  41. package/lib/external/mcp/handlers/dimension-complete-external.js +6 -6
  42. package/lib/external/mcp/handlers/guard.js +3 -3
  43. package/lib/external/mcp/handlers/structure.js +62 -0
  44. package/lib/external/mcp/handlers/wiki-external.js +66 -3
  45. package/lib/external/mcp/tools.js +36 -1
  46. package/lib/http/HttpServer.js +1 -1
  47. package/lib/http/middleware/requestLogger.js +1 -0
  48. package/lib/http/routes/ai.js +240 -35
  49. package/lib/http/routes/candidates.js +2 -3
  50. package/lib/http/routes/extract.js +13 -11
  51. package/lib/http/routes/modules.js +2 -2
  52. package/lib/http/routes/recipes.js +9 -5
  53. package/lib/http/routes/remote.js +149 -270
  54. package/lib/http/routes/violations.js +0 -54
  55. package/lib/http/utils/sse-sessions.js +1 -1
  56. package/lib/infrastructure/logging/Logger.js +5 -4
  57. package/lib/infrastructure/monitoring/PerformanceMonitor.js +3 -2
  58. package/lib/injection/ServiceContainer.js +70 -28
  59. package/lib/platform/ScreenCaptureService.js +177 -0
  60. package/lib/platform/ios/index.js +2 -2
  61. package/lib/platform/ios/routes/spm.js +2 -2
  62. package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
  63. package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
  64. package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
  65. package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
  66. package/lib/service/agent/AgentEventBus.js +207 -0
  67. package/lib/service/agent/AgentFactory.js +490 -0
  68. package/lib/service/agent/AgentMessage.js +240 -0
  69. package/lib/service/agent/AgentRouter.js +228 -0
  70. package/lib/service/agent/AgentRuntime.js +1016 -0
  71. package/lib/service/agent/AgentState.js +217 -0
  72. package/lib/service/agent/IntentClassifier.js +331 -0
  73. package/lib/service/agent/LarkTransport.js +389 -0
  74. package/lib/service/agent/capabilities.js +408 -0
  75. package/lib/service/{chat → agent/context}/ContextWindow.js +37 -12
  76. package/lib/service/{chat → agent/context}/ExplorationTracker.js +77 -22
  77. package/lib/service/{chat → agent/core}/ChatAgentPrompts.js +14 -2
  78. package/lib/service/agent/core/LoopContext.js +170 -0
  79. package/lib/service/agent/core/MessageAdapter.js +223 -0
  80. package/lib/service/agent/core/ToolExecutionPipeline.js +376 -0
  81. package/lib/service/{chat → agent/domain}/ChatAgentTasks.js +19 -98
  82. package/lib/service/{chat → agent/domain}/EpisodicConsolidator.js +7 -7
  83. package/lib/service/{chat → agent/domain}/EvidenceCollector.js +4 -2
  84. package/lib/service/{chat/AnalystAgent.js → agent/domain/insight-analyst.js} +37 -172
  85. package/lib/service/{chat/HandoffProtocol.js → agent/domain/insight-gate.js} +91 -123
  86. package/lib/service/agent/domain/insight-producer.js +267 -0
  87. package/lib/service/agent/domain/scan-prompts.js +105 -0
  88. package/lib/service/agent/forced-summary.js +266 -0
  89. package/lib/service/agent/index.js +91 -0
  90. package/lib/service/{chat → agent}/memory/ActiveContext.js +3 -1
  91. package/lib/service/{chat → agent}/memory/MemoryCoordinator.js +7 -7
  92. package/lib/service/{chat/ProjectSemanticMemory.js → agent/memory/PersistentMemory.js} +359 -89
  93. package/lib/service/{chat → agent}/memory/SessionStore.js +5 -4
  94. package/lib/service/{chat → agent}/memory/index.js +1 -1
  95. package/lib/service/agent/policies.js +442 -0
  96. package/lib/service/agent/presets.js +303 -0
  97. package/lib/service/agent/strategies.js +717 -0
  98. package/lib/service/{chat → agent/tools}/ToolRegistry.js +3 -3
  99. package/lib/service/agent/tools/ai-analysis.js +75 -0
  100. package/lib/service/{chat → agent}/tools/ast-graph.js +229 -32
  101. package/lib/service/{chat → agent}/tools/composite.js +2 -1
  102. package/lib/service/{chat → agent}/tools/guard.js +1 -121
  103. package/lib/service/{chat → agent}/tools/index.js +33 -22
  104. package/lib/service/{chat → agent}/tools/infrastructure.js +6 -1
  105. package/lib/service/agent/tools/knowledge-graph.js +112 -0
  106. package/lib/service/agent/tools/scan-recipe.js +189 -0
  107. package/lib/service/agent/tools/system-interaction.js +476 -0
  108. package/lib/service/automation/DirectiveDetector.js +0 -1
  109. package/lib/service/automation/FileWatcher.js +0 -8
  110. package/lib/service/automation/handlers/CreateHandler.js +7 -3
  111. package/lib/service/automation/handlers/DraftHandler.js +7 -6
  112. package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
  113. package/lib/service/knowledge/CodeEntityGraph.js +327 -2
  114. package/lib/service/knowledge/KnowledgeService.js +5 -1
  115. package/lib/service/module/ModuleService.js +49 -73
  116. package/lib/service/skills/SignalCollector.js +26 -19
  117. package/lib/service/snippet/codecs/VSCodeCodec.js +1 -1
  118. package/lib/service/wiki/WikiGenerator.js +1 -1
  119. package/lib/shared/FieldSpec.js +1 -1
  120. package/lib/shared/PathGuard.js +1 -1
  121. package/lib/shared/StyleGuide.js +1 -1
  122. package/package.json +4 -1
  123. package/resources/native-ui/screenshot.swift +228 -0
  124. package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
  125. package/dashboard/dist/assets/index-DfHY_3ln.js +0 -128
  126. package/lib/core/discovery/SpmDiscoverer.js +0 -5
  127. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +0 -749
  128. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +0 -277
  129. package/lib/http/routes/spm.js +0 -5
  130. package/lib/infrastructure/external/XcodeAutomation.js +0 -15
  131. package/lib/service/chat/ChatAgent.js +0 -1602
  132. package/lib/service/chat/Memory.js +0 -161
  133. package/lib/service/chat/ProducerAgent.js +0 -431
  134. package/lib/service/chat/ReasoningTrace.js +0 -523
  135. package/lib/service/chat/TaskPipeline.js +0 -357
  136. package/lib/service/chat/WorkingMemory.js +0 -357
  137. package/lib/service/chat/memory/PersistentMemory.js +0 -450
  138. package/lib/service/chat/tools/ai-analysis.js +0 -267
  139. package/lib/service/chat/tools/knowledge-graph.js +0 -234
  140. package/lib/service/chat/tools.js +0 -18
  141. package/lib/service/snippet/PlaceholderConverter.js +0 -5
  142. package/lib/service/snippet/codecs/XcodeCodec.js +0 -5
  143. /package/lib/service/{chat → agent}/ConversationStore.js +0 -0
  144. /package/lib/service/{chat → agent}/tools/_shared.js +0 -0
  145. /package/lib/service/{chat → agent}/tools/lifecycle.js +0 -0
  146. /package/lib/service/{chat → agent}/tools/project-access.js +0 -0
  147. /package/lib/service/{chat → agent}/tools/query.js +0 -0
@@ -35,11 +35,12 @@ export class CursorDeliveryPipeline {
35
35
  * @param {string} [options.projectName] - 项目名称
36
36
  * @param {Object} [options.logger] - 日志器
37
37
  */
38
- constructor({ knowledgeService, projectRoot, projectName, logger }) {
38
+ constructor({ knowledgeService, projectRoot, projectName, logger, database }) {
39
39
  this.knowledgeService = knowledgeService;
40
40
  this.projectRoot = projectRoot;
41
41
  this.projectName = projectName || this._inferProjectName(projectRoot);
42
42
  this.logger = logger || console;
43
+ this.database = database || null;
43
44
 
44
45
  // 子模块
45
46
  this.compressor = new KnowledgeCompressor();
@@ -87,6 +88,18 @@ export class CursorDeliveryPipeline {
87
88
  const channelB = this._generateChannelB(patterns, facts);
88
89
  stats.channelB = channelB;
89
90
 
91
+ // ── Channel B+: Call Graph Architecture Rules (Phase 5.2) ──
92
+ const archResult = this._generateCallGraphArchitectureRules();
93
+ if (archResult) {
94
+ stats.channelB.topicCount++;
95
+ stats.channelB.totalTokens += archResult.tokensUsed;
96
+ stats.channelB.topics['call-architecture'] = {
97
+ patternsCount: archResult.insightsCount,
98
+ factsCount: 0,
99
+ tokensUsed: archResult.tokensUsed,
100
+ };
101
+ }
102
+
90
103
  // ── Channel C: Skills Sync ──
91
104
  const channelC = await this._generateChannelC();
92
105
  stats.channelC = channelC;
@@ -326,6 +339,159 @@ export class CursorDeliveryPipeline {
326
339
  return result;
327
340
  }
328
341
 
342
+ /**
343
+ * Channel B+ — Call Graph Architecture Rules (Phase 5.2)
344
+ * 从调用图拓扑分析架构分层,生成 architecture smart rule
345
+ * @private
346
+ * @returns {{ insightsCount: number, tokensUsed: number, filePath: string }|null}
347
+ */
348
+ _generateCallGraphArchitectureRules() {
349
+ if (!this.database) return null;
350
+
351
+ try {
352
+ const db = typeof this.database.getDb === 'function' ? this.database.getDb() : this.database;
353
+
354
+ // 查询调用边中的跨目录调用模式
355
+ const callEdges = db.prepare(
356
+ `SELECT from_id, to_id, metadata_json FROM knowledge_edges
357
+ WHERE relation = 'calls' AND metadata_json LIKE '%phase5%'`
358
+ ).all();
359
+
360
+ if (!callEdges || callEdges.length < 5) return null;
361
+
362
+ // 提取 caller/callee 对应的文件路径
363
+ const entityFiles = new Map();
364
+ const entities = db.prepare(
365
+ `SELECT entity_id, file_path FROM code_entities WHERE entity_type = 'method'`
366
+ ).all();
367
+ for (const e of entities) {
368
+ entityFiles.set(e.entity_id, e.file_path);
369
+ }
370
+
371
+ // 构建目录级调用矩阵
372
+ const dirCalls = new Map(); // 'src/controllers' → Map('src/services' → count)
373
+ for (const edge of callEdges) {
374
+ const callerFile = entityFiles.get(edge.from_id);
375
+ const calleeFile = entityFiles.get(edge.to_id);
376
+ if (!callerFile || !calleeFile || callerFile === calleeFile) continue;
377
+
378
+ const callerDir = this._extractLayerDir(callerFile);
379
+ const calleeDir = this._extractLayerDir(calleeFile);
380
+ if (!callerDir || !calleeDir || callerDir === calleeDir) continue;
381
+
382
+ if (!dirCalls.has(callerDir)) dirCalls.set(callerDir, new Map());
383
+ const targets = dirCalls.get(callerDir);
384
+ targets.set(calleeDir, (targets.get(calleeDir) || 0) + 1);
385
+ }
386
+
387
+ if (dirCalls.size === 0) return null;
388
+
389
+ // 检测架构层: 入度高(被调用多)的目录是底层服务, 出度高(调用多)的是上层
390
+ const dirInDegree = new Map();
391
+ const dirOutDegree = new Map();
392
+ for (const [from, targets] of dirCalls) {
393
+ for (const [to, count] of targets) {
394
+ dirOutDegree.set(from, (dirOutDegree.get(from) || 0) + count);
395
+ dirInDegree.set(to, (dirInDegree.get(to) || 0) + count);
396
+ }
397
+ }
398
+
399
+ // 生成架构洞察
400
+ const lines = [];
401
+ lines.push('## Call Graph Architecture');
402
+ lines.push('');
403
+
404
+ // 分层推断: 按 (inDegree - outDegree) 排序, 值越大 = 越底层
405
+ const allDirs = new Set([...dirInDegree.keys(), ...dirOutDegree.keys()]);
406
+ const layers = [...allDirs].map((dir) => ({
407
+ dir,
408
+ inDegree: dirInDegree.get(dir) || 0,
409
+ outDegree: dirOutDegree.get(dir) || 0,
410
+ layerScore: (dirInDegree.get(dir) || 0) - (dirOutDegree.get(dir) || 0),
411
+ })).sort((a, b) => b.layerScore - a.layerScore);
412
+
413
+ // 分层标签
414
+ const total = layers.length;
415
+ let insightsCount = 0;
416
+
417
+ if (total >= 2) {
418
+ lines.push('### Architecture Layers (inferred from call graph)');
419
+ lines.push('');
420
+
421
+ for (let i = 0; i < layers.length && i < 10; i++) {
422
+ const l = layers[i];
423
+ let layerLabel;
424
+ if (i < total * 0.33) layerLabel = '🔽 low-level (service/repository)';
425
+ else if (i < total * 0.66) layerLabel = '↔️ mid-level (business logic)';
426
+ else layerLabel = '🔼 high-level (controller/UI)';
427
+
428
+ lines.push(`- \`${l.dir}/\` — ${layerLabel} (in:${l.inDegree} out:${l.outDegree})`);
429
+ insightsCount++;
430
+ }
431
+ lines.push('');
432
+ }
433
+
434
+ // 核心调用链
435
+ const hotPaths = [...dirCalls.entries()]
436
+ .flatMap(([from, targets]) =>
437
+ [...targets.entries()].map(([to, count]) => ({ from, to, count }))
438
+ )
439
+ .sort((a, b) => b.count - a.count)
440
+ .slice(0, 8);
441
+
442
+ if (hotPaths.length > 0) {
443
+ lines.push('### Key Call Paths');
444
+ lines.push('');
445
+ for (const p of hotPaths) {
446
+ lines.push(`- \`${p.from}/\` → \`${p.to}/\` (${p.count} calls)`);
447
+ insightsCount++;
448
+ }
449
+ lines.push('');
450
+ }
451
+
452
+ if (insightsCount === 0) return null;
453
+
454
+ // 构建 description (用于 smart rule 关联性判断)
455
+ const dirList = layers.map((l) => l.dir).join(', ');
456
+ const description = `Architecture layer analysis for ${this.projectName}. ` +
457
+ `Relevant when editing files in: ${dirList}. ` +
458
+ `Call graph shows ${callEdges.length} cross-file call relationships.`;
459
+
460
+ const body = lines.join('\n');
461
+ const writeResult = this.rulesGenerator.writeSmartRules('call-architecture', body, description);
462
+
463
+ this.logger.info?.(
464
+ `[CursorDelivery] Channel B+: call-architecture — ${insightsCount} insights → ${writeResult.filePath}`
465
+ );
466
+
467
+ return {
468
+ insightsCount,
469
+ tokensUsed: writeResult.tokensUsed,
470
+ filePath: writeResult.filePath,
471
+ };
472
+ } catch (err) {
473
+ this.logger.warn?.(`[CursorDelivery] Call graph architecture rules failed: ${err.message}`);
474
+ return null;
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 从文件路径中提取层级目录 (第一或第二级有意义的目录)
480
+ * @private
481
+ */
482
+ _extractLayerDir(filePath) {
483
+ if (!filePath) return null;
484
+ const parts = filePath.split('/').filter(Boolean);
485
+ // 跳过 src/ lib/ app/ 等通用前缀
486
+ const skipPrefixes = new Set(['src', 'lib', 'app', 'pkg', 'internal', 'cmd']);
487
+ let startIdx = 0;
488
+ if (parts.length > 1 && skipPrefixes.has(parts[0])) {
489
+ startIdx = 1;
490
+ }
491
+ // 取第一个有意义的目录
492
+ return parts[startIdx] || parts[0] || null;
493
+ }
494
+
329
495
  /**
330
496
  * Channel C 生成
331
497
  * @private
@@ -664,19 +664,281 @@ export class CodeEntityGraph {
664
664
  lines.push('');
665
665
  }
666
666
 
667
+ // 调用图热路径 (Phase 5)
668
+ try {
669
+ const hotCallees = this.db
670
+ .prepare(
671
+ `SELECT to_id, COUNT(*) as call_count
672
+ FROM knowledge_edges
673
+ WHERE relation = 'calls'
674
+ GROUP BY to_id
675
+ ORDER BY call_count DESC
676
+ LIMIT 15`
677
+ )
678
+ .all();
679
+
680
+ if (hotCallees.length > 0) {
681
+ lines.push('### 调用图热路径 (Call Graph Hot Paths)');
682
+ for (const row of hotCallees) {
683
+ // 查找前几个调用者
684
+ const topCallers = this.db
685
+ .prepare(
686
+ `SELECT from_id FROM knowledge_edges
687
+ WHERE relation = 'calls' AND to_id = ?
688
+ LIMIT 3`
689
+ )
690
+ .all(row.to_id);
691
+ const callerNames = topCallers.map((c) => `\`${c.from_id}\``).join(', ');
692
+ lines.push(
693
+ `- \`${row.to_id}\` ← ${row.call_count} 次调用 (${callerNames}${topCallers.length < row.call_count ? '...' : ''})`
694
+ );
695
+ }
696
+ lines.push('');
697
+ }
698
+
699
+ // 数据流边摘要
700
+ const dataFlowCount = this.db
701
+ .prepare(
702
+ `SELECT COUNT(*) as cnt FROM knowledge_edges WHERE relation = 'data_flow'`
703
+ )
704
+ .get();
705
+
706
+ if (dataFlowCount?.cnt > 0) {
707
+ lines.push(`### 数据流`);
708
+ lines.push(`- 数据流边: ${dataFlowCount.cnt} 条`);
709
+ lines.push('');
710
+ }
711
+ } catch (_e) {
712
+ // 调用图数据可能尚未填充, 静默跳过
713
+ }
714
+
667
715
  return lines.join('\n');
668
716
  }
669
717
 
718
+ // ────────────────────────────────────────────
719
+ // Public API — Phase 5: 调用图
720
+ // ────────────────────────────────────────────
721
+
722
+ /**
723
+ * 从解析后的调用边填充图谱 (Phase 5)
724
+ *
725
+ * @param {Array<{ caller: string, callee: string, callType: string, resolveMethod: string, line: number, file: string, isAwait: boolean }>} callEdges
726
+ * @param {Array<{ from: string, to: string, flowType: string, direction: string }>} dataFlowEdges
727
+ * @returns {GraphPopulateResult}
728
+ */
729
+ populateCallGraph(callEdges, dataFlowEdges) {
730
+ const t0 = Date.now();
731
+ let edges = 0;
732
+ let entities = 0;
733
+
734
+ const run = this.db.transaction(() => {
735
+ // ── 注册方法实体 (确保 from/to 的 entity 存在) ──
736
+ const registeredMethods = new Set();
737
+ for (const edge of callEdges) {
738
+ for (const fqn of [edge.caller, edge.callee]) {
739
+ if (registeredMethods.has(fqn)) continue;
740
+ registeredMethods.add(fqn);
741
+
742
+ const entityId = this._extractEntityId(fqn);
743
+ const entityName = entityId; // 短名
744
+ const filePath = fqn.includes('::') ? fqn.split('::')[0] : null;
745
+
746
+ this.#upsertEntity({
747
+ entityId,
748
+ entityType: 'method',
749
+ name: entityName,
750
+ filePath,
751
+ metadata: { fqn, source: 'phase5-call-graph' },
752
+ });
753
+ entities++;
754
+ }
755
+ }
756
+
757
+ // ── 调用边 (聚合同一 caller-callee 对的多次调用,解决 Issue #4) ──
758
+ const aggregated = new Map(); // key = "callerId|calleeId" → aggregated metadata
759
+ for (const edge of callEdges) {
760
+ const callerId = this._extractEntityId(edge.caller);
761
+ const calleeId = this._extractEntityId(edge.callee);
762
+ const key = `${callerId}|${calleeId}`;
763
+
764
+ if (aggregated.has(key)) {
765
+ const agg = aggregated.get(key);
766
+ agg.callCount++;
767
+ agg.callSites.push({ line: edge.line, isAwait: edge.isAwait });
768
+ // 提升权重: direct 优先
769
+ if (edge.resolveMethod === 'direct') agg.resolveMethod = 'direct';
770
+ if (edge.isAwait) agg.hasAwait = true;
771
+ } else {
772
+ aggregated.set(key, {
773
+ callerId,
774
+ calleeId,
775
+ callType: edge.callType,
776
+ resolveMethod: edge.resolveMethod,
777
+ file: edge.file,
778
+ hasAwait: edge.isAwait,
779
+ callCount: 1,
780
+ callSites: [{ line: edge.line, isAwait: edge.isAwait }],
781
+ });
782
+ }
783
+ }
784
+
785
+ for (const agg of aggregated.values()) {
786
+ this.#addEdge(agg.callerId, 'method', agg.calleeId, 'method', 'calls', {
787
+ weight: agg.resolveMethod === 'direct' ? 1.0 : 0.6,
788
+ source: 'phase5-call-graph',
789
+ callType: agg.callType,
790
+ resolveMethod: agg.resolveMethod,
791
+ file: agg.file,
792
+ isAwait: agg.hasAwait,
793
+ callCount: agg.callCount,
794
+ callSites: agg.callSites.slice(0, 10), // 最多保留 10 个调用点
795
+ });
796
+ edges++;
797
+ }
798
+
799
+ // ── 数据流边 ──
800
+ for (const flow of dataFlowEdges) {
801
+ const fromId = this._extractEntityId(flow.from);
802
+ const toId = this._extractEntityId(flow.to);
803
+
804
+ this.#addEdge(fromId, 'method', toId, 'method', 'data_flow', {
805
+ weight: 0.5,
806
+ source: 'phase5-data-flow',
807
+ flowType: flow.flowType,
808
+ direction: flow.direction,
809
+ });
810
+ edges++;
811
+ }
812
+ });
813
+
814
+ run();
815
+
816
+ const result = { entitiesUpserted: entities, edgesCreated: edges, durationMs: Date.now() - t0 };
817
+ this.log.info(
818
+ `[CodeEntityGraph] Call graph: ${callEdges.length} call edges, ${dataFlowEdges.length} data flow edges, ${entities} method entities (${result.durationMs}ms)`
819
+ );
820
+ return result;
821
+ }
822
+
823
+ /**
824
+ * 获取调用者 — 谁调用了这个方法?
825
+ *
826
+ * @param {string} methodId — "ClassName.methodName" 或 FQN
827
+ * @param {number} [maxDepth=2]
828
+ * @returns {Array<{ caller: string, depth: number, callType: string }>}
829
+ */
830
+ getCallers(methodId, maxDepth = 2) {
831
+ const results = [];
832
+ const visited = new Set();
833
+ const queue = [{ id: methodId, depth: 0 }];
834
+
835
+ while (queue.length > 0) {
836
+ const { id, depth } = queue.shift();
837
+ if (depth >= maxDepth || visited.has(id)) continue;
838
+ visited.add(id);
839
+
840
+ const callers = this.stmts.getCallers.all(id);
841
+
842
+ for (const row of callers) {
843
+ const meta = JSON.parse(row.metadata_json || '{}');
844
+ results.push({
845
+ caller: row.from_id,
846
+ depth: depth + 1,
847
+ callType: meta.callType || 'unknown',
848
+ });
849
+ if (depth + 1 < maxDepth) {
850
+ queue.push({ id: row.from_id, depth: depth + 1 });
851
+ }
852
+ }
853
+ }
854
+
855
+ return results;
856
+ }
857
+
858
+ /**
859
+ * 获取被调用者 — 这个方法调用了谁?
860
+ *
861
+ * @param {string} methodId — "ClassName.methodName" 或 FQN
862
+ * @param {number} [maxDepth=2]
863
+ * @returns {Array<{ callee: string, depth: number, callType: string }>}
864
+ */
865
+ getCallees(methodId, maxDepth = 2) {
866
+ const results = [];
867
+ const visited = new Set();
868
+ const queue = [{ id: methodId, depth: 0 }];
869
+
870
+ while (queue.length > 0) {
871
+ const { id, depth } = queue.shift();
872
+ if (depth >= maxDepth || visited.has(id)) continue;
873
+ visited.add(id);
874
+
875
+ const callees = this.stmts.getCallees.all(id);
876
+
877
+ for (const row of callees) {
878
+ const meta = JSON.parse(row.metadata_json || '{}');
879
+ results.push({
880
+ callee: row.to_id,
881
+ depth: depth + 1,
882
+ callType: meta.callType || 'unknown',
883
+ });
884
+ if (depth + 1 < maxDepth) {
885
+ queue.push({ id: row.to_id, depth: depth + 1 });
886
+ }
887
+ }
888
+ }
889
+
890
+ return results;
891
+ }
892
+
893
+ /**
894
+ * 获取方法的 Impact Radius (基于调用图)
895
+ * — 修改此方法可能影响哪些上游方法?
896
+ *
897
+ * @param {string} methodId — "ClassName.methodName"
898
+ * @returns {{ directCallers: number, transitiveCallers: number, affectedFiles: string[] }}
899
+ */
900
+ getCallImpactRadius(methodId) {
901
+ const callers = this.getCallers(methodId, 3);
902
+ const affectedFiles = new Set();
903
+
904
+ for (const c of callers) {
905
+ const entity = this.getEntity(c.caller, 'method');
906
+ if (entity?.filePath) affectedFiles.add(entity.filePath);
907
+ }
908
+
909
+ return {
910
+ directCallers: callers.filter((c) => c.depth === 1).length,
911
+ transitiveCallers: callers.length,
912
+ affectedFiles: [...affectedFiles],
913
+ };
914
+ }
915
+
916
+ /**
917
+ * 从 FQN 中提取短 Entity ID
918
+ *
919
+ * "src/service/UserService.ts::UserService.getUser" → "UserService.getUser"
920
+ * "src/utils/helpers.ts::formatDate" → "formatDate"
921
+ *
922
+ * @param {string} fqn
923
+ * @returns {string}
924
+ */
925
+ _extractEntityId(fqn) {
926
+ if (fqn.includes('::')) {
927
+ return fqn.split('::')[1];
928
+ }
929
+ return fqn;
930
+ }
931
+
670
932
  /**
671
933
  * 清除项目的所有代码实体 (重新 populate 前调用)
672
934
  */
673
935
  clearProject() {
674
936
  const run = this.db.transaction(() => {
675
937
  this.stmts.clearEntities.run(this.projectRoot);
676
- // 只清除 AST 产出的边 (保留 recipe/module 边)
938
+ // 清除 AST 产出的边 + Phase 5 调用图边 (保留 recipe/module 边)
677
939
  this.db
678
940
  .prepare(
679
- `DELETE FROM knowledge_edges WHERE metadata_json LIKE '%ast-bootstrap%' OR metadata_json LIKE '%ast-pattern-detection%'`
941
+ `DELETE FROM knowledge_edges WHERE metadata_json LIKE '%ast-bootstrap%' OR metadata_json LIKE '%ast-pattern-detection%' OR metadata_json LIKE '%phase5-%'`
680
942
  )
681
943
  .run();
682
944
  });
@@ -684,6 +946,55 @@ export class CodeEntityGraph {
684
946
  this.log.info(`[CodeEntityGraph] Cleared entities for project: ${this.projectRoot}`);
685
947
  }
686
948
 
949
+ /**
950
+ * 增量清除 — 仅删除指定文件的 call graph 边和 method 实体
951
+ *
952
+ * @param {string[]} filePaths — 变更文件的相对路径列表
953
+ * @returns {{ deletedEdges: number, deletedEntities: number }}
954
+ */
955
+ clearCallGraphForFiles(filePaths) {
956
+ if (!filePaths?.length) return { deletedEdges: 0, deletedEntities: 0 };
957
+
958
+ let deletedEdges = 0;
959
+ let deletedEntities = 0;
960
+
961
+ const run = this.db.transaction(() => {
962
+ // 1. 删除相关 call edges (metadata_json 包含 file 字段)
963
+ const deleteEdgesStmt = this.db.prepare(
964
+ `DELETE FROM knowledge_edges
965
+ WHERE metadata_json LIKE ?
966
+ AND (relation = 'calls' OR relation = 'data_flow')
967
+ AND metadata_json LIKE '%phase5-%'`
968
+ );
969
+
970
+ for (const filePath of filePaths) {
971
+ // 匹配 metadata 中 "file":"xxx" 字段
972
+ const result = deleteEdgesStmt.run(`%"file":"${filePath}"%`);
973
+ deletedEdges += result.changes;
974
+ }
975
+
976
+ // 2. 删除相关 method 实体
977
+ const deleteEntitiesStmt = this.db.prepare(
978
+ `DELETE FROM code_entities
979
+ WHERE file_path = ? AND entity_type = 'method' AND project_root = ?`
980
+ );
981
+
982
+ for (const filePath of filePaths) {
983
+ const result = deleteEntitiesStmt.run(filePath, this.projectRoot);
984
+ deletedEntities += result.changes;
985
+ }
986
+ });
987
+
988
+ run();
989
+
990
+ this.log.info(
991
+ `[CodeEntityGraph] Incremental clear: ${deletedEdges} edges, ${deletedEntities} entities ` +
992
+ `for ${filePaths.length} files`
993
+ );
994
+
995
+ return { deletedEdges, deletedEntities };
996
+ }
997
+
687
998
  // ────────────────────────────────────────────
688
999
  // Private — Schema & Statements
689
1000
  // ────────────────────────────────────────────
@@ -738,6 +1049,19 @@ export class CodeEntityGraph {
738
1049
  INSERT OR REPLACE INTO knowledge_edges (from_id, from_type, to_id, to_type, relation, weight, metadata_json, created_at, updated_at)
739
1050
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
740
1051
  `),
1052
+ // Phase 5: 调用图查询 (pre-prepared 避免每次调用都创建)
1053
+ getCallers: this.db.prepare(
1054
+ `SELECT from_id, from_type, metadata_json FROM knowledge_edges
1055
+ WHERE to_id = ? AND relation = 'calls'`
1056
+ ),
1057
+ getCallees: this.db.prepare(
1058
+ `SELECT to_id, to_type, metadata_json FROM knowledge_edges
1059
+ WHERE from_id = ? AND relation = 'calls'`
1060
+ ),
1061
+ getEdge: this.db.prepare(
1062
+ `SELECT metadata_json FROM knowledge_edges
1063
+ WHERE from_id = ? AND from_type = ? AND to_id = ? AND to_type = ? AND relation = ?`
1064
+ ),
741
1065
  };
742
1066
  }
743
1067
 
@@ -788,6 +1112,7 @@ export class CodeEntityGraph {
788
1112
  * 从 AST 数据推断实体类型
789
1113
  */
790
1114
  #inferEntityType(name, astSummary) {
1115
+ if (!name) return 'class'; // guard against undefined
791
1116
  if (astSummary.protocols?.some((p) => p.name === name)) {
792
1117
  return 'protocol';
793
1118
  }
@@ -75,7 +75,7 @@ export class KnowledgeService {
75
75
  }
76
76
  }
77
77
 
78
- // ── ConfidenceRouter — 仅标记 auto_approvable,不改变 lifecycle ──
78
+ // ── ConfidenceRouter — 标记 auto_approvable ──
79
79
  if (this._confidenceRouter) {
80
80
  const route = await this._confidenceRouter.route(entry);
81
81
  if (route.action === 'auto_approve') {
@@ -84,6 +84,10 @@ export class KnowledgeService {
84
84
  // reject / pending 都保持 pending 状态,等待人工审核
85
85
  }
86
86
 
87
+ // 注意: Bootstrap 候选保持 pending 状态,由 Dashboard 审核后发布。
88
+ // autoApprovable 标记保留,供前端显示「推荐批准」徽章。
89
+ // CursorDelivery 已支持高置信度 pending 条目的交付。
90
+
87
91
  const saved = await this.repository.create(entry);
88
92
 
89
93
  // 同步 relations → knowledge_edges