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.
- package/bin/cli.js +13 -5
- package/dashboard/dist/assets/index-BTAsOZv2.js +128 -0
- package/dashboard/dist/assets/index-C_72Ct98.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/lib/cli/AiScanService.js +26 -29
- package/lib/cli/SetupService.js +1 -1
- package/lib/core/AstAnalyzer.js +27 -5
- package/lib/core/analysis/CallEdgeResolver.js +402 -0
- package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
- package/lib/core/analysis/CallSiteExtractor.js +629 -0
- package/lib/core/analysis/DataFlowInferrer.js +57 -0
- package/lib/core/analysis/ImportPathResolver.js +189 -0
- package/lib/core/analysis/ImportRecord.js +105 -0
- package/lib/core/analysis/SymbolTableBuilder.js +211 -0
- package/lib/core/ast/ProjectGraph.js +8 -0
- package/lib/core/ast/lang-dart.js +352 -5
- package/lib/core/ast/lang-go.js +212 -10
- package/lib/core/ast/lang-java.js +205 -1
- package/lib/core/ast/lang-kotlin.js +330 -1
- package/lib/core/ast/lang-python.js +31 -2
- package/lib/core/ast/lang-rust.js +284 -3
- package/lib/core/ast/lang-swift.js +180 -1
- package/lib/core/ast/lang-typescript.js +290 -1
- package/lib/core/discovery/index.js +2 -2
- package/lib/external/ai/AiProvider.js +66 -172
- package/lib/external/ai/providers/GoogleGeminiProvider.js +23 -1
- package/lib/external/mcp/McpServer.js +1 -0
- package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/ExternalSubmissionTracker.js +3 -3
- package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +22 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +8 -8
- package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +311 -162
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +102 -7
- package/lib/external/mcp/handlers/bootstrap/shared/dimension-sop.js +1 -1
- package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
- package/lib/external/mcp/handlers/bootstrap-internal.js +19 -8
- package/lib/external/mcp/handlers/consolidated.js +9 -0
- package/lib/external/mcp/handlers/dimension-complete-external.js +6 -6
- package/lib/external/mcp/handlers/guard.js +3 -3
- package/lib/external/mcp/handlers/structure.js +62 -0
- package/lib/external/mcp/handlers/wiki-external.js +66 -3
- package/lib/external/mcp/tools.js +36 -1
- package/lib/http/HttpServer.js +1 -1
- package/lib/http/middleware/requestLogger.js +1 -0
- package/lib/http/routes/ai.js +240 -35
- package/lib/http/routes/candidates.js +2 -3
- package/lib/http/routes/extract.js +13 -11
- package/lib/http/routes/modules.js +2 -2
- package/lib/http/routes/recipes.js +9 -5
- package/lib/http/routes/remote.js +149 -270
- package/lib/http/routes/violations.js +0 -54
- package/lib/http/utils/sse-sessions.js +1 -1
- package/lib/infrastructure/logging/Logger.js +5 -4
- package/lib/infrastructure/monitoring/PerformanceMonitor.js +3 -2
- package/lib/injection/ServiceContainer.js +70 -28
- package/lib/platform/ScreenCaptureService.js +177 -0
- package/lib/platform/ios/index.js +2 -2
- package/lib/platform/ios/routes/spm.js +2 -2
- package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
- package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
- package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
- package/lib/service/agent/AgentEventBus.js +207 -0
- package/lib/service/agent/AgentFactory.js +490 -0
- package/lib/service/agent/AgentMessage.js +240 -0
- package/lib/service/agent/AgentRouter.js +228 -0
- package/lib/service/agent/AgentRuntime.js +1016 -0
- package/lib/service/agent/AgentState.js +217 -0
- package/lib/service/agent/IntentClassifier.js +331 -0
- package/lib/service/agent/LarkTransport.js +389 -0
- package/lib/service/agent/capabilities.js +408 -0
- package/lib/service/{chat → agent/context}/ContextWindow.js +37 -12
- package/lib/service/{chat → agent/context}/ExplorationTracker.js +77 -22
- package/lib/service/{chat → agent/core}/ChatAgentPrompts.js +14 -2
- package/lib/service/agent/core/LoopContext.js +170 -0
- package/lib/service/agent/core/MessageAdapter.js +223 -0
- package/lib/service/agent/core/ToolExecutionPipeline.js +376 -0
- package/lib/service/{chat → agent/domain}/ChatAgentTasks.js +19 -98
- package/lib/service/{chat → agent/domain}/EpisodicConsolidator.js +7 -7
- package/lib/service/{chat → agent/domain}/EvidenceCollector.js +4 -2
- package/lib/service/{chat/AnalystAgent.js → agent/domain/insight-analyst.js} +37 -172
- package/lib/service/{chat/HandoffProtocol.js → agent/domain/insight-gate.js} +91 -123
- package/lib/service/agent/domain/insight-producer.js +267 -0
- package/lib/service/agent/domain/scan-prompts.js +105 -0
- package/lib/service/agent/forced-summary.js +266 -0
- package/lib/service/agent/index.js +91 -0
- package/lib/service/{chat → agent}/memory/ActiveContext.js +3 -1
- package/lib/service/{chat → agent}/memory/MemoryCoordinator.js +7 -7
- package/lib/service/{chat/ProjectSemanticMemory.js → agent/memory/PersistentMemory.js} +359 -89
- package/lib/service/{chat → agent}/memory/SessionStore.js +5 -4
- package/lib/service/{chat → agent}/memory/index.js +1 -1
- package/lib/service/agent/policies.js +442 -0
- package/lib/service/agent/presets.js +303 -0
- package/lib/service/agent/strategies.js +717 -0
- package/lib/service/{chat → agent/tools}/ToolRegistry.js +3 -3
- package/lib/service/agent/tools/ai-analysis.js +75 -0
- package/lib/service/{chat → agent}/tools/ast-graph.js +229 -32
- package/lib/service/{chat → agent}/tools/composite.js +2 -1
- package/lib/service/{chat → agent}/tools/guard.js +1 -121
- package/lib/service/{chat → agent}/tools/index.js +33 -22
- package/lib/service/{chat → agent}/tools/infrastructure.js +6 -1
- package/lib/service/agent/tools/knowledge-graph.js +112 -0
- package/lib/service/agent/tools/scan-recipe.js +189 -0
- package/lib/service/agent/tools/system-interaction.js +476 -0
- package/lib/service/automation/DirectiveDetector.js +0 -1
- package/lib/service/automation/FileWatcher.js +0 -8
- package/lib/service/automation/handlers/CreateHandler.js +7 -3
- package/lib/service/automation/handlers/DraftHandler.js +7 -6
- package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
- package/lib/service/knowledge/CodeEntityGraph.js +327 -2
- package/lib/service/knowledge/KnowledgeService.js +5 -1
- package/lib/service/module/ModuleService.js +49 -73
- package/lib/service/skills/SignalCollector.js +26 -19
- package/lib/service/snippet/codecs/VSCodeCodec.js +1 -1
- package/lib/service/wiki/WikiGenerator.js +1 -1
- package/lib/shared/FieldSpec.js +1 -1
- package/lib/shared/PathGuard.js +1 -1
- package/lib/shared/StyleGuide.js +1 -1
- package/package.json +4 -1
- package/resources/native-ui/screenshot.swift +228 -0
- package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
- package/dashboard/dist/assets/index-DfHY_3ln.js +0 -128
- package/lib/core/discovery/SpmDiscoverer.js +0 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +0 -749
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +0 -277
- package/lib/http/routes/spm.js +0 -5
- package/lib/infrastructure/external/XcodeAutomation.js +0 -15
- package/lib/service/chat/ChatAgent.js +0 -1602
- package/lib/service/chat/Memory.js +0 -161
- package/lib/service/chat/ProducerAgent.js +0 -431
- package/lib/service/chat/ReasoningTrace.js +0 -523
- package/lib/service/chat/TaskPipeline.js +0 -357
- package/lib/service/chat/WorkingMemory.js +0 -357
- package/lib/service/chat/memory/PersistentMemory.js +0 -450
- package/lib/service/chat/tools/ai-analysis.js +0 -267
- package/lib/service/chat/tools/knowledge-graph.js +0 -234
- package/lib/service/chat/tools.js +0 -18
- package/lib/service/snippet/PlaceholderConverter.js +0 -5
- package/lib/service/snippet/codecs/XcodeCodec.js +0 -5
- /package/lib/service/{chat → agent}/ConversationStore.js +0 -0
- /package/lib/service/{chat → agent}/tools/_shared.js +0 -0
- /package/lib/service/{chat → agent}/tools/lifecycle.js +0 -0
- /package/lib/service/{chat → agent}/tools/project-access.js +0 -0
- /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
|
-
//
|
|
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 —
|
|
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
|