autosnippet 3.2.7 → 3.2.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.
Files changed (60) hide show
  1. package/bin/cli.js +7 -0
  2. package/dashboard/dist/assets/index-D5jiDBQG.css +1 -0
  3. package/dashboard/dist/assets/{index-DfHY_3ln.js → index-e5OKj-Ni.js} +38 -38
  4. package/dashboard/dist/index.html +2 -2
  5. package/lib/cli/AiScanService.js +3 -3
  6. package/lib/core/AstAnalyzer.js +26 -4
  7. package/lib/core/analysis/CallEdgeResolver.js +402 -0
  8. package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
  9. package/lib/core/analysis/CallSiteExtractor.js +629 -0
  10. package/lib/core/analysis/DataFlowInferrer.js +57 -0
  11. package/lib/core/analysis/ImportPathResolver.js +189 -0
  12. package/lib/core/analysis/ImportRecord.js +105 -0
  13. package/lib/core/analysis/SymbolTableBuilder.js +211 -0
  14. package/lib/core/ast/ProjectGraph.js +8 -0
  15. package/lib/core/ast/lang-dart.js +352 -5
  16. package/lib/core/ast/lang-go.js +212 -10
  17. package/lib/core/ast/lang-java.js +205 -1
  18. package/lib/core/ast/lang-kotlin.js +330 -1
  19. package/lib/core/ast/lang-python.js +31 -2
  20. package/lib/core/ast/lang-rust.js +284 -3
  21. package/lib/core/ast/lang-swift.js +180 -1
  22. package/lib/core/ast/lang-typescript.js +290 -1
  23. package/lib/external/mcp/McpServer.js +1 -0
  24. package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +21 -0
  25. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +5 -4
  26. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
  27. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +70 -4
  28. package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +95 -1
  29. package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
  30. package/lib/external/mcp/handlers/bootstrap-internal.js +17 -6
  31. package/lib/external/mcp/handlers/consolidated.js +9 -0
  32. package/lib/external/mcp/handlers/guard.js +3 -3
  33. package/lib/external/mcp/handlers/structure.js +62 -0
  34. package/lib/external/mcp/handlers/wiki-external.js +66 -3
  35. package/lib/external/mcp/tools.js +36 -1
  36. package/lib/http/routes/remote.js +15 -15
  37. package/lib/injection/ServiceContainer.js +6 -11
  38. package/lib/platform/ios/index.js +2 -2
  39. package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
  40. package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
  41. package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
  42. package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
  43. package/lib/service/chat/ChatAgent.js +1 -1
  44. package/lib/service/chat/ChatAgentPrompts.js +13 -1
  45. package/lib/service/chat/ExplorationTracker.js +52 -8
  46. package/lib/service/chat/HandoffProtocol.js +19 -1
  47. package/lib/service/chat/WorkingMemory.js +3 -1
  48. package/lib/service/chat/memory/ActiveContext.js +3 -1
  49. package/lib/service/chat/memory/SessionStore.js +4 -3
  50. package/lib/service/chat/tools/ast-graph.js +229 -32
  51. package/lib/service/chat/tools/index.js +6 -1
  52. package/lib/service/chat/tools/infrastructure.js +5 -0
  53. package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
  54. package/lib/service/knowledge/CodeEntityGraph.js +327 -2
  55. package/lib/service/knowledge/KnowledgeService.js +5 -1
  56. package/lib/service/module/ModuleService.js +9 -0
  57. package/lib/service/wiki/WikiGenerator.js +1 -1
  58. package/lib/shared/PathGuard.js +1 -1
  59. package/package.json +1 -1
  60. package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
@@ -75,6 +75,9 @@ export async function bootstrapExternal(ctx) {
75
75
  contentMaxLines: 120,
76
76
  sourceTag: 'bootstrap-external',
77
77
  summaryPrefix: 'Bootstrap-external scan',
78
+ clearOldData: true,
79
+ generateReport: true,
80
+ incremental: true,
78
81
  });
79
82
 
80
83
  // 空项目 fast-path
@@ -88,9 +91,9 @@ export async function bootstrapExternal(ctx) {
88
91
 
89
92
  const {
90
93
  allFiles, primaryLang, depGraphData, langStats,
91
- astProjectSummary, codeEntityResult, guardAudit,
94
+ astProjectSummary, codeEntityResult, callGraphResult, guardAudit,
92
95
  activeDimensions: dimensions, targetsSummary,
93
- langProfile,
96
+ langProfile, incrementalPlan,
94
97
  } = phaseResults;
95
98
 
96
99
  // ═══════════════════════════════════════════════════════════
@@ -115,6 +118,7 @@ export async function bootstrapExternal(ctx) {
115
118
  allFiles,
116
119
  astProjectSummary,
117
120
  codeEntityResult,
121
+ callGraphResult,
118
122
  depGraphData,
119
123
  guardAudit,
120
124
  langStats,
@@ -138,12 +142,15 @@ export async function bootstrapExternal(ctx) {
138
142
  projectMeta,
139
143
  astData: astProjectSummary,
140
144
  codeEntityResult,
145
+ callGraphResult,
141
146
  depGraphData,
142
147
  guardAudit,
143
148
  targets: targetsSummary,
144
149
  activeDimensions: dimensions,
145
150
  session,
146
151
  languageExtension: buildLanguageExtension(primaryLang), // §7.1
152
+ incrementalPlan,
153
+ languageStats: langStats,
147
154
  });
148
155
 
149
156
  // 附加 warnings
@@ -87,6 +87,7 @@ export async function bootstrapKnowledge(ctx, args) {
87
87
  const maxFiles = args.maxFiles || 500;
88
88
  const skipGuard = args.skipGuard || false;
89
89
  const contentMaxLines = args.contentMaxLines || 120;
90
+ const skipAsyncFill = args.skipAsyncFill || false;
90
91
 
91
92
  // ═══════════════════════════════════════════════════════════
92
93
  // Phase 1-4: 共享管线(文件收集→AST→依赖→Guard→维度解析)
@@ -149,7 +150,12 @@ export async function bootstrapKnowledge(ctx, args) {
149
150
  categories: astProjectSummary?.categories?.length || 0,
150
151
  patterns: Object.keys(astProjectSummary?.patternStats || {}),
151
152
  },
152
- codeEntityGraph: phaseReport?.phases?.entityGraph || { entities: 0, edges: 0, durationMs: 0 },
153
+ codeEntityGraph: phaseReport?.phases?.entityGraph || { entityCount: 0, edgeCount: 0, ms: 0 },
154
+ callGraph: phaseReport?.phases?.callGraph ? {
155
+ entities: phaseReport.phases.callGraph.result?.entitiesUpserted || 0,
156
+ edges: phaseReport.phases.callGraph.result?.edgesCreated || 0,
157
+ ms: phaseReport.phases.callGraph.ms || 0,
158
+ } : { entities: 0, edges: 0, ms: 0 },
153
159
  dependencyGraph: { edgesWritten: depEdgesWritten || 0 },
154
160
  enhancementPacks: {
155
161
  matched: enhancementPackInfo,
@@ -371,12 +377,17 @@ export async function bootstrapKnowledge(ctx, args) {
371
377
  };
372
378
 
373
379
  // 使用 setImmediate 避免阻塞 HTTP 响应
374
- setImmediate(() => {
375
- ctx.logger.info(`[Bootstrap] Dispatching v3 AI-First pipeline`);
376
- fillDimensionsV3(fillContext).catch((e) => {
377
- ctx.logger.error(`[Bootstrap] Async fill (v3) failed: ${e.message}`);
380
+ // skipAsyncFill: CLI 非 --wait 模式跳过异步填充,避免进程退出后 DB 断连
381
+ if (!skipAsyncFill) {
382
+ setImmediate(() => {
383
+ ctx.logger.info(`[Bootstrap] Dispatching v3 AI-First pipeline`);
384
+ fillDimensionsV3(fillContext).catch((e) => {
385
+ ctx.logger.error(`[Bootstrap] Async fill (v3) failed: ${e.message}`);
386
+ });
378
387
  });
379
- });
388
+ } else {
389
+ ctx.logger.info(`[Bootstrap] Async fill skipped (skipAsyncFill=true)`);
390
+ }
380
391
 
381
392
  // ── SkillHooks: onBootstrapStarted (fire-and-forget) ──
382
393
  try {
@@ -99,6 +99,15 @@ export async function consolidatedStructure(ctx, args) {
99
99
  }
100
100
  }
101
101
 
102
+ // ─── autosnippet_call_context (Phase 5) ─────────────────────
103
+
104
+ /**
105
+ * 调用链上下文查询:直接转发到 structure.callContext
106
+ */
107
+ export async function consolidatedCallContext(ctx, args) {
108
+ return structureHandlers.callContext(ctx, args);
109
+ }
110
+
102
111
  // ─── autosnippet_graph (整合 4 → 1) ─────────────────────────
103
112
 
104
113
  /**
@@ -416,14 +416,14 @@ export async function scanProject(ctx, args) {
416
416
 
417
417
  const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
418
418
 
419
- // 优先使用 ModuleService(多语言统一入口),回退到 SpmService
419
+ // 优先使用 ModuleService(多语言统一入口),回退到 SpmHelper
420
420
  let service;
421
421
  try {
422
422
  const { ModuleService } = await import('../../../service/module/ModuleService.js');
423
423
  service = new ModuleService(projectRoot);
424
424
  } catch {
425
- const { SpmService } = await import('../../../platform/ios/spm/SpmService.js');
426
- service = new SpmService(projectRoot);
425
+ const { SpmHelper } = await import('../../../platform/ios/spm/SpmHelper.js');
426
+ service = new SpmHelper(projectRoot);
427
427
  }
428
428
  await service.load();
429
429
  const allTargets = await service.listTargets();
@@ -527,6 +527,68 @@ async function _fallbackPathFromRecipe(ctx, fromId, toId) {
527
527
  }
528
528
  }
529
529
 
530
+ // ─── call_context — 调用链上下文 (Phase 5) ──────────────────
531
+
532
+ /**
533
+ * autosnippet_call_context handler
534
+ * 查询方法的调用者、被调用者、影响半径
535
+ */
536
+ export async function callContext(ctx, args) {
537
+ if (!args.methodName) {
538
+ throw new Error('Missing required parameter: methodName');
539
+ }
540
+
541
+ const ceg = ctx.container.get('codeEntityGraph');
542
+ if (!ceg) {
543
+ return envelope({
544
+ success: false,
545
+ message: 'CodeEntityGraph not available — 请先运行 bootstrap',
546
+ meta: { tool: 'autosnippet_call_context' },
547
+ });
548
+ }
549
+
550
+ const direction = args.direction || 'both';
551
+ const maxDepth = Math.min(Math.max(args.maxDepth ?? 2, 1), 5);
552
+ const result = {};
553
+
554
+ try {
555
+ if (direction === 'callers' || direction === 'both') {
556
+ result.callers = ceg.getCallers(args.methodName, maxDepth);
557
+ }
558
+ if (direction === 'callees' || direction === 'both') {
559
+ result.callees = ceg.getCallees(args.methodName, maxDepth);
560
+ }
561
+ if (direction === 'impact') {
562
+ result.impact = ceg.getCallImpactRadius(args.methodName);
563
+ }
564
+ } catch (err) {
565
+ if (err.message?.includes('no such table')) {
566
+ return envelope({
567
+ success: true,
568
+ data: {
569
+ methodName: args.methodName,
570
+ callers: [],
571
+ callees: [],
572
+ note: 'knowledge_edges 表不存在,请运行 bootstrap 后再查询',
573
+ },
574
+ meta: { tool: 'autosnippet_call_context' },
575
+ });
576
+ }
577
+ throw err;
578
+ }
579
+
580
+ return envelope({
581
+ success: true,
582
+ data: {
583
+ methodName: args.methodName,
584
+ direction,
585
+ maxDepth,
586
+ ...result,
587
+ },
588
+ meta: { tool: 'autosnippet_call_context' },
589
+ });
590
+ }
591
+
530
592
  // ─── graph_stats — 图谱统计 ────────────────────────────────
531
593
 
532
594
  export async function graphStats(ctx) {
@@ -60,10 +60,73 @@ export async function wikiPlan(ctx, args) {
60
60
  const session = getActiveSession(container, args.sessionId);
61
61
  const cachedData = session?.phaseCache;
62
62
 
63
- if (cachedData?.projectInfo && cachedData?.astInfo) {
64
- ({ projectInfo, astInfo, moduleInfo, knowledgeInfo } = cachedData);
63
+ if (cachedData?.astProjectSummary) {
64
+ // Bootstrap phase cache WikiGenerator-compatible format 转换
65
+ const allFiles = cachedData.allFiles || [];
66
+ const ast = cachedData.astProjectSummary;
67
+
68
+ // projectInfo: 从 bootstrap 文件列表和语言统计构建
69
+ const filesByModule = {};
70
+ for (const f of allFiles) {
71
+ const mod = f.targetName || '_default';
72
+ if (!filesByModule[mod]) filesByModule[mod] = [];
73
+ filesByModule[mod].push(f.relativePath);
74
+ }
75
+ projectInfo = {
76
+ name: path.basename(projectRoot),
77
+ root: projectRoot,
78
+ sourceFiles: allFiles.map(f => f.relativePath),
79
+ languages: cachedData.langStats || {},
80
+ primaryLanguage: cachedData.primaryLang || 'unknown',
81
+ sourceFilesByModule: filesByModule,
82
+ buildSystems: [],
83
+ };
84
+
85
+ // astInfo: 从 AstAnalyzer 结果构建
86
+ const classesByModule = {};
87
+ const protocolsByModule = {};
88
+ for (const cls of ast.classes || []) {
89
+ const mod = cls.targetName || '_default';
90
+ if (!classesByModule[mod]) classesByModule[mod] = [];
91
+ classesByModule[mod].push(cls.name);
92
+ }
93
+ for (const p of ast.protocols || []) {
94
+ const mod = p.targetName || '_default';
95
+ if (!protocolsByModule[mod]) protocolsByModule[mod] = [];
96
+ protocolsByModule[mod].push(p.name);
97
+ }
98
+ astInfo = {
99
+ classes: (ast.classes || []).map(c => c.name),
100
+ protocols: (ast.protocols || []).map(p => p.name),
101
+ overview: ast.projectMetrics || null,
102
+ classNamesByModule: classesByModule,
103
+ protocolNamesByModule: protocolsByModule,
104
+ };
105
+
106
+ // moduleInfo: 从依赖图和 targets 构建
107
+ moduleInfo = {
108
+ targets: (cachedData.targetsSummary || []).map(t => ({
109
+ name: t.name, type: t.type, fileCount: t.fileCount,
110
+ })),
111
+ depGraph: cachedData.depGraphData || null,
112
+ };
113
+
114
+ // knowledgeInfo: 始终从 DB 获取最新(bootstrap 期间可能已写入知识)
115
+ try {
116
+ const ks = tryGet(container, 'knowledgeService');
117
+ if (ks) {
118
+ const items = await ks.list({ limit: 200 });
119
+ const stats = typeof ks.getStats === 'function' ? await ks.getStats() : null;
120
+ knowledgeInfo = { recipes: items?.items || items || [], stats };
121
+ } else {
122
+ knowledgeInfo = { recipes: [], stats: null };
123
+ }
124
+ } catch {
125
+ knowledgeInfo = { recipes: [], stats: null };
126
+ }
127
+
65
128
  cacheHit = true;
66
- logger.info('[wiki-plan] Reusing bootstrap phase cache');
129
+ logger.info('[wiki-plan] Reusing bootstrap phase cache (converted to WikiGenerator format)');
67
130
  } else {
68
131
  // 无缓存(独立调用 wiki_plan 或进程已重启)→ 重新扫描
69
132
  logger.info('[wiki-plan] No bootstrap cache, running fresh scan...');
@@ -210,7 +210,42 @@ export const TOOLS = [
210
210
  },
211
211
  },
212
212
 
213
- // 6. Guard 检查(统一入口)
213
+ // 6. 调用链上下文 (Phase 5)
214
+ {
215
+ name: 'autosnippet_call_context',
216
+ tier: 'agent',
217
+ description:
218
+ '查询方法的调用链上下文。\n' +
219
+ '• callers: 谁调用了这个方法?(调用者链)\n' +
220
+ '• callees: 这个方法调用了谁?(依赖链)\n' +
221
+ '• impact: 修改此方法的影响半径分析\n' +
222
+ '• both: 同时获取调用者和被调用者',
223
+ inputSchema: {
224
+ type: 'object',
225
+ properties: {
226
+ methodName: {
227
+ type: 'string',
228
+ description: '方法名 (e.g. "UserService.getUser" 或 "getUser")',
229
+ },
230
+ direction: {
231
+ type: 'string',
232
+ enum: ['callers', 'callees', 'both', 'impact'],
233
+ default: 'both',
234
+ description: 'callers=调用者 | callees=被调用者 | both=双向 | impact=影响分析',
235
+ },
236
+ maxDepth: {
237
+ type: 'number',
238
+ default: 2,
239
+ minimum: 1,
240
+ maximum: 5,
241
+ description: '最大遍历深度',
242
+ },
243
+ },
244
+ required: ['methodName'],
245
+ },
246
+ },
247
+
248
+ // 7. Guard 检查(统一入口)
214
249
  {
215
250
  name: 'autosnippet_guard',
216
251
  tier: 'agent',
@@ -120,7 +120,7 @@ let _larkClient = null;
120
120
  let _wsConnected = false;
121
121
  let _wsStarting = false;
122
122
 
123
- async function startLarkWS() {
123
+ async function startLarkWS({ silent = false } = {}) {
124
124
  // 如果已连接且对象存在 → 直接返回
125
125
  if (_wsClient && _wsConnected) return { success: true, message: 'Already connected' };
126
126
  if (_wsStarting) return { success: true, message: 'Connection in progress' };
@@ -173,16 +173,18 @@ async function startLarkWS() {
173
173
 
174
174
  logger.info('[Remote/Lark] ✅ WebSocket long connection established');
175
175
 
176
- // 向飞书发送上线通知(延迟 1 秒确保 chatId 已恢复)
177
- setTimeout(() => {
178
- sendLarkNotification([
179
- '🟢 IDE 桥接已上线',
180
- `时间: ${new Date().toLocaleString('zh-CN')}`,
181
- `平台: macOS | Node ${process.version}`,
182
- '',
183
- '发送任意文字即可远程编程,/help 查看命令。',
184
- ].join('\n')).catch(() => {});
185
- }, 1000);
176
+ // 向飞书发送上线通知(仅首次启动,重连时静默)
177
+ if (!silent) {
178
+ setTimeout(() => {
179
+ sendLarkNotification([
180
+ '🟢 IDE 桥接已上线',
181
+ `时间: ${new Date().toLocaleString('zh-CN')}`,
182
+ `平台: macOS | Node ${process.version}`,
183
+ '',
184
+ '发送任意文字即可远程编程,/help 查看命令。',
185
+ ].join('\n')).catch(() => {});
186
+ }, 1000);
187
+ }
186
188
 
187
189
  return { success: true, message: 'Connected via WebSocket' };
188
190
  } catch (err) {
@@ -246,14 +248,12 @@ setInterval(async () => {
246
248
  }
247
249
  }
248
250
 
249
- // WSClient 不存在或已标记断开 → 自动重连
251
+ // WSClient 不存在或已标记断开 → 自动重连(静默,不打扰用户)
250
252
  if (!_wsClient && !_wsStarting) {
251
253
  logger.info('[Remote/Lark] Connection lost, auto-reconnecting...');
252
- const result = await startLarkWS();
254
+ const result = await startLarkWS({ silent: true });
253
255
  if (result.success) {
254
256
  logger.info('[Remote/Lark] ✅ Auto-reconnected successfully');
255
- // 重连成功通知
256
- sendLarkNotification('🔄 IDE 桥接重连成功').catch(() => {});
257
257
  } else {
258
258
  logger.warn(`[Remote/Lark] Auto-reconnect failed: ${result.message}`);
259
259
  }
@@ -18,7 +18,7 @@ import { IndexingPipeline } from '../infrastructure/vector/IndexingPipeline.js';
18
18
  // ─── P0: Vector Storage ──────────────────────────────
19
19
  import { JsonVectorAdapter } from '../infrastructure/vector/JsonVectorAdapter.js';
20
20
  // ─── P2: SPM ──────────────────────────────────────────
21
- import { SpmService } from '../platform/ios/spm/SpmService.js';
21
+ import { SpmHelper } from '../platform/ios/spm/SpmHelper.js';
22
22
  import { KnowledgeRepositoryImpl } from '../repository/knowledge/KnowledgeRepository.impl.js';
23
23
  // ─── P1: Token Usage Tracking ─────────────────────────
24
24
  import { TokenUsageStore } from '../repository/token/TokenUsageStore.js';
@@ -121,7 +121,7 @@ export class ServiceContainer {
121
121
  this.singletons.skillHooks = bootstrapComponents.skillHooks;
122
122
  }
123
123
 
124
- // AiFactory 模块引用(用于 SpmService AI 扫描)
124
+ // AiFactory 模块引用(用于 SpmHelper AI 扫描)
125
125
  try {
126
126
  this.singletons._aiFactory = await import('../external/ai/AiFactory.js');
127
127
  } catch {
@@ -658,18 +658,11 @@ export class ServiceContainer {
658
658
  return this.singletons.feedbackCollector;
659
659
  });
660
660
 
661
- // SpmService (with AI + tool injection)
661
+ // SpmHelper (SPM 包结构解析与依赖操作辅助)
662
662
  this.register('spmService', () => {
663
663
  if (!this.singletons.spmService) {
664
664
  const projectRoot = this.singletons._projectRoot || process.cwd();
665
- this.singletons.spmService = new SpmService(projectRoot, {
666
- aiFactory: this.singletons._aiFactory || null,
667
- chatAgent: this.singletons.chatAgent || null,
668
- qualityScorer: this.get('qualityScorer'),
669
- recipeExtractor: this.singletons._recipeExtractor || null,
670
- guardCheckEngine: this.get('guardCheckEngine'),
671
- violationsStore: this.get('violationsStore'),
672
- });
665
+ this.singletons.spmService = new SpmHelper(projectRoot);
673
666
  }
674
667
  return this.singletons.spmService;
675
668
  });
@@ -766,9 +759,11 @@ export class ServiceContainer {
766
759
  if (!this.singletons.cursorDeliveryPipeline) {
767
760
  const knowledgeService = this.get('knowledgeService');
768
761
  const projectRoot = this.singletons._projectRoot || process.cwd();
762
+ const database = this.get('database');
769
763
  this.singletons.cursorDeliveryPipeline = new CursorDeliveryPipeline({
770
764
  knowledgeService,
771
765
  projectRoot,
766
+ database,
772
767
  logger: this.logger,
773
768
  });
774
769
  }
@@ -14,7 +14,7 @@
14
14
  * PlaceholderConverter.js — Xcode <#…#> ↔ VSCode ${N:…} 占位符转换
15
15
  *
16
16
  * spm/
17
- * SpmService.js Swift Package Manager 服务(Target / 依赖 / 策略)
17
+ * SpmHelper.js SPM 包结构解析与依赖操作辅助工具
18
18
  * SpmDiscoverer.js — SPM 项目自动发现(ProjectDiscoverer 接口)
19
19
  * PackageSwiftParser.js — Package.swift 解析器
20
20
  * DependencyGraph.js — SPM Target 依赖图
@@ -36,7 +36,7 @@ export { PackageSwiftParser } from './spm/PackageSwiftParser.js';
36
36
  export { PolicyEngine } from './spm/PolicyEngine.js';
37
37
  export { SpmDiscoverer } from './spm/SpmDiscoverer.js';
38
38
  // ── Swift Package Manager ──
39
- export { SpmService } from './spm/SpmService.js';
39
+ export { SpmHelper } from './spm/SpmHelper.js';
40
40
  export { saveEventFilter } from './xcode/SaveEventFilter.js';
41
41
  // ── Xcode IDE 自动化 ──
42
42
  export {
@@ -218,10 +218,11 @@ export class PackageSwiftParser {
218
218
 
219
219
  #extractDependencies(content) {
220
220
  const deps = [];
221
- const re = /\.package\s*\(\s*url\s*:\s*"([^"]+)"[^)]*\)/g;
221
+
222
+ // 1. URL 依赖: .package(url: "...", ...)
223
+ const urlRe = /\.package\s*\(\s*url\s*:\s*"([^"]+)"[^)]*\)/g;
222
224
  let m;
223
- while ((m = re.exec(content)) !== null) {
224
- // 提取版本约束
225
+ while ((m = urlRe.exec(content)) !== null) {
225
226
  const block = m[0];
226
227
  const fromMatch = block.match(/from\s*:\s*"([^"]+)"/);
227
228
  const exactMatch = block.match(/exact\s*:\s*"([^"]+)"/);
@@ -231,6 +232,16 @@ export class PackageSwiftParser {
231
232
  type: 'package',
232
233
  });
233
234
  }
235
+
236
+ // 2. Local path 依赖: .package(path: "...")
237
+ const pathRe = /\.package\s*\(\s*path\s*:\s*"([^"]+)"\s*\)/g;
238
+ while ((m = pathRe.exec(content)) !== null) {
239
+ deps.push({
240
+ path: m[1],
241
+ type: 'local',
242
+ });
243
+ }
244
+
234
245
  return deps;
235
246
  }
236
247
 
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * @module SpmDiscoverer
3
- * @description 包装现有 SpmService,适配 ProjectDiscoverer 接口
3
+ * @description 包装现有 SpmHelper,适配 ProjectDiscoverer 接口
4
4
  *
5
5
  * 检测: 项目根或子目录存在 Package.swift
6
6
  */
7
7
 
8
8
  import { existsSync, readdirSync } from 'node:fs';
9
- import { basename, join, relative } from 'node:path';
9
+ import { basename, dirname, join, relative } from 'node:path';
10
10
  import { ProjectDiscoverer } from '../../../core/discovery/ProjectDiscoverer.js';
11
11
  import { LanguageService } from '../../../shared/LanguageService.js';
12
+ import { PackageSwiftParser } from './PackageSwiftParser.js';
12
13
 
13
14
  export class SpmDiscoverer extends ProjectDiscoverer {
14
- /** @type {import('./SpmService.js').SpmService|null} */
15
+ /** @type {import('./SpmHelper.js').SpmHelper|null} */
15
16
  #spm = null;
16
17
  #projectRoot = null;
17
18
 
@@ -52,9 +53,9 @@ export class SpmDiscoverer extends ProjectDiscoverer {
52
53
 
53
54
  async load(projectRoot) {
54
55
  this.#projectRoot = projectRoot;
55
- // 动态加载 SpmService(避免循环导入)
56
- const { SpmService } = await import('./SpmService.js');
57
- this.#spm = new SpmService(projectRoot);
56
+ // 动态加载 SpmHelper(避免循环导入)
57
+ const { SpmHelper } = await import('./SpmHelper.js');
58
+ this.#spm = new SpmHelper(projectRoot);
58
59
  await this.#spm.load();
59
60
  }
60
61
 
@@ -97,21 +98,126 @@ export class SpmDiscoverer extends ProjectDiscoverer {
97
98
  }
98
99
 
99
100
  async getDependencyGraph() {
100
- if (!this.#spm) {
101
+ if (!this.#projectRoot) {
102
+ return { nodes: [], edges: [] };
103
+ }
104
+
105
+ // 直接用 PackageSwiftParser 构建依赖图,不依赖 SpmHelper
106
+ const parser = new PackageSwiftParser(this.#projectRoot);
107
+ const allPkgPaths = parser.findAllPackageSwifts(this.#projectRoot);
108
+
109
+ if (allPkgPaths.length === 0) {
101
110
  return { nodes: [], edges: [] };
102
111
  }
103
- const raw = await this.#spm.getDependencyGraph();
104
- return {
105
- nodes: (raw.nodes || []).map((n) => (typeof n === 'string' ? n : n.id || n.name)),
106
- edges: (raw.edges || []).map((e) => ({
107
- from: e.from,
108
- to: e.to,
109
- type: 'depends_on',
110
- })),
111
- };
112
+
113
+ const nodes = [];
114
+ const edges = [];
115
+ const pkgNameSet = new Set();
116
+ // targetName → 所属 packageName 映射(用于跨包 target 依赖解析)
117
+ const targetToPkg = new Map();
118
+
119
+ // ── 第一遍:收集所有 package + target 节点 ──
120
+ const allParsed = [];
121
+ // 记录 umbrella 包名(无 targets 且无 products 的纯组织性入口包),不作为图节点
122
+ const umbrellaNames = new Set();
123
+ for (const pkgPath of allPkgPaths) {
124
+ try {
125
+ const parsed = parser.parse(pkgPath);
126
+ if (pkgNameSet.has(parsed.name)) continue;
127
+ pkgNameSet.add(parsed.name);
128
+ allParsed.push({ ...parsed, _dir: dirname(pkgPath) });
129
+
130
+ // 跳过 umbrella 包(无 targets + 无 products)——它只是组织子包的入口
131
+ const hasTargets = parsed.targets && parsed.targets.length > 0;
132
+ const hasProducts = parsed.products && parsed.products.length > 0;
133
+ if (!hasTargets && !hasProducts) {
134
+ umbrellaNames.add(parsed.name);
135
+ continue;
136
+ }
137
+
138
+ // package 节点
139
+ nodes.push({
140
+ id: parsed.name,
141
+ label: parsed.name,
142
+ type: 'package',
143
+ fullPath: dirname(pkgPath),
144
+ targetCount: parsed.targets.length,
145
+ });
146
+
147
+ // target 节点
148
+ for (const t of parsed.targets) {
149
+ nodes.push({
150
+ id: t.name,
151
+ label: t.name,
152
+ type: 'target',
153
+ parent: parsed.name,
154
+ targetType: t.type,
155
+ });
156
+ targetToPkg.set(t.name, parsed.name);
157
+ }
158
+
159
+ // product name → package(product 名可能和 target 名不同)
160
+ for (const prod of parsed.products || []) {
161
+ if (!targetToPkg.has(prod.name)) {
162
+ targetToPkg.set(prod.name, parsed.name);
163
+ }
164
+ }
165
+ } catch {
166
+ // 解析失败,跳过
167
+ }
168
+ }
169
+
170
+ // ── 第二遍:构建 edges ──
171
+ for (const parsed of allParsed) {
172
+ // 跳过 umbrella 包的边
173
+ if (umbrellaNames.has(parsed.name)) continue;
174
+
175
+ // 包级 local path 依赖
176
+ for (const dep of parsed.dependencies || []) {
177
+ if (dep.type === 'local' && dep.path) {
178
+ const depPkgSwift = join(parsed._dir, dep.path, 'Package.swift');
179
+ if (existsSync(depPkgSwift)) {
180
+ try {
181
+ const depParsed = parser.parse(depPkgSwift);
182
+ // 跳过指向 umbrella 包的边
183
+ if (!umbrellaNames.has(depParsed.name)) {
184
+ edges.push({ from: parsed.name, to: depParsed.name, type: 'depends_on' });
185
+ }
186
+ } catch {
187
+ const targetName = basename(dep.path);
188
+ if (!umbrellaNames.has(targetName)) {
189
+ edges.push({ from: parsed.name, to: targetName, type: 'depends_on' });
190
+ }
191
+ }
192
+ }
193
+ } else if (dep.url) {
194
+ const remoteName = basename(dep.url).replace(/\.git$/, '');
195
+ if (!pkgNameSet.has(remoteName)) {
196
+ pkgNameSet.add(remoteName);
197
+ nodes.push({ id: remoteName, label: remoteName, type: 'remote', indirect: true });
198
+ }
199
+ edges.push({ from: parsed.name, to: remoteName, type: 'depends_on' });
200
+ }
201
+ }
202
+
203
+ // target 级依赖
204
+ for (const t of parsed.targets || []) {
205
+ // target → parent package (contains)
206
+ edges.push({ from: parsed.name, to: t.name, type: 'contains' });
207
+
208
+ for (const depName of t.dependencies || []) {
209
+ // target → target 依赖(跳过指向 umbrella 包的)
210
+ if (!umbrellaNames.has(depName)) {
211
+ edges.push({ from: t.name, to: depName, type: 'depends_on' });
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ return { nodes, edges };
112
218
  }
113
219
 
114
- /** 获取底层 SpmService(向后兼容) */
220
+ /** 获取底层 SpmHelper(向后兼容) */
115
221
  getSpmService() {
116
222
  return this.#spm;
117
223
  }