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
@@ -1,34 +1,23 @@
1
1
  /**
2
- * SpmService — SPM 依赖管理门面服务
2
+ * SpmHelper — SPM 包结构解析与依赖操作辅助工具
3
3
  * 整合 PackageSwiftParser + DependencyGraph + PolicyEngine
4
+ * 由 SpmDiscoverer 持有,不作为独立 Service 使用
4
5
  */
5
6
 
6
7
  import { readFileSync, writeFileSync } from 'node:fs';
7
- import {
8
- basename as _pathBasename,
9
- dirname,
10
- resolve as pathResolve,
11
- relative,
12
- sep,
13
- } from 'node:path';
8
+ import { dirname, resolve as pathResolve, relative, sep } from 'node:path';
14
9
  import { GraphCache } from '../../../infrastructure/cache/GraphCache.js';
15
10
  import Logger from '../../../infrastructure/logging/Logger.js';
16
11
  import { DependencyGraph } from './DependencyGraph.js';
17
12
  import { PackageSwiftParser } from './PackageSwiftParser.js';
18
13
  import { PolicyEngine } from './PolicyEngine.js';
19
14
 
20
- export class SpmService {
15
+ export class SpmHelper {
21
16
  #parser;
22
17
  #graph;
23
18
  #policy;
24
19
  #logger;
25
20
  #projectRoot;
26
- #aiFactory;
27
- #chatAgent;
28
- #qualityScorer;
29
- #recipeExtractor;
30
- #guardCheckEngine;
31
- #violationsStore;
32
21
 
33
22
  /**
34
23
  * target → { packageName, packagePath } 映射(V1 spmmap 等价)
@@ -50,12 +39,6 @@ export class SpmService {
50
39
 
51
40
  constructor(projectRoot, options = {}) {
52
41
  this.#projectRoot = projectRoot;
53
- this.#aiFactory = options.aiFactory || null;
54
- this.#chatAgent = options.chatAgent || null;
55
- this.#qualityScorer = options.qualityScorer || null;
56
- this.#recipeExtractor = options.recipeExtractor || null;
57
- this.#guardCheckEngine = options.guardCheckEngine || null;
58
- this.#violationsStore = options.violationsStore || null;
59
42
  this.#parser = options.parser || new PackageSwiftParser(projectRoot);
60
43
  this.#graph = options.graph || new DependencyGraph();
61
44
  this.#policy = options.policy || new PolicyEngine();
@@ -78,11 +61,28 @@ export class SpmService {
78
61
 
79
62
  // ── 收集所有 Package.swift 路径 + 联合 hash ──
80
63
  const packagePath = this.#parser.findPackageSwift(this.#projectRoot);
81
- const allPaths = packagePath
82
- ? [packagePath]
83
- : this.#parser.findAllPackageSwifts(this.#projectRoot);
64
+
65
+ // 判断是否需要多包模式:
66
+ // 1. 根目录没有 Package.swift → findAllPackageSwifts
67
+ // 2. 根目录有 Package.swift 但 targets 为空且有 local path dependencies → 聚合根 + 子包
68
+ let allPaths;
69
+ if (packagePath) {
70
+ const rootParsed = this.#parser.parse(packagePath);
71
+ const hasNoTargets = !rootParsed?.targets || rootParsed.targets.length === 0;
72
+ const hasLocalDeps = (rootParsed?.dependencies || []).some(d => d.type === 'local' || d.path);
73
+ if (hasNoTargets && hasLocalDeps) {
74
+ // 聚合根模式:根 Package.swift 仅声明 local path 依赖,target 在子包里
75
+ allPaths = this.#parser.findAllPackageSwifts(this.#projectRoot);
76
+ this.#logger.info(`[SpmHelper] 聚合根检测: 根无 target 但有 ${rootParsed.dependencies.length} 个 local dep,切换多包模式`);
77
+ } else {
78
+ allPaths = [packagePath];
79
+ }
80
+ } else {
81
+ allPaths = this.#parser.findAllPackageSwifts(this.#projectRoot);
82
+ }
83
+
84
84
  if (allPaths.length === 0) {
85
- this.#logger.warn('[SpmService] Package.swift 未找到');
85
+ this.#logger.warn('[SpmHelper] Package.swift 未找到');
86
86
  return null;
87
87
  }
88
88
 
@@ -93,7 +93,7 @@ export class SpmService {
93
93
  if (cached && cached.contentHash === combinedHash) {
94
94
  this.#restoreFromCache(cached.data);
95
95
  this.#logger.info(
96
- `[SpmService] ⚡ 缓存命中 (${this.#graph.getNodes().length} targets, hash=${combinedHash.substring(0, 8)})`
96
+ `[SpmHelper] ⚡ 缓存命中 (${this.#graph.getNodes().length} targets, hash=${combinedHash.substring(0, 8)})`
97
97
  );
98
98
  return cached.data.parsedResult;
99
99
  }
@@ -102,15 +102,15 @@ export class SpmService {
102
102
  const startTime = Date.now();
103
103
  let parsedResult;
104
104
 
105
- if (packagePath) {
106
- // 单包模式
105
+ if (packagePath && allPaths.length === 1) {
106
+ // 单包模式(根有 target)
107
107
  const parsed = this.#parser.parse(packagePath);
108
108
  this.#graph.buildFromParsed(parsed);
109
109
  for (const t of parsed.targets || []) {
110
110
  this.#targetPackageMap.set(t.name, { packageName: parsed.name, packagePath });
111
111
  }
112
112
  this.#buildPackageDepGraph([{ path: packagePath, parsed }]);
113
- this.#logger.info(`[SpmService] 加载完成: ${parsed.name} (${parsed.targets.length} targets)`);
113
+ this.#logger.info(`[SpmHelper] 加载完成: ${parsed.name} (${parsed.targets.length} targets)`);
114
114
  parsedResult = parsed;
115
115
  } else {
116
116
  // 多包模式
@@ -120,7 +120,7 @@ export class SpmService {
120
120
  // ── 写入缓存 ──
121
121
  if (parsedResult) {
122
122
  this.#saveToCache(combinedHash, parsedResult);
123
- this.#logger.info(`[SpmService] 缓存已写入 (${Date.now() - startTime}ms 解析)`);
123
+ this.#logger.info(`[SpmHelper] 缓存已写入 (${Date.now() - startTime}ms 解析)`);
124
124
  }
125
125
 
126
126
  return parsedResult;
@@ -132,7 +132,7 @@ export class SpmService {
132
132
  * @returns {object|null}
133
133
  */
134
134
  #loadMultiPackage(allPaths) {
135
- this.#logger.info(`[SpmService] 发现 ${allPaths.length} 个 Package.swift,逐一解析...`);
135
+ this.#logger.info(`[SpmHelper] 发现 ${allPaths.length} 个 Package.swift,逐一解析...`);
136
136
  const mergedTargets = [];
137
137
  let lastName = 'multi-package';
138
138
  const allParsed = [];
@@ -160,14 +160,14 @@ export class SpmService {
160
160
  lastName = parsed.name;
161
161
  }
162
162
  } catch (e) {
163
- this.#logger.warn(`[SpmService] 解析失败: ${pkgPath} - ${e.message}`);
163
+ this.#logger.warn(`[SpmHelper] 解析失败: ${pkgPath} - ${e.message}`);
164
164
  }
165
165
  }
166
166
 
167
167
  this.#buildPackageDepGraph(allParsed);
168
168
 
169
169
  this.#logger.info(
170
- `[SpmService] 多包加载完成: ${mergedTargets.length} targets from ${allPaths.length} packages`
170
+ `[SpmHelper] 多包加载完成: ${mergedTargets.length} targets from ${allPaths.length} packages`
171
171
  );
172
172
  return {
173
173
  name: lastName,
@@ -260,7 +260,7 @@ export class SpmService {
260
260
  }
261
261
  }
262
262
 
263
- this.#logger.debug(`[SpmService] 包级依赖图: ${this.#packageDepGraph.size} packages`);
263
+ this.#logger.debug(`[SpmHelper] 包级依赖图: ${this.#packageDepGraph.size} packages`);
264
264
  }
265
265
 
266
266
  /**
@@ -321,35 +321,6 @@ export class SpmService {
321
321
  return 'suggest'; // 默认仅提示模式
322
322
  }
323
323
 
324
- /**
325
- * 获取依赖图
326
- */
327
- getGraph() {
328
- return this.#graph;
329
- }
330
-
331
- /**
332
- * 检查 from 是否可达 to
333
- */
334
- isReachable(from, to) {
335
- return this.#graph.isReachable(from, to);
336
- }
337
-
338
- /**
339
- * 运行策略检查
340
- * @param {{ layerOrder?: string[] }} config
341
- */
342
- checkPolicies(config = {}) {
343
- return this.#policy.check(this.#graph, config);
344
- }
345
-
346
- /**
347
- * 检查能否添加依赖
348
- */
349
- canAddDependency(from, to) {
350
- return this.#policy.canAddDependency(this.#graph, from, to);
351
- }
352
-
353
324
  /**
354
325
  * 确保依赖存在: 如果不存在则评估是否可以添加
355
326
  * 支持跨包循环检测:如果 from 和 to 在不同包内,额外检查包级依赖图
@@ -487,11 +458,11 @@ export class SpmService {
487
458
  this.#parser.clearCache();
488
459
 
489
460
  this.#logger.info(
490
- `[SpmService] 已自动补齐依赖: ${from} -> ${to}${isCrossPackage ? ' (跨包)' : ''} (${packagePath})`
461
+ `[SpmHelper] 已自动补齐依赖: ${from} -> ${to}${isCrossPackage ? ' (跨包)' : ''} (${packagePath})`
491
462
  );
492
463
  return { ok: true, changed: true, file: packagePath, crossPackage: isCrossPackage };
493
464
  } catch (err) {
494
- this.#logger.error(`[SpmService] addDependency failed: ${err.message}`);
465
+ this.#logger.error(`[SpmHelper] addDependency failed: ${err.message}`);
495
466
  return { ok: false, changed: false, error: err.message };
496
467
  }
497
468
  }
@@ -536,11 +507,11 @@ export class SpmService {
536
507
  pkgDepsRe,
537
508
  `${pkgDepsMatch[1]}${existing}${separator}${newDep}\n ${pkgDepsMatch[3]}`
538
509
  );
539
- this.#logger.info(`[SpmService] 已添加包级依赖: .package(path: "${relPath}")`);
510
+ this.#logger.info(`[SpmHelper] 已添加包级依赖: .package(path: "${relPath}")`);
540
511
  return { changed: true, content: patched };
541
512
  }
542
513
 
543
- this.#logger.warn(`[SpmService] 未能找到包级 dependencies 数组,跳过 .package(path:) 插入`);
514
+ this.#logger.warn(`[SpmHelper] 未能找到包级 dependencies 数组,跳过 .package(path:) 插入`);
544
515
  return { changed: false, content };
545
516
  }
546
517
 
@@ -548,6 +519,7 @@ export class SpmService {
548
519
  * 推断文件所属 target(源自 V1 ModuleResolverV2.determineCurrentModule)
549
520
  *
550
521
  * 从文件到 Package.swift 所在目录的相对路径中,反向匹配已知 target 名。
522
+ * 如果文件不在任何 SPM target 的源码目录中(如 Xcode 主 App target),返回 null。
551
523
  * @param {string} filePath - 源文件绝对路径
552
524
  * @returns {string|null} target 名称,未匹配返回 null
553
525
  */
@@ -575,44 +547,14 @@ export class SpmService {
575
547
  }
576
548
  }
577
549
 
578
- // 兜底:第一个 target
579
- return nodes[0] || null;
550
+ // 文件不属于任何 SPM target(如 Xcode 主 App target 下的文件)
551
+ // 不做错误的 fallback,返回 null 让调用方跳过依赖检查
552
+ return null;
580
553
  } catch {
581
554
  return null;
582
555
  }
583
556
  }
584
557
 
585
- /**
586
- * 获取 target 的分层信息
587
- */
588
- getLevels() {
589
- return Object.fromEntries(this.#graph.computeLevels());
590
- }
591
-
592
- /**
593
- * 拓扑排序
594
- */
595
- getTopologicalOrder() {
596
- return this.#graph.topologicalSort();
597
- }
598
-
599
- /**
600
- * 获取摘要报告
601
- */
602
- getSummary() {
603
- const nodes = this.#graph.getNodes();
604
- const levels = this.#graph.computeLevels();
605
- const cycles = this.#graph.detectCycles();
606
-
607
- return {
608
- nodeCount: nodes.length,
609
- edgeCount: this.#graph.edgeCount(),
610
- levels: Object.fromEntries(levels),
611
- cycleCount: cycles.length,
612
- cycles: cycles.map((c) => c.join(' → ')),
613
- };
614
- }
615
-
616
558
  // ─────────────── Dashboard API 适配方法 ───────────────
617
559
 
618
560
  /**
@@ -688,26 +630,6 @@ export class SpmService {
688
630
  return virtualTargets;
689
631
  }
690
632
 
691
- /**
692
- * 获取依赖关系图(路由: GET /spm/dep-graph)
693
- * @param {{ level?: 'package'|'target' }} options
694
- */
695
- async getDependencyGraph(options = {}) {
696
- await this.#ensureLoaded();
697
- const json = this.#graph.toJSON();
698
-
699
- return {
700
- nodes: json.nodes.map((name) => ({
701
- id: name,
702
- label: name,
703
- type: options.level === 'package' ? 'package' : 'target',
704
- })),
705
- edges: json.edges.map((e) => ({ from: e.from, to: e.to, source: 'spm' })),
706
- projectRoot: this.#projectRoot,
707
- generatedAt: new Date().toISOString(),
708
- };
709
- }
710
-
711
633
  /**
712
634
  * 获取 Target 源文件列表(路由: POST /spm/target-files)
713
635
  * 支持多 Package 项目:先在 target 所属 package 目录查找
@@ -876,538 +798,6 @@ export class SpmService {
876
798
  return files;
877
799
  }
878
800
 
879
- /**
880
- * 共用增强管线:补充 extract_recipes handler 未覆盖的增量增强。
881
- * extract_recipes handler 已负责:normalizeSemanticFields + RecipeExtractor + QualityScorer + V3 结构化。
882
- * 此方法仅做以下增量处理:
883
- * - 确保 normalizeSemanticFields 已执行(幂等,防止非 ChatAgent 路径跳过)
884
- * - 若 handler 未注入质量评分,由 SpmService 补充
885
- */
886
- _enrichRecipes(recipes) {
887
- for (const recipe of recipes) {
888
- // QualityScorer 评分(程序化,仅在未评分时补充)
889
- if (!recipe.quality && this.#qualityScorer) {
890
- try {
891
- const scoreResult = this.#qualityScorer.score(recipe);
892
- recipe.quality = {
893
- completeness: 0,
894
- adaptation: 0,
895
- documentation: 0,
896
- overall: scoreResult.score ?? 0,
897
- grade: scoreResult.grade || '',
898
- };
899
- } catch (e) {
900
- this.#logger.debug(`[SpmService] QualityScorer failed: ${e.message}`);
901
- }
902
- }
903
- }
904
- }
905
-
906
- /**
907
- * AI 扫描 Target 发现候选项(路由: POST /spm/scan)
908
- * 完整管线: 读文件 → AI 提取 → Header 解析 → 工具增强(语义标签/质量评分)
909
- */
910
- async scanTarget(target, options = {}) {
911
- const targetName = typeof target === 'string' ? target : target?.name;
912
- /** @type {((event: {type: string, [key:string]: any}) => void) | undefined} */
913
- const onProgress = options.onProgress;
914
-
915
- // 1. 获取源文件列表
916
- onProgress?.({ type: 'scan:started', targetName });
917
- const fileList = await this.getTargetFiles(target);
918
- if (!fileList || fileList.length === 0) {
919
- return {
920
- recipes: [],
921
- scannedFiles: [],
922
- message: `No source files found for target: ${targetName}`,
923
- };
924
- }
925
-
926
- const scannedFilesMeta = fileList.map((f) => {
927
- const filePath = typeof f === 'string' ? f : f.path;
928
- return { name: _pathBasename(filePath), path: f.relativePath || _pathBasename(filePath) };
929
- });
930
- onProgress?.({ type: 'scan:files-loaded', files: scannedFilesMeta, count: fileList.length });
931
-
932
- // 2. 读取文件内容
933
- onProgress?.({ type: 'scan:reading', count: fileList.length });
934
- const { readFileSync } = await import('node:fs');
935
- const { basename } = await import('node:path');
936
- const files = fileList
937
- .map((f) => {
938
- const filePath = typeof f === 'string' ? f : f.path;
939
- try {
940
- return {
941
- name: basename(filePath),
942
- path: filePath,
943
- relativePath: f.relativePath || basename(filePath),
944
- content: readFileSync(filePath, 'utf8'),
945
- };
946
- } catch (err) {
947
- this.#logger.warn(`[SpmService] 读取文件失败: ${filePath} — ${err.message}`);
948
- return null;
949
- }
950
- })
951
- .filter(Boolean);
952
-
953
- if (files.length === 0) {
954
- return { recipes: [], scannedFiles: [], message: 'All source files unreadable' };
955
- }
956
-
957
- const scannedFiles = files.map((f) => ({ name: f.name, path: f.relativePath }));
958
- this.#logger.info(`[SpmService] scanTarget: ${targetName}, ${files.length} files`);
959
-
960
- // 3. AI 提取 Recipes(通过 ChatAgent 统一入口)
961
- if (!this.#chatAgent && !this.#aiFactory) {
962
- return {
963
- recipes: [],
964
- scannedFiles,
965
- message: 'AI provider not configured. Please set ASD_AI_PROVIDER.',
966
- };
967
- }
968
-
969
- onProgress?.({ type: 'scan:ai-extracting', fileCount: files.length, targetName });
970
- const AI_EXTRACT_TIMEOUT = 120_000; // 2 分钟 AI 提取超时
971
- let recipes;
972
- try {
973
- if (this.#chatAgent) {
974
- const extractPromise = this.#chatAgent.executeTool('extract_recipes', {
975
- targetName,
976
- files,
977
- });
978
- const timeoutPromise = new Promise((_, reject) =>
979
- setTimeout(
980
- () => reject(new Error(`AI extraction timeout (${AI_EXTRACT_TIMEOUT / 1000}s)`)),
981
- AI_EXTRACT_TIMEOUT
982
- )
983
- );
984
- const result = await Promise.race([extractPromise, timeoutPromise]);
985
- if (result?.error) {
986
- return { recipes: [], scannedFiles, message: result.error };
987
- }
988
- recipes = result?.recipes || [];
989
- } else {
990
- // 降级: 直接使用 aiFactory(兼容未注入 chatAgent 的场景)
991
- const {
992
- getProviderWithFallback,
993
- isGeoOrProviderError,
994
- getAvailableFallbacks,
995
- createProvider,
996
- } = this.#aiFactory;
997
- let ai = await getProviderWithFallback();
998
- if (!ai) {
999
- return { recipes: [], scannedFiles, message: 'AI provider not available' };
1000
- }
1001
-
1002
- try {
1003
- recipes = await ai.extractRecipes(targetName, files, {});
1004
- } catch (primaryErr) {
1005
- if (isGeoOrProviderError(primaryErr)) {
1006
- const currentProvider = (process.env.ASD_AI_PROVIDER || 'google').toLowerCase();
1007
- const fallbacks = getAvailableFallbacks(currentProvider);
1008
- let fallbackOk = false;
1009
- for (const fbName of fallbacks) {
1010
- try {
1011
- ai = createProvider({ provider: fbName });
1012
- recipes = await ai.extractRecipes(targetName, files, {});
1013
- fallbackOk = true;
1014
- break;
1015
- } catch (fbErr) {
1016
- this.#logger.warn(`[SpmService] fallback "${fbName}" failed: ${fbErr.message}`);
1017
- }
1018
- }
1019
- if (!fallbackOk) {
1020
- throw primaryErr;
1021
- }
1022
- } else {
1023
- throw primaryErr;
1024
- }
1025
- }
1026
- }
1027
- } catch (err) {
1028
- this.#logger.warn(`[SpmService] scanTarget AI extraction failed: ${err.message}`);
1029
- return { recipes: [], scannedFiles, message: `AI extraction failed: ${err.message}` };
1030
- }
1031
-
1032
- if (!Array.isArray(recipes)) {
1033
- recipes = [];
1034
- }
1035
-
1036
- // 3.5 Header 路径解析 + moduleName 注入
1037
- try {
1038
- const PathFinder = await import('../../../infrastructure/paths/PathFinder.js');
1039
- const HeaderResolver = await import('../../../infrastructure/paths/HeaderResolver.js');
1040
- const targetRootDir = await PathFinder.findTargetRootDir(files[0].path);
1041
- for (const recipe of recipes) {
1042
- const headerList = recipe.headers || [];
1043
- recipe.headerPaths = await Promise.all(
1044
- headerList.map((h) => HeaderResolver.resolveHeaderRelativePath(h, targetRootDir))
1045
- );
1046
- recipe.moduleName = targetName;
1047
- }
1048
- } catch (err) {
1049
- this.#logger.warn(`[SpmService] Header resolution failed: ${err.message}`);
1050
- }
1051
-
1052
- // 4. 工具增强:语义标准化 + 标签 + 评分
1053
- onProgress?.({ type: 'scan:enriching', recipeCount: recipes.length });
1054
- this._enrichRecipes(recipes);
1055
-
1056
- const result = { recipes, scannedFiles };
1057
- if (recipes.length === 0) {
1058
- const aiInfo = this.#aiFactory
1059
- ? (await import('../../../external/ai/AiFactory.js')).getAiConfigInfo()
1060
- : null;
1061
- if (aiInfo && !aiInfo.hasKey) {
1062
- result.noAi = true;
1063
- result.message = 'AI 未配置,已跳过智能提取。请在 .env 中设置 API Key 后重试。';
1064
- } else {
1065
- result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`;
1066
- }
1067
- }
1068
- onProgress?.({
1069
- type: 'scan:completed',
1070
- recipeCount: recipes.length,
1071
- fileCount: scannedFiles.length,
1072
- });
1073
- return result;
1074
- }
1075
-
1076
- /**
1077
- * 全项目扫描 — 遍历所有 Target,AI 提取候选 + Guard 审计
1078
- * 返回: { targets: [...], recipes: [], guardAudit: { files, summary }, scannedFiles: [] }
1079
- */
1080
- async scanProject(options = {}) {
1081
- this.#logger.info('[SpmService] scanProject: starting full-project scan');
1082
-
1083
- const { readFileSync, existsSync, readdirSync, statSync } = await import('node:fs');
1084
- const { basename: bn, join, extname, relative } = await import('node:path');
1085
-
1086
- // 1. 列出所有 target
1087
- const allTargets = await this.listTargets();
1088
-
1089
- // 2. 收集所有源文件(去重)
1090
- const seenPaths = new Set();
1091
- const allFiles = []; // { name, path, relativePath, content, targetName }
1092
- const MAX_FILES = options.maxFiles || 200;
1093
-
1094
- if (allTargets && allTargets.length > 0) {
1095
- // SPM 项目:从 Target 中收集
1096
- for (const t of allTargets) {
1097
- try {
1098
- const fileList = await this.getTargetFiles(t);
1099
- for (const f of fileList) {
1100
- const fp = typeof f === 'string' ? f : f.path;
1101
- if (seenPaths.has(fp)) {
1102
- continue;
1103
- }
1104
- seenPaths.add(fp);
1105
- try {
1106
- const content = readFileSync(fp, 'utf8');
1107
- allFiles.push({
1108
- name: bn(fp),
1109
- path: fp,
1110
- relativePath: f.relativePath || bn(fp),
1111
- content,
1112
- targetName: t.name,
1113
- });
1114
- } catch {
1115
- /* unreadable */
1116
- }
1117
- if (allFiles.length >= MAX_FILES) {
1118
- break;
1119
- }
1120
- }
1121
- } catch (e) {
1122
- this.#logger.warn(`[SpmService] scanProject: skipping target ${t.name}: ${e.message}`);
1123
- }
1124
- if (allFiles.length >= MAX_FILES) {
1125
- break;
1126
- }
1127
- }
1128
- } else {
1129
- // 非 SPM 项目:直接扫描常见源码目录(fallback)
1130
- this.#logger.info('[SpmService] scanProject: No SPM targets, falling back to directory scan');
1131
- const CODE_EXTS = new Set([
1132
- '.swift',
1133
- '.m',
1134
- '.mm',
1135
- '.h',
1136
- '.js',
1137
- '.ts',
1138
- '.tsx',
1139
- '.jsx',
1140
- '.py',
1141
- '.java',
1142
- '.kt',
1143
- '.go',
1144
- '.rs',
1145
- '.rb',
1146
- '.vue',
1147
- '.mjs',
1148
- '.cjs',
1149
- ]);
1150
- const SKIP_DIRS = new Set([
1151
- 'node_modules',
1152
- '.git',
1153
- 'dist',
1154
- 'build',
1155
- '.next',
1156
- 'Pods',
1157
- 'Carthage',
1158
- '.build',
1159
- 'DerivedData',
1160
- 'vendor',
1161
- '__pycache__',
1162
- '.venv',
1163
- 'target',
1164
- ]);
1165
- const srcDirs = [
1166
- 'Sources',
1167
- 'src',
1168
- 'lib',
1169
- 'app',
1170
- 'pages',
1171
- 'components',
1172
- 'modules',
1173
- 'packages',
1174
- ];
1175
-
1176
- const walkDir = (dir, targetName) => {
1177
- if (allFiles.length >= MAX_FILES) {
1178
- return;
1179
- }
1180
- let entries;
1181
- try {
1182
- entries = readdirSync(dir, { withFileTypes: true });
1183
- } catch {
1184
- return;
1185
- }
1186
- for (const ent of entries) {
1187
- if (allFiles.length >= MAX_FILES) {
1188
- break;
1189
- }
1190
- if (ent.name.startsWith('.')) {
1191
- continue;
1192
- }
1193
- const fp = join(dir, ent.name);
1194
- if (ent.isDirectory()) {
1195
- if (SKIP_DIRS.has(ent.name)) {
1196
- continue;
1197
- }
1198
- walkDir(fp, targetName);
1199
- } else if (ent.isFile() && CODE_EXTS.has(extname(ent.name).toLowerCase())) {
1200
- if (seenPaths.has(fp)) {
1201
- continue;
1202
- }
1203
- seenPaths.add(fp);
1204
- try {
1205
- const st = statSync(fp);
1206
- if (st.size > 512 * 1024) {
1207
- continue; // 跳过 > 512KB
1208
- }
1209
- const content = readFileSync(fp, 'utf8');
1210
- if (content.split('\n').length < 5) {
1211
- continue; // 跳过微小文件
1212
- }
1213
- allFiles.push({
1214
- name: ent.name,
1215
- path: fp,
1216
- relativePath: relative(this.#projectRoot, fp),
1217
- content,
1218
- targetName,
1219
- });
1220
- } catch {
1221
- /* unreadable */
1222
- }
1223
- }
1224
- }
1225
- };
1226
-
1227
- for (const dir of srcDirs) {
1228
- const dirPath = join(this.#projectRoot, dir);
1229
- if (existsSync(dirPath)) {
1230
- walkDir(dirPath, dir);
1231
- }
1232
- }
1233
-
1234
- // 如果常见目录都为空,扫描根目录(浅层)
1235
- if (allFiles.length === 0) {
1236
- walkDir(this.#projectRoot, 'root');
1237
- }
1238
- }
1239
-
1240
- this.#logger.info(
1241
- `[SpmService] scanProject: ${allFiles.length} unique files from ${allTargets?.length || 0} targets`
1242
- );
1243
-
1244
- if (allFiles.length === 0) {
1245
- return {
1246
- targets: (allTargets || []).map((t) => t.name),
1247
- recipes: [],
1248
- guardAudit: null,
1249
- scannedFiles: [],
1250
- message: 'No readable source files',
1251
- };
1252
- }
1253
-
1254
- const scannedFiles = allFiles.map((f) => ({
1255
- name: f.name,
1256
- path: f.relativePath,
1257
- targetName: f.targetName,
1258
- }));
1259
-
1260
- // 3. AI 提取 Recipes(全局批量,分批避免 token 超限)— 通过 ChatAgent 统一入口
1261
- const allRecipes = [];
1262
- const PER_BATCH_TIMEOUT = options.batchTimeout || 90000; // 每批最多 90 秒
1263
- const startTime = Date.now();
1264
- const TOTAL_TIMEOUT = options.totalTimeout || 540000; // 总超时 9 分钟(留 1 分钟给 Guard)
1265
- let timedOut = false;
1266
-
1267
- if (this.#chatAgent || this.#aiFactory) {
1268
- const BATCH_SIZE = options.batchSize || 20;
1269
-
1270
- if (this.#chatAgent) {
1271
- // 通过 ChatAgent extract_recipes 工具(内置 fallback)
1272
- for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
1273
- if (Date.now() - startTime > TOTAL_TIMEOUT) {
1274
- this.#logger.warn(
1275
- `[SpmService] scanProject: total timeout reached after ${Math.floor((Date.now() - startTime) / 1000)}s, returning partial results`
1276
- );
1277
- timedOut = true;
1278
- break;
1279
- }
1280
- const batch = allFiles.slice(i, i + BATCH_SIZE);
1281
- const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`;
1282
- try {
1283
- const result = await Promise.race([
1284
- this.#chatAgent.executeTool('extract_recipes', {
1285
- targetName: batchLabel,
1286
- files: batch,
1287
- }),
1288
- new Promise((_, reject) =>
1289
- setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)
1290
- ),
1291
- ]);
1292
- if (Array.isArray(result?.recipes)) {
1293
- allRecipes.push(...result.recipes);
1294
- }
1295
- } catch (err) {
1296
- this.#logger.warn(
1297
- `[SpmService] scanProject ChatAgent batch ${batchLabel} failed: ${err.message}`
1298
- );
1299
- }
1300
- }
1301
- } else {
1302
- // 降级: 直接使用 aiFactory(兼容未注入 chatAgent 的场景)
1303
- const {
1304
- getProviderWithFallback,
1305
- isGeoOrProviderError,
1306
- getAvailableFallbacks,
1307
- createProvider,
1308
- } = this.#aiFactory;
1309
- let ai = await getProviderWithFallback();
1310
-
1311
- if (ai) {
1312
- for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
1313
- if (Date.now() - startTime > TOTAL_TIMEOUT) {
1314
- this.#logger.warn(
1315
- `[SpmService] scanProject: total timeout reached, returning partial results`
1316
- );
1317
- timedOut = true;
1318
- break;
1319
- }
1320
- const batch = allFiles.slice(i, i + BATCH_SIZE);
1321
- const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`;
1322
- try {
1323
- const recipes = await Promise.race([
1324
- ai.extractRecipes(batchLabel, batch),
1325
- new Promise((_, reject) =>
1326
- setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)
1327
- ),
1328
- ]);
1329
- if (Array.isArray(recipes)) {
1330
- allRecipes.push(...recipes);
1331
- }
1332
- } catch (err) {
1333
- if (isGeoOrProviderError(err)) {
1334
- const fallbacks = getAvailableFallbacks(
1335
- (process.env.ASD_AI_PROVIDER || 'google').toLowerCase()
1336
- );
1337
- for (const fb of fallbacks) {
1338
- try {
1339
- ai = createProvider({ provider: fb });
1340
- const recipes = await ai.extractRecipes(batchLabel, batch);
1341
- if (Array.isArray(recipes)) {
1342
- allRecipes.push(...recipes);
1343
- }
1344
- break;
1345
- } catch {
1346
- /* next fallback */
1347
- }
1348
- }
1349
- } else {
1350
- this.#logger.warn(
1351
- `[SpmService] scanProject AI batch ${batchLabel} failed: ${err.message}`
1352
- );
1353
- }
1354
- }
1355
- }
1356
- }
1357
- }
1358
-
1359
- // 工具增强:语义标准化 + 标签 + 评分
1360
- this._enrichRecipes(allRecipes);
1361
- }
1362
-
1363
- // 4. Guard 审计 — 对所有文件运行 GuardCheckEngine
1364
- let guardAudit = null;
1365
- if (this.#guardCheckEngine) {
1366
- try {
1367
- const guardFiles = allFiles.map((f) => ({ path: f.path, content: f.content }));
1368
- guardAudit = this.#guardCheckEngine.auditFiles(guardFiles, { scope: 'project' });
1369
-
1370
- // 将有违反的文件写入 ViolationsStore
1371
- if (this.#violationsStore && guardAudit.files) {
1372
- for (const fileResult of guardAudit.files) {
1373
- if (fileResult.violations.length > 0) {
1374
- this.#violationsStore.appendRun({
1375
- filePath: fileResult.filePath,
1376
- violations: fileResult.violations,
1377
- summary: `Project scan: ${fileResult.summary.errors} errors, ${fileResult.summary.warnings} warnings`,
1378
- });
1379
- }
1380
- }
1381
- }
1382
- } catch (e) {
1383
- this.#logger.warn(`[SpmService] Guard audit failed: ${e.message}`);
1384
- }
1385
- }
1386
-
1387
- this.#logger.info(
1388
- `[SpmService] scanProject complete: ${allRecipes.length} recipes, ${guardAudit?.summary?.totalViolations || 0} violations${timedOut ? ' (partial — timed out)' : ''}`
1389
- );
1390
-
1391
- return {
1392
- targets: allTargets.map((t) => t.name),
1393
- recipes: allRecipes,
1394
- guardAudit,
1395
- scannedFiles,
1396
- partial: timedOut,
1397
- };
1398
- }
1399
-
1400
- /**
1401
- * 标准化 AI 提取结果中的语义字段
1402
- * - preconditions (flat array) → constraints.preconditions
1403
- * - steps (string[]) → [{title, description}] 结构化格式
1404
- * - 确保 rationale / knowledgeType / complexity / scope 就位
1405
- */
1406
- static normalizeSemanticFields(recipe) {
1407
- // V3: AI 已输出完整结构,不再填充默认值
1408
- return recipe;
1409
- }
1410
-
1411
801
  /**
1412
802
  * 确保已加载 Package.swift(惰性加载)
1413
803
  */
@@ -1416,30 +806,8 @@ export class SpmService {
1416
806
  try {
1417
807
  await this.load();
1418
808
  } catch (e) {
1419
- this.#logger.warn(`[SpmService] 自动加载失败: ${e.message}`);
809
+ this.#logger.warn(`[SpmHelper] 自动加载失败: ${e.message}`);
1420
810
  }
1421
811
  }
1422
812
  }
1423
-
1424
- /**
1425
- * 刷新依赖映射(路由: POST /commands/spm-map)
1426
- * 会主动使缓存失效并重新解析
1427
- */
1428
- async updateDependencyMap(options = {}) {
1429
- this.#graphCache.invalidate('spm-graph');
1430
- this.#targetFilesCache.clear();
1431
- this.#graph.clear();
1432
- const parsed = await this.load();
1433
- if (!parsed) {
1434
- return { success: false, message: 'Package.swift not found', targets: 0, edges: 0 };
1435
- }
1436
- const json = this.#graph.toJSON();
1437
- return {
1438
- success: true,
1439
- message: `Dependency map updated for ${parsed.name}`,
1440
- targets: json.nodes.length,
1441
- edges: json.edges.length,
1442
- projectRoot: this.#projectRoot,
1443
- };
1444
- }
1445
813
  }