autosnippet 3.0.0 → 3.0.2

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 (290) hide show
  1. package/README.md +230 -324
  2. package/bin/api-server.js +1 -1
  3. package/bin/cli.js +204 -244
  4. package/bin/mcp-server.js +5 -3
  5. package/config/knowledge-base.config.js +132 -132
  6. package/dashboard/dist/assets/{icons-CEfgGaZi.js → icons-Cdq22n2i.js} +95 -100
  7. package/dashboard/dist/assets/index-ClkyPkDX.js +133 -0
  8. package/dashboard/dist/assets/index-t4QrJwv1.css +1 -0
  9. package/dashboard/dist/index.html +3 -3
  10. package/lib/bootstrap.js +8 -8
  11. package/lib/cli/AiScanService.js +86 -40
  12. package/lib/cli/KnowledgeSyncService.js +113 -74
  13. package/lib/cli/SetupService.js +439 -277
  14. package/lib/cli/UpgradeService.js +63 -100
  15. package/lib/core/AstAnalyzer.js +276 -597
  16. package/lib/core/ast/ProjectGraph.js +101 -40
  17. package/lib/core/ast/ensure-grammars.js +232 -0
  18. package/lib/core/ast/index.js +115 -0
  19. package/lib/core/ast/lang-dart.js +661 -0
  20. package/lib/core/ast/lang-go.js +530 -0
  21. package/lib/core/ast/lang-java.js +435 -0
  22. package/lib/core/ast/lang-javascript.js +272 -0
  23. package/lib/core/ast/lang-kotlin.js +423 -0
  24. package/lib/core/ast/lang-objc.js +388 -0
  25. package/lib/core/ast/lang-python.js +371 -0
  26. package/lib/core/ast/lang-swift.js +337 -0
  27. package/lib/core/ast/lang-typescript.js +503 -0
  28. package/lib/core/capability/CapabilityProbe.js +18 -9
  29. package/lib/core/constitution/Constitution.js +2 -3
  30. package/lib/core/constitution/ConstitutionValidator.js +65 -24
  31. package/lib/core/discovery/DartDiscoverer.js +534 -0
  32. package/lib/core/discovery/DiscovererRegistry.js +83 -0
  33. package/lib/core/discovery/GenericDiscoverer.js +225 -0
  34. package/lib/core/discovery/GoDiscoverer.js +541 -0
  35. package/lib/core/discovery/JvmDiscoverer.js +506 -0
  36. package/lib/core/discovery/NodeDiscoverer.js +466 -0
  37. package/lib/core/discovery/ProjectDiscoverer.js +93 -0
  38. package/lib/core/discovery/PythonDiscoverer.js +338 -0
  39. package/lib/core/discovery/SpmDiscoverer.js +5 -0
  40. package/lib/core/discovery/index.js +53 -0
  41. package/lib/core/enhancement/EnhancementPack.js +71 -0
  42. package/lib/core/enhancement/EnhancementRegistry.js +47 -0
  43. package/lib/core/enhancement/android-enhancement.js +102 -0
  44. package/lib/core/enhancement/django-enhancement.js +70 -0
  45. package/lib/core/enhancement/fastapi-enhancement.js +63 -0
  46. package/lib/core/enhancement/go-grpc-enhancement.js +152 -0
  47. package/lib/core/enhancement/go-web-enhancement.js +201 -0
  48. package/lib/core/enhancement/index.js +65 -0
  49. package/lib/core/enhancement/node-server-enhancement.js +88 -0
  50. package/lib/core/enhancement/react-enhancement.js +86 -0
  51. package/lib/core/enhancement/spring-enhancement.js +112 -0
  52. package/lib/core/enhancement/vue-enhancement.js +96 -0
  53. package/lib/core/gateway/Gateway.js +8 -9
  54. package/lib/core/gateway/GatewayActionRegistry.js +1 -1
  55. package/lib/core/permission/PermissionManager.js +12 -8
  56. package/lib/domain/index.js +13 -9
  57. package/lib/domain/knowledge/KnowledgeEntry.js +111 -101
  58. package/lib/domain/knowledge/KnowledgeRepository.js +0 -1
  59. package/lib/domain/knowledge/Lifecycle.js +22 -22
  60. package/lib/domain/knowledge/index.js +9 -12
  61. package/lib/domain/knowledge/values/Constraints.js +31 -21
  62. package/lib/domain/knowledge/values/Content.js +21 -13
  63. package/lib/domain/knowledge/values/Quality.js +31 -18
  64. package/lib/domain/knowledge/values/Reasoning.js +20 -12
  65. package/lib/domain/knowledge/values/Relations.js +37 -25
  66. package/lib/domain/knowledge/values/Stats.js +18 -12
  67. package/lib/domain/knowledge/values/index.js +4 -3
  68. package/lib/domain/snippet/Snippet.js +35 -10
  69. package/lib/external/ai/AiFactory.js +48 -16
  70. package/lib/external/ai/AiProvider.js +184 -90
  71. package/lib/external/ai/providers/ClaudeProvider.js +25 -12
  72. package/lib/external/ai/providers/GoogleGeminiProvider.js +59 -30
  73. package/lib/external/ai/providers/MockProvider.js +9 -3
  74. package/lib/external/ai/providers/OpenAiProvider.js +51 -29
  75. package/lib/external/mcp/McpServer.js +66 -36
  76. package/lib/external/mcp/errorHandler.js +23 -11
  77. package/lib/external/mcp/handlers/LanguageExtensions.js +138 -53
  78. package/lib/external/mcp/handlers/TargetClassifier.js +52 -16
  79. package/lib/external/mcp/handlers/bootstrap/pipeline/BootstrapSnapshot.js +81 -20
  80. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +71 -42
  81. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +9 -17
  82. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +14 -9
  83. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +15 -7
  84. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +352 -153
  85. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +52 -12
  86. package/lib/external/mcp/handlers/bootstrap/skills.js +143 -39
  87. package/lib/external/mcp/handlers/bootstrap.js +691 -168
  88. package/lib/external/mcp/handlers/browse.js +66 -22
  89. package/lib/external/mcp/handlers/candidate.js +118 -35
  90. package/lib/external/mcp/handlers/consolidated.js +49 -17
  91. package/lib/external/mcp/handlers/guard.js +104 -39
  92. package/lib/external/mcp/handlers/knowledge.js +60 -36
  93. package/lib/external/mcp/handlers/search.js +43 -14
  94. package/lib/external/mcp/handlers/skill.js +120 -45
  95. package/lib/external/mcp/handlers/structure.js +240 -86
  96. package/lib/external/mcp/handlers/system.js +42 -12
  97. package/lib/external/mcp/handlers/wiki.js +58 -33
  98. package/lib/external/mcp/tools.js +306 -123
  99. package/lib/http/HttpServer.js +72 -47
  100. package/lib/http/middleware/RateLimiter.js +5 -3
  101. package/lib/http/middleware/errorHandler.js +6 -1
  102. package/lib/http/middleware/requestLogger.js +14 -3
  103. package/lib/http/middleware/roleResolver.js +30 -23
  104. package/lib/http/routes/ai.js +387 -265
  105. package/lib/http/routes/auth.js +81 -61
  106. package/lib/http/routes/candidates.js +430 -320
  107. package/lib/http/routes/commands.js +289 -189
  108. package/lib/http/routes/extract.js +158 -125
  109. package/lib/http/routes/guardRules.js +309 -217
  110. package/lib/http/routes/knowledge.js +213 -154
  111. package/lib/http/routes/modules.js +578 -0
  112. package/lib/http/routes/monitoring.js +6 -6
  113. package/lib/http/routes/recipes.js +104 -93
  114. package/lib/http/routes/search.js +361 -305
  115. package/lib/http/routes/skills.js +145 -98
  116. package/lib/http/routes/snippets.js +42 -30
  117. package/lib/http/routes/spm.js +3 -405
  118. package/lib/http/routes/violations.js +113 -93
  119. package/lib/http/routes/wiki.js +211 -170
  120. package/lib/http/utils/routeHelpers.js +3 -1
  121. package/lib/http/utils/sse-sessions.js +16 -6
  122. package/lib/http/utils/sse.js +15 -5
  123. package/lib/infrastructure/audit/AuditLogger.js +5 -2
  124. package/lib/infrastructure/audit/AuditStore.js +10 -7
  125. package/lib/infrastructure/cache/CacheService.js +3 -1
  126. package/lib/infrastructure/cache/GraphCache.js +8 -4
  127. package/lib/infrastructure/cache/UnifiedCacheAdapter.js +1 -1
  128. package/lib/infrastructure/config/ConfigLoader.js +9 -5
  129. package/lib/infrastructure/config/Defaults.js +30 -10
  130. package/lib/infrastructure/config/Paths.js +28 -8
  131. package/lib/infrastructure/config/TriggerSymbol.js +22 -10
  132. package/lib/infrastructure/database/DatabaseConnection.js +15 -10
  133. package/lib/infrastructure/database/migrations/001_initial_schema.js +0 -1
  134. package/lib/infrastructure/external/ClipboardManager.js +6 -2
  135. package/lib/infrastructure/external/NativeUi.js +50 -43
  136. package/lib/infrastructure/external/OpenBrowser.js +14 -17
  137. package/lib/infrastructure/external/XcodeAutomation.js +14 -258
  138. package/lib/infrastructure/logging/Logger.js +46 -30
  139. package/lib/infrastructure/monitoring/ErrorTracker.js +7 -5
  140. package/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -4
  141. package/lib/infrastructure/paths/HeaderResolver.js +25 -9
  142. package/lib/infrastructure/paths/PathFinder.js +34 -12
  143. package/lib/infrastructure/plugin/PluginManager.js +26 -8
  144. package/lib/infrastructure/realtime/RealtimeService.js +2 -2
  145. package/lib/infrastructure/vector/Chunker.js +22 -7
  146. package/lib/infrastructure/vector/IndexingPipeline.js +46 -22
  147. package/lib/infrastructure/vector/JsonVectorAdapter.js +90 -53
  148. package/lib/infrastructure/vector/VectorStore.js +28 -10
  149. package/lib/injection/ServiceContainer.js +247 -93
  150. package/lib/platform/ios/index.js +63 -0
  151. package/lib/platform/ios/routes/spm.js +437 -0
  152. package/lib/platform/ios/snippet/PlaceholderConverter.js +55 -0
  153. package/lib/platform/ios/snippet/XcodeCodec.js +112 -0
  154. package/lib/{service → platform/ios}/spm/DependencyGraph.js +41 -17
  155. package/lib/{service → platform/ios}/spm/PackageSwiftParser.js +41 -14
  156. package/lib/{service → platform/ios}/spm/PolicyEngine.js +9 -4
  157. package/lib/platform/ios/spm/SpmDiscoverer.js +122 -0
  158. package/lib/{service → platform/ios}/spm/SpmService.js +385 -127
  159. package/lib/{service/automation → platform/ios/xcode}/SaveEventFilter.js +8 -7
  160. package/lib/platform/ios/xcode/XcodeAutomation.js +350 -0
  161. package/lib/{service/automation → platform/ios/xcode}/XcodeIntegration.js +325 -145
  162. package/lib/repository/base/BaseRepository.js +7 -9
  163. package/lib/repository/knowledge/KnowledgeRepository.impl.js +98 -75
  164. package/lib/repository/token/TokenUsageStore.js +4 -2
  165. package/lib/service/automation/ActionPipeline.js +1 -1
  166. package/lib/service/automation/AutomationOrchestrator.js +8 -4
  167. package/lib/service/automation/ContextCollector.js +7 -5
  168. package/lib/service/automation/DirectiveDetector.js +23 -16
  169. package/lib/service/automation/FileWatcher.js +112 -56
  170. package/lib/service/automation/TriggerResolver.js +6 -4
  171. package/lib/service/automation/handlers/AlinkHandler.js +24 -12
  172. package/lib/service/automation/handlers/CreateHandler.js +19 -20
  173. package/lib/service/automation/handlers/DraftHandler.js +14 -8
  174. package/lib/service/automation/handlers/GuardHandler.js +93 -63
  175. package/lib/service/automation/handlers/HeaderHandler.js +1 -6
  176. package/lib/service/automation/handlers/SearchHandler.js +155 -88
  177. package/lib/service/bootstrap/BootstrapTaskManager.js +77 -35
  178. package/lib/service/candidate/SimilarityService.js +25 -9
  179. package/lib/service/chat/AnalystAgent.js +50 -24
  180. package/lib/service/chat/CandidateGuardrail.js +143 -17
  181. package/lib/service/chat/ChatAgent.js +759 -243
  182. package/lib/service/chat/ContextWindow.js +116 -71
  183. package/lib/service/chat/ConversationStore.js +77 -36
  184. package/lib/service/chat/EpisodicConsolidator.js +47 -23
  185. package/lib/service/chat/HandoffProtocol.js +98 -22
  186. package/lib/service/chat/Memory.js +34 -14
  187. package/lib/service/chat/ProducerAgent.js +40 -20
  188. package/lib/service/chat/ProjectSemanticMemory.js +109 -78
  189. package/lib/service/chat/ReasoningLayer.js +148 -70
  190. package/lib/service/chat/ReasoningTrace.js +44 -32
  191. package/lib/service/chat/TaskPipeline.js +39 -19
  192. package/lib/service/chat/ToolRegistry.js +48 -29
  193. package/lib/service/chat/WorkingMemory.js +44 -18
  194. package/lib/service/chat/tools.js +1096 -494
  195. package/lib/service/context/RecipeExtractor.js +132 -51
  196. package/lib/service/cursor/CursorDeliveryPipeline.js +82 -37
  197. package/lib/service/cursor/KnowledgeCompressor.js +25 -22
  198. package/lib/service/cursor/RulesGenerator.js +13 -7
  199. package/lib/service/cursor/SkillsSyncer.js +77 -27
  200. package/lib/service/cursor/TokenBudget.js +2 -2
  201. package/lib/service/cursor/TopicClassifier.js +54 -20
  202. package/lib/service/guard/ComplianceReporter.js +55 -43
  203. package/lib/service/guard/ExclusionManager.js +67 -29
  204. package/lib/service/guard/GuardCheckEngine.js +381 -86
  205. package/lib/service/guard/GuardFeedbackLoop.js +22 -10
  206. package/lib/service/guard/GuardService.js +29 -19
  207. package/lib/service/guard/RuleLearner.js +55 -23
  208. package/lib/service/guard/SourceFileCollector.js +27 -20
  209. package/lib/service/guard/ViolationsStore.js +43 -38
  210. package/lib/service/knowledge/CodeEntityGraph.js +147 -82
  211. package/lib/service/knowledge/ConfidenceRouter.js +12 -10
  212. package/lib/service/knowledge/KnowledgeFileWriter.js +147 -56
  213. package/lib/service/knowledge/KnowledgeGraphService.js +81 -34
  214. package/lib/service/knowledge/KnowledgeService.js +222 -112
  215. package/lib/service/module/ModuleService.js +969 -0
  216. package/lib/service/quality/FeedbackCollector.js +27 -15
  217. package/lib/service/quality/QualityScorer.js +78 -24
  218. package/lib/service/recipe/RecipeCandidateValidator.js +110 -44
  219. package/lib/service/recipe/RecipeParser.js +78 -45
  220. package/lib/service/search/CoarseRanker.js +43 -28
  221. package/lib/service/search/CrossEncoderReranker.js +32 -21
  222. package/lib/service/search/InvertedIndex.js +21 -7
  223. package/lib/service/search/MultiSignalRanker.js +90 -28
  224. package/lib/service/search/RetrievalFunnel.js +45 -24
  225. package/lib/service/search/SearchEngine.js +255 -103
  226. package/lib/service/skills/EventAggregator.js +32 -15
  227. package/lib/service/skills/SignalCollector.js +140 -64
  228. package/lib/service/skills/SkillAdvisor.js +79 -42
  229. package/lib/service/skills/SkillHooks.js +16 -14
  230. package/lib/service/snippet/PlaceholderConverter.js +5 -0
  231. package/lib/service/snippet/SnippetFactory.js +116 -99
  232. package/lib/service/snippet/SnippetInstaller.js +234 -62
  233. package/lib/service/snippet/codecs/SnippetCodec.js +67 -0
  234. package/lib/service/snippet/codecs/VSCodeCodec.js +102 -0
  235. package/lib/service/snippet/codecs/XcodeCodec.js +5 -0
  236. package/lib/service/wiki/WikiGenerator.js +637 -263
  237. package/lib/shared/DimensionCopyRegistry.js +472 -0
  238. package/lib/shared/LanguageService.js +399 -0
  239. package/lib/shared/PathGuard.js +45 -28
  240. package/lib/shared/RecipeReadinessChecker.js +72 -12
  241. package/lib/shared/constants.js +41 -41
  242. package/lib/shared/errors/BaseError.js +2 -2
  243. package/lib/shared/errors/index.js +4 -4
  244. package/lib/shared/similarity.js +25 -8
  245. package/lib/shared/token-utils.js +6 -2
  246. package/lib/shared/utils/common.js +12 -4
  247. package/package.json +49 -13
  248. package/scripts/bench-real-projects.mjs +256 -0
  249. package/scripts/build-native-ui.js +30 -30
  250. package/scripts/clear-old-vector-index.js +5 -35
  251. package/scripts/clear-vector-cache.js +7 -37
  252. package/scripts/collect-test-project-stats.mjs +160 -0
  253. package/scripts/diagnose-mcp.js +41 -32
  254. package/scripts/ensure-parse-package.js +6 -9
  255. package/scripts/generate-recipe-drafts.js +116 -77
  256. package/scripts/init-db.js +3 -20
  257. package/scripts/init-snippets.js +305 -0
  258. package/scripts/init-vector-db.js +173 -170
  259. package/scripts/install-cursor-skill.js +148 -104
  260. package/scripts/install-full.js +8 -21
  261. package/scripts/install-vscode-copilot.js +146 -145
  262. package/scripts/migrate-md-to-knowledge.mjs +139 -151
  263. package/scripts/postinstall-safe.js +5 -17
  264. package/scripts/recipe-audit.js +106 -82
  265. package/scripts/release.js +283 -323
  266. package/scripts/setup-mcp-config.js +60 -52
  267. package/scripts/verify-context-api.js +20 -20
  268. package/skills/autosnippet-analysis/SKILL.md +10 -6
  269. package/skills/autosnippet-candidates/SKILL.md +27 -26
  270. package/skills/autosnippet-coldstart/SKILL.md +555 -38
  271. package/skills/autosnippet-concepts/SKILL.md +349 -337
  272. package/skills/autosnippet-create/SKILL.md +5 -5
  273. package/skills/autosnippet-reference-dart/SKILL.md +543 -0
  274. package/skills/autosnippet-reference-go/SKILL.md +539 -0
  275. package/skills/autosnippet-reference-java/SKILL.md +534 -0
  276. package/skills/autosnippet-reference-jsts/SKILL.md +41 -9
  277. package/skills/autosnippet-reference-kotlin/SKILL.md +526 -0
  278. package/skills/autosnippet-reference-objc/SKILL.md +29 -6
  279. package/skills/autosnippet-reference-python/SKILL.md +800 -0
  280. package/skills/autosnippet-reference-swift/SKILL.md +70 -14
  281. package/skills/autosnippet-structure/SKILL.md +4 -4
  282. package/templates/cursor-rules/autosnippet-conventions.mdc +2 -2
  283. package/templates/recipes-setup/README.md +2 -2
  284. package/templates/recipes-setup/_template.md +1 -1
  285. package/dashboard/dist/assets/index-Bun3ld_J.css +0 -1
  286. package/dashboard/dist/assets/index-_Sk_Dmg3.js +0 -143
  287. package/resources/asd-entry/main.swift +0 -159
  288. package/scripts/build-asd-entry.js +0 -51
  289. package/scripts/init-xcode-snippets.js +0 -311
  290. package/template.json +0 -39
@@ -3,13 +3,19 @@
3
3
  * 整合 PackageSwiftParser + DependencyGraph + PolicyEngine
4
4
  */
5
5
 
6
- import Logger from '../../infrastructure/logging/Logger.js';
7
- import { PackageSwiftParser } from './PackageSwiftParser.js';
6
+ 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';
14
+ import { GraphCache } from '../../../infrastructure/cache/GraphCache.js';
15
+ import Logger from '../../../infrastructure/logging/Logger.js';
8
16
  import { DependencyGraph } from './DependencyGraph.js';
17
+ import { PackageSwiftParser } from './PackageSwiftParser.js';
9
18
  import { PolicyEngine } from './PolicyEngine.js';
10
- import { GraphCache } from '../../infrastructure/cache/GraphCache.js';
11
- import { basename as _pathBasename, dirname, relative, sep, resolve as pathResolve } from 'node:path';
12
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
13
19
 
14
20
  export class SpmService {
15
21
  #parser;
@@ -62,7 +68,7 @@ export class SpmService {
62
68
 
63
69
  /**
64
70
  * 加载并解析 Package.swift,构建依赖图
65
- * 支持多 Package 项目(如 BiliDemo 有多个子 Package)
71
+ * 支持多 Package 项目
66
72
  * 优先从磁盘缓存加载(Package.swift contentHash 匹配即命中)
67
73
  */
68
74
  async load() {
@@ -72,21 +78,23 @@ export class SpmService {
72
78
 
73
79
  // ── 收集所有 Package.swift 路径 + 联合 hash ──
74
80
  const packagePath = this.#parser.findPackageSwift(this.#projectRoot);
75
- const allPaths = packagePath ? [packagePath] : this.#parser.findAllPackageSwifts(this.#projectRoot);
81
+ const allPaths = packagePath
82
+ ? [packagePath]
83
+ : this.#parser.findAllPackageSwifts(this.#projectRoot);
76
84
  if (allPaths.length === 0) {
77
85
  this.#logger.warn('[SpmService] Package.swift 未找到');
78
86
  return null;
79
87
  }
80
88
 
81
- const combinedHash = allPaths
82
- .map(p => this.#graphCache.computeFileHash(p))
83
- .join(':');
89
+ const combinedHash = allPaths.map((p) => this.#graphCache.computeFileHash(p)).join(':');
84
90
 
85
91
  // ── 尝试命中缓存 ──
86
92
  const cached = this.#graphCache.load('spm-graph');
87
93
  if (cached && cached.contentHash === combinedHash) {
88
94
  this.#restoreFromCache(cached.data);
89
- this.#logger.info(`[SpmService] ⚡ 缓存命中 (${this.#graph.getNodes().length} targets, hash=${combinedHash.substring(0, 8)})`);
95
+ this.#logger.info(
96
+ `[SpmService] ⚡ 缓存命中 (${this.#graph.getNodes().length} targets, hash=${combinedHash.substring(0, 8)})`
97
+ );
90
98
  return cached.data.parsedResult;
91
99
  }
92
100
 
@@ -125,7 +133,7 @@ export class SpmService {
125
133
  */
126
134
  #loadMultiPackage(allPaths) {
127
135
  this.#logger.info(`[SpmService] 发现 ${allPaths.length} 个 Package.swift,逐一解析...`);
128
- let mergedTargets = [];
136
+ const mergedTargets = [];
129
137
  let lastName = 'multi-package';
130
138
  const allParsed = [];
131
139
 
@@ -158,7 +166,9 @@ export class SpmService {
158
166
 
159
167
  this.#buildPackageDepGraph(allParsed);
160
168
 
161
- this.#logger.info(`[SpmService] 多包加载完成: ${mergedTargets.length} targets from ${allPaths.length} packages`);
169
+ this.#logger.info(
170
+ `[SpmService] 多包加载完成: ${mergedTargets.length} targets from ${allPaths.length} packages`
171
+ );
162
172
  return {
163
173
  name: lastName,
164
174
  targets: mergedTargets,
@@ -172,17 +182,19 @@ export class SpmService {
172
182
  #saveToCache(contentHash, parsedResult) {
173
183
  const graphJSON = this.#graph.toJSON();
174
184
  const targetPackageEntries = [...this.#targetPackageMap.entries()];
175
- const packageDepEntries = [...this.#packageDepGraph.entries()].map(
176
- ([k, v]) => [k, [...v]]
185
+ const packageDepEntries = [...this.#packageDepGraph.entries()].map(([k, v]) => [k, [...v]]);
186
+
187
+ this.#graphCache.save(
188
+ 'spm-graph',
189
+ {
190
+ parsedResult,
191
+ graphNodes: graphJSON.nodes,
192
+ graphEdges: graphJSON.edges,
193
+ targetPackageMap: targetPackageEntries,
194
+ packageDepGraph: packageDepEntries,
195
+ },
196
+ { contentHash }
177
197
  );
178
-
179
- this.#graphCache.save('spm-graph', {
180
- parsedResult,
181
- graphNodes: graphJSON.nodes,
182
- graphEdges: graphJSON.edges,
183
- targetPackageMap: targetPackageEntries,
184
- packageDepGraph: packageDepEntries,
185
- }, { contentHash });
186
198
  }
187
199
 
188
200
  /**
@@ -258,17 +270,25 @@ export class SpmService {
258
270
  * @returns {boolean}
259
271
  */
260
272
  _canReachPackage(fromPkgPath, toPkgPath) {
261
- if (fromPkgPath === toPkgPath) return true;
273
+ if (fromPkgPath === toPkgPath) {
274
+ return true;
275
+ }
262
276
  const visited = new Set();
263
277
  const queue = [fromPkgPath];
264
278
  while (queue.length > 0) {
265
279
  const current = queue.shift();
266
- if (current === toPkgPath) return true;
267
- if (visited.has(current)) continue;
280
+ if (current === toPkgPath) {
281
+ return true;
282
+ }
283
+ if (visited.has(current)) {
284
+ continue;
285
+ }
268
286
  visited.add(current);
269
287
  const neighbors = this.#packageDepGraph.get(current);
270
288
  if (neighbors) {
271
- for (const n of neighbors) queue.push(n);
289
+ for (const n of neighbors) {
290
+ queue.push(n);
291
+ }
272
292
  }
273
293
  }
274
294
  return false;
@@ -295,7 +315,9 @@ export class SpmService {
295
315
  */
296
316
  getFixMode() {
297
317
  const env = (process.env.ASD_FIX_SPM_DEPS_MODE || '').toLowerCase().trim();
298
- if (env === 'off' || env === 'suggest' || env === 'fix') return env;
318
+ if (env === 'off' || env === 'suggest' || env === 'fix') {
319
+ return env;
320
+ }
299
321
  return 'suggest'; // 默认仅提示模式
300
322
  }
301
323
 
@@ -398,7 +420,7 @@ export class SpmService {
398
420
  return { ok: false, changed: false, error: 'Package.swift not found' };
399
421
  }
400
422
 
401
- let content = readFileSync(packagePath, 'utf8');
423
+ const content = readFileSync(packagePath, 'utf8');
402
424
 
403
425
  // ── 1. 构建依赖 token ──
404
426
  let depToken;
@@ -464,7 +486,9 @@ export class SpmService {
464
486
  this.#graph.addEdge(from, to);
465
487
  this.#parser.clearCache();
466
488
 
467
- this.#logger.info(`[SpmService] 已自动补齐依赖: ${from} -> ${to}${isCrossPackage ? ' (跨包)' : ''} (${packagePath})`);
489
+ this.#logger.info(
490
+ `[SpmService] 已自动补齐依赖: ${from} -> ${to}${isCrossPackage ? ' (跨包)' : ''} (${packagePath})`
491
+ );
468
492
  return { ok: true, changed: true, file: packagePath, crossPackage: isCrossPackage };
469
493
  } catch (err) {
470
494
  this.#logger.error(`[SpmService] addDependency failed: ${err.message}`);
@@ -486,7 +510,10 @@ export class SpmService {
486
510
 
487
511
  // 检查是否已有对该路径的 .package 声明
488
512
  const escapedPath = relPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
489
- const existsRe = new RegExp(`\\.package\\s*\\(\\s*(?:name\\s*:[^,]*,\\s*)?path\\s*:\\s*"${escapedPath}"`, 'm');
513
+ const existsRe = new RegExp(
514
+ `\\.package\\s*\\(\\s*(?:name\\s*:[^,]*,\\s*)?path\\s*:\\s*"${escapedPath}"`,
515
+ 'm'
516
+ );
490
517
  if (existsRe.test(content)) {
491
518
  return { changed: false, content };
492
519
  }
@@ -527,10 +554,14 @@ export class SpmService {
527
554
  resolveCurrentTarget(filePath) {
528
555
  try {
529
556
  const packagePath = this.#parser.findPackageSwift(dirname(filePath));
530
- if (!packagePath) return null;
557
+ if (!packagePath) {
558
+ return null;
559
+ }
531
560
 
532
561
  const nodes = this.#graph.getNodes();
533
- if (nodes.length === 0) return null;
562
+ if (nodes.length === 0) {
563
+ return null;
564
+ }
534
565
 
535
566
  const packageDir = dirname(packagePath);
536
567
  const rel = relative(packageDir, filePath);
@@ -539,7 +570,9 @@ export class SpmService {
539
570
  // 从路径段反向查找第一个匹配的 target(V1 原始逻辑)
540
571
  const nodeSet = new Set(nodes);
541
572
  for (let i = segments.length - 1; i >= 0; i--) {
542
- if (nodeSet.has(segments[i])) return segments[i];
573
+ if (nodeSet.has(segments[i])) {
574
+ return segments[i];
575
+ }
543
576
  }
544
577
 
545
578
  // 兜底:第一个 target
@@ -576,7 +609,7 @@ export class SpmService {
576
609
  edgeCount: this.#graph.edgeCount(),
577
610
  levels: Object.fromEntries(levels),
578
611
  cycleCount: cycles.length,
579
- cycles: cycles.map(c => c.join(' → ')),
612
+ cycles: cycles.map((c) => c.join(' → ')),
580
613
  };
581
614
  }
582
615
 
@@ -592,7 +625,7 @@ export class SpmService {
592
625
  // 如果图中有节点,先用图数据
593
626
  const nodes = this.#graph.getNodes();
594
627
  if (nodes.length > 0) {
595
- return [...nodes].map(name => ({
628
+ return [...nodes].map((name) => ({
596
629
  name,
597
630
  type: 'target',
598
631
  }));
@@ -602,12 +635,12 @@ export class SpmService {
602
635
  const allPaths = this.#parser.findAllPackageSwifts(this.#projectRoot);
603
636
 
604
637
  if (allPaths.length > 0) {
605
- const { dirname } = await import('path');
638
+ const { dirname } = await import('node:path');
606
639
  const targets = [];
607
640
  for (const pkgPath of allPaths) {
608
641
  try {
609
642
  const parsed = this.#parser.parse(pkgPath);
610
- if (parsed && parsed.targets) {
643
+ if (parsed?.targets) {
611
644
  const pkgDir = dirname(pkgPath);
612
645
  for (const t of parsed.targets) {
613
646
  targets.push({
@@ -624,13 +657,24 @@ export class SpmService {
624
657
  // Skip unparseable packages
625
658
  }
626
659
  }
627
- if (targets.length > 0) return targets;
660
+ if (targets.length > 0) {
661
+ return targets;
662
+ }
628
663
  }
629
664
 
630
665
  // 非 SPM 项目 fallback:将常见源码目录作为虚拟 Target
631
- const { existsSync } = await import('fs');
632
- const { join } = await import('path');
633
- const fallbackDirs = ['Sources', 'src', 'lib', 'app', 'pages', 'components', 'modules', 'packages'];
666
+ const { existsSync } = await import('node:fs');
667
+ const { join } = await import('node:path');
668
+ const fallbackDirs = [
669
+ 'Sources',
670
+ 'src',
671
+ 'lib',
672
+ 'app',
673
+ 'pages',
674
+ 'components',
675
+ 'modules',
676
+ 'packages',
677
+ ];
634
678
  const virtualTargets = [];
635
679
  for (const dir of fallbackDirs) {
636
680
  if (existsSync(join(this.#projectRoot, dir))) {
@@ -653,12 +697,12 @@ export class SpmService {
653
697
  const json = this.#graph.toJSON();
654
698
 
655
699
  return {
656
- nodes: json.nodes.map(name => ({
700
+ nodes: json.nodes.map((name) => ({
657
701
  id: name,
658
702
  label: name,
659
703
  type: options.level === 'package' ? 'package' : 'target',
660
704
  })),
661
- edges: json.edges.map(e => ({ from: e.from, to: e.to, source: 'spm' })),
705
+ edges: json.edges.map((e) => ({ from: e.from, to: e.to, source: 'spm' })),
662
706
  projectRoot: this.#projectRoot,
663
707
  generatedAt: new Date().toISOString(),
664
708
  };
@@ -672,7 +716,9 @@ export class SpmService {
672
716
  */
673
717
  async getTargetFiles(target) {
674
718
  const targetName = typeof target === 'string' ? target : target?.name;
675
- if (!targetName) return [];
719
+ if (!targetName) {
720
+ return [];
721
+ }
676
722
 
677
723
  // 内存缓存命中
678
724
  if (this.#targetFilesCache.has(targetName)) {
@@ -688,8 +734,8 @@ export class SpmService {
688
734
  * 实际的文件遍历逻辑(拆出以供缓存用)
689
735
  */
690
736
  async #walkTargetFiles(target, targetName) {
691
- const { existsSync, readdirSync, statSync } = await import('fs');
692
- const { join, dirname, relative, extname } = await import('path');
737
+ const { existsSync, readdirSync, statSync } = await import('node:fs');
738
+ const { join, dirname, extname } = await import('node:path');
693
739
 
694
740
  // 判断是否为虚拟目录 target(非 SPM fallback)
695
741
  const isDirectoryTarget = typeof target === 'object' && target?.type === 'directory';
@@ -716,9 +762,9 @@ export class SpmService {
716
762
  for (const pkgPath of allPaths) {
717
763
  try {
718
764
  const parsed = this.#parser.parse(pkgPath);
719
- if (parsed?.targets?.some(t => t.name === targetName)) {
765
+ if (parsed?.targets?.some((t) => t.name === targetName)) {
720
766
  const pkgDir = dirname(pkgPath);
721
- const matchTarget = parsed.targets.find(t => t.name === targetName);
767
+ const matchTarget = parsed.targets.find((t) => t.name === targetName);
722
768
  if (matchTarget?.path) {
723
769
  searchDirs.push(join(pkgDir, matchTarget.path));
724
770
  }
@@ -742,32 +788,86 @@ export class SpmService {
742
788
  break;
743
789
  }
744
790
  }
745
- if (!sourcesDir) return [];
791
+ if (!sourcesDir) {
792
+ return [];
793
+ }
746
794
 
747
795
  // 文件扩展名白名单
748
796
  const CODE_EXTS = isDirectoryTarget
749
- ? new Set(['.swift', '.m', '.mm', '.h', '.c', '.cpp', '.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.kt', '.go', '.rs', '.rb', '.vue', '.mjs', '.cjs'])
797
+ ? new Set([
798
+ '.swift',
799
+ '.m',
800
+ '.mm',
801
+ '.h',
802
+ '.c',
803
+ '.cpp',
804
+ '.js',
805
+ '.ts',
806
+ '.tsx',
807
+ '.jsx',
808
+ '.py',
809
+ '.java',
810
+ '.kt',
811
+ '.go',
812
+ '.rs',
813
+ '.rb',
814
+ '.vue',
815
+ '.mjs',
816
+ '.cjs',
817
+ ])
750
818
  : new Set(['.swift', '.m', '.h', '.c', '.cpp', '.mm']);
751
- const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'Pods', 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__', '.venv', 'target']);
819
+ const SKIP_DIRS = new Set([
820
+ 'node_modules',
821
+ '.git',
822
+ 'dist',
823
+ 'build',
824
+ '.next',
825
+ 'Pods',
826
+ 'Carthage',
827
+ '.build',
828
+ 'DerivedData',
829
+ 'vendor',
830
+ '__pycache__',
831
+ '.venv',
832
+ 'target',
833
+ ]);
752
834
  const MAX_FILES = 300;
753
835
 
754
836
  const files = [];
755
837
  const walk = (dir, rel = '') => {
756
- if (files.length >= MAX_FILES) return;
838
+ if (files.length >= MAX_FILES) {
839
+ return;
840
+ }
757
841
  let entries;
758
- try { entries = readdirSync(dir); } catch { return; }
842
+ try {
843
+ entries = readdirSync(dir);
844
+ } catch {
845
+ return;
846
+ }
759
847
  for (const entry of entries) {
760
- if (files.length >= MAX_FILES) break;
761
- if (entry.startsWith('.')) continue;
848
+ if (files.length >= MAX_FILES) {
849
+ break;
850
+ }
851
+ if (entry.startsWith('.')) {
852
+ continue;
853
+ }
762
854
  const full = join(dir, entry);
763
855
  const relPath = rel ? `${rel}/${entry}` : entry;
764
856
  let st;
765
- try { st = statSync(full); } catch { continue; }
857
+ try {
858
+ st = statSync(full);
859
+ } catch {
860
+ continue;
861
+ }
766
862
  if (st.isDirectory()) {
767
- if (SKIP_DIRS.has(entry)) continue;
863
+ if (SKIP_DIRS.has(entry)) {
864
+ continue;
865
+ }
768
866
  walk(full, relPath);
769
867
  } else if (CODE_EXTS.has(extname(entry).toLowerCase())) {
770
- if (st.size > 512 * 1024) continue; // 跳过 > 512KB
868
+ if (st.size > 512 * 1024) {
869
+ continue; // 跳过 > 512KB
870
+ }
771
871
  files.push({ name: entry, path: full, relativePath: relPath, size: st.size });
772
872
  }
773
873
  }
@@ -816,10 +916,14 @@ export class SpmService {
816
916
  onProgress?.({ type: 'scan:started', targetName });
817
917
  const fileList = await this.getTargetFiles(target);
818
918
  if (!fileList || fileList.length === 0) {
819
- return { recipes: [], scannedFiles: [], message: `No source files found for target: ${targetName}` };
919
+ return {
920
+ recipes: [],
921
+ scannedFiles: [],
922
+ message: `No source files found for target: ${targetName}`,
923
+ };
820
924
  }
821
925
 
822
- const scannedFilesMeta = fileList.map(f => {
926
+ const scannedFilesMeta = fileList.map((f) => {
823
927
  const filePath = typeof f === 'string' ? f : f.path;
824
928
  return { name: _pathBasename(filePath), path: f.relativePath || _pathBasename(filePath) };
825
929
  });
@@ -827,28 +931,39 @@ export class SpmService {
827
931
 
828
932
  // 2. 读取文件内容
829
933
  onProgress?.({ type: 'scan:reading', count: fileList.length });
830
- const { readFileSync } = await import('fs');
831
- const { basename, resolve } = await import('path');
832
- const files = fileList.map(f => {
833
- const filePath = typeof f === 'string' ? f : f.path;
834
- try {
835
- return { name: basename(filePath), path: filePath, relativePath: f.relativePath || basename(filePath), content: readFileSync(filePath, 'utf8') };
836
- } catch (err) {
837
- this.#logger.warn(`[SpmService] 读取文件失败: ${filePath} — ${err.message}`);
838
- return null;
839
- }
840
- }).filter(Boolean);
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);
841
952
 
842
953
  if (files.length === 0) {
843
954
  return { recipes: [], scannedFiles: [], message: 'All source files unreadable' };
844
955
  }
845
956
 
846
- const scannedFiles = files.map(f => ({ name: f.name, path: f.relativePath }));
957
+ const scannedFiles = files.map((f) => ({ name: f.name, path: f.relativePath }));
847
958
  this.#logger.info(`[SpmService] scanTarget: ${targetName}, ${files.length} files`);
848
959
 
849
960
  // 3. AI 提取 Recipes(通过 ChatAgent 统一入口)
850
961
  if (!this.#chatAgent && !this.#aiFactory) {
851
- return { recipes: [], scannedFiles, message: 'AI provider not configured. Please set ASD_AI_PROVIDER.' };
962
+ return {
963
+ recipes: [],
964
+ scannedFiles,
965
+ message: 'AI provider not configured. Please set ASD_AI_PROVIDER.',
966
+ };
852
967
  }
853
968
 
854
969
  onProgress?.({ type: 'scan:ai-extracting', fileCount: files.length, targetName });
@@ -856,9 +971,16 @@ export class SpmService {
856
971
  let recipes;
857
972
  try {
858
973
  if (this.#chatAgent) {
859
- const extractPromise = this.#chatAgent.executeTool('extract_recipes', { targetName, files });
974
+ const extractPromise = this.#chatAgent.executeTool('extract_recipes', {
975
+ targetName,
976
+ files,
977
+ });
860
978
  const timeoutPromise = new Promise((_, reject) =>
861
- setTimeout(() => reject(new Error(`AI extraction timeout (${AI_EXTRACT_TIMEOUT / 1000}s)`)), AI_EXTRACT_TIMEOUT));
979
+ setTimeout(
980
+ () => reject(new Error(`AI extraction timeout (${AI_EXTRACT_TIMEOUT / 1000}s)`)),
981
+ AI_EXTRACT_TIMEOUT
982
+ )
983
+ );
862
984
  const result = await Promise.race([extractPromise, timeoutPromise]);
863
985
  if (result?.error) {
864
986
  return { recipes: [], scannedFiles, message: result.error };
@@ -866,7 +988,12 @@ export class SpmService {
866
988
  recipes = result?.recipes || [];
867
989
  } else {
868
990
  // 降级: 直接使用 aiFactory(兼容未注入 chatAgent 的场景)
869
- const { getProviderWithFallback, isGeoOrProviderError, getAvailableFallbacks, createProvider } = this.#aiFactory;
991
+ const {
992
+ getProviderWithFallback,
993
+ isGeoOrProviderError,
994
+ getAvailableFallbacks,
995
+ createProvider,
996
+ } = this.#aiFactory;
870
997
  let ai = await getProviderWithFallback();
871
998
  if (!ai) {
872
999
  return { recipes: [], scannedFiles, message: 'AI provider not available' };
@@ -875,7 +1002,7 @@ export class SpmService {
875
1002
  // 加载语言参考 Skill 注入 AI 提取 prompt
876
1003
  let extractOpts = {};
877
1004
  try {
878
- const { loadBootstrapSkills } = await import('../../external/mcp/handlers/bootstrap.js');
1005
+ const { loadBootstrapSkills } = await import('../../../external/mcp/handlers/bootstrap.js');
879
1006
  const langProfile = ai._detectLanguageProfile?.(files);
880
1007
  const primaryLang = langProfile?.primaryLanguage;
881
1008
  if (primaryLang) {
@@ -884,7 +1011,9 @@ export class SpmService {
884
1011
  extractOpts = { skillReference: skillCtx.languageSkill.substring(0, 2000) };
885
1012
  }
886
1013
  }
887
- } catch { /* Skills not available */ }
1014
+ } catch {
1015
+ /* Skills not available */
1016
+ }
888
1017
 
889
1018
  try {
890
1019
  recipes = await ai.extractRecipes(targetName, files, extractOpts);
@@ -903,7 +1032,9 @@ export class SpmService {
903
1032
  this.#logger.warn(`[SpmService] fallback "${fbName}" failed: ${fbErr.message}`);
904
1033
  }
905
1034
  }
906
- if (!fallbackOk) throw primaryErr;
1035
+ if (!fallbackOk) {
1036
+ throw primaryErr;
1037
+ }
907
1038
  } else {
908
1039
  throw primaryErr;
909
1040
  }
@@ -914,17 +1045,19 @@ export class SpmService {
914
1045
  return { recipes: [], scannedFiles, message: `AI extraction failed: ${err.message}` };
915
1046
  }
916
1047
 
917
- if (!Array.isArray(recipes)) recipes = [];
1048
+ if (!Array.isArray(recipes)) {
1049
+ recipes = [];
1050
+ }
918
1051
 
919
1052
  // 3.5 Header 路径解析 + moduleName 注入
920
1053
  try {
921
- const PathFinder = await import('../../infrastructure/paths/PathFinder.js');
922
- const HeaderResolver = await import('../../infrastructure/paths/HeaderResolver.js');
1054
+ const PathFinder = await import('../../../infrastructure/paths/PathFinder.js');
1055
+ const HeaderResolver = await import('../../../infrastructure/paths/HeaderResolver.js');
923
1056
  const targetRootDir = await PathFinder.findTargetRootDir(files[0].path);
924
1057
  for (const recipe of recipes) {
925
1058
  const headerList = recipe.headers || [];
926
1059
  recipe.headerPaths = await Promise.all(
927
- headerList.map(h => HeaderResolver.resolveHeaderRelativePath(h, targetRootDir))
1060
+ headerList.map((h) => HeaderResolver.resolveHeaderRelativePath(h, targetRootDir))
928
1061
  );
929
1062
  recipe.moduleName = targetName;
930
1063
  }
@@ -938,9 +1071,21 @@ export class SpmService {
938
1071
 
939
1072
  const result = { recipes, scannedFiles };
940
1073
  if (recipes.length === 0) {
941
- result.message = `AI extraction returned 0 recipes for ${targetName} (${files.length} files analyzed)`;
1074
+ const aiInfo = this.#aiFactory
1075
+ ? (await import('../../../external/ai/AiFactory.js')).getAiConfigInfo()
1076
+ : null;
1077
+ if (aiInfo && !aiInfo.hasKey) {
1078
+ result.noAi = true;
1079
+ result.message = 'AI 未配置,已跳过智能提取。请在 .env 中设置 API Key 后重试。';
1080
+ } else {
1081
+ result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`;
1082
+ }
942
1083
  }
943
- onProgress?.({ type: 'scan:completed', recipeCount: recipes.length, fileCount: scannedFiles.length });
1084
+ onProgress?.({
1085
+ type: 'scan:completed',
1086
+ recipeCount: recipes.length,
1087
+ fileCount: scannedFiles.length,
1088
+ });
944
1089
  return result;
945
1090
  }
946
1091
 
@@ -951,15 +1096,15 @@ export class SpmService {
951
1096
  async scanProject(options = {}) {
952
1097
  this.#logger.info('[SpmService] scanProject: starting full-project scan');
953
1098
 
954
- const { readFileSync, existsSync, readdirSync, statSync } = await import('fs');
955
- const { basename: bn, join, extname, relative } = await import('path');
1099
+ const { readFileSync, existsSync, readdirSync, statSync } = await import('node:fs');
1100
+ const { basename: bn, join, extname, relative } = await import('node:path');
956
1101
 
957
1102
  // 1. 列出所有 target
958
1103
  const allTargets = await this.listTargets();
959
1104
 
960
1105
  // 2. 收集所有源文件(去重)
961
1106
  const seenPaths = new Set();
962
- const allFiles = []; // { name, path, relativePath, content, targetName }
1107
+ const allFiles = []; // { name, path, relativePath, content, targetName }
963
1108
  const MAX_FILES = options.maxFiles || 200;
964
1109
 
965
1110
  if (allTargets && allTargets.length > 0) {
@@ -969,7 +1114,9 @@ export class SpmService {
969
1114
  const fileList = await this.getTargetFiles(t);
970
1115
  for (const f of fileList) {
971
1116
  const fp = typeof f === 'string' ? f : f.path;
972
- if (seenPaths.has(fp)) continue;
1117
+ if (seenPaths.has(fp)) {
1118
+ continue;
1119
+ }
973
1120
  seenPaths.add(fp);
974
1121
  try {
975
1122
  const content = readFileSync(fp, 'utf8');
@@ -980,40 +1127,105 @@ export class SpmService {
980
1127
  content,
981
1128
  targetName: t.name,
982
1129
  });
983
- } catch { /* unreadable */ }
984
- if (allFiles.length >= MAX_FILES) break;
1130
+ } catch {
1131
+ /* unreadable */
1132
+ }
1133
+ if (allFiles.length >= MAX_FILES) {
1134
+ break;
1135
+ }
985
1136
  }
986
1137
  } catch (e) {
987
1138
  this.#logger.warn(`[SpmService] scanProject: skipping target ${t.name}: ${e.message}`);
988
1139
  }
989
- if (allFiles.length >= MAX_FILES) break;
1140
+ if (allFiles.length >= MAX_FILES) {
1141
+ break;
1142
+ }
990
1143
  }
991
1144
  } else {
992
1145
  // 非 SPM 项目:直接扫描常见源码目录(fallback)
993
1146
  this.#logger.info('[SpmService] scanProject: No SPM targets, falling back to directory scan');
994
- const CODE_EXTS = new Set(['.swift', '.m', '.mm', '.h', '.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.kt', '.go', '.rs', '.rb', '.vue', '.mjs', '.cjs']);
995
- const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'Pods', 'Carthage', '.build', 'DerivedData', 'vendor', '__pycache__', '.venv', 'target']);
996
- const srcDirs = ['Sources', 'src', 'lib', 'app', 'pages', 'components', 'modules', 'packages'];
1147
+ const CODE_EXTS = new Set([
1148
+ '.swift',
1149
+ '.m',
1150
+ '.mm',
1151
+ '.h',
1152
+ '.js',
1153
+ '.ts',
1154
+ '.tsx',
1155
+ '.jsx',
1156
+ '.py',
1157
+ '.java',
1158
+ '.kt',
1159
+ '.go',
1160
+ '.rs',
1161
+ '.rb',
1162
+ '.vue',
1163
+ '.mjs',
1164
+ '.cjs',
1165
+ ]);
1166
+ const SKIP_DIRS = new Set([
1167
+ 'node_modules',
1168
+ '.git',
1169
+ 'dist',
1170
+ 'build',
1171
+ '.next',
1172
+ 'Pods',
1173
+ 'Carthage',
1174
+ '.build',
1175
+ 'DerivedData',
1176
+ 'vendor',
1177
+ '__pycache__',
1178
+ '.venv',
1179
+ 'target',
1180
+ ]);
1181
+ const srcDirs = [
1182
+ 'Sources',
1183
+ 'src',
1184
+ 'lib',
1185
+ 'app',
1186
+ 'pages',
1187
+ 'components',
1188
+ 'modules',
1189
+ 'packages',
1190
+ ];
997
1191
 
998
1192
  const walkDir = (dir, targetName) => {
999
- if (allFiles.length >= MAX_FILES) return;
1193
+ if (allFiles.length >= MAX_FILES) {
1194
+ return;
1195
+ }
1000
1196
  let entries;
1001
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
1197
+ try {
1198
+ entries = readdirSync(dir, { withFileTypes: true });
1199
+ } catch {
1200
+ return;
1201
+ }
1002
1202
  for (const ent of entries) {
1003
- if (allFiles.length >= MAX_FILES) break;
1004
- if (ent.name.startsWith('.')) continue;
1203
+ if (allFiles.length >= MAX_FILES) {
1204
+ break;
1205
+ }
1206
+ if (ent.name.startsWith('.')) {
1207
+ continue;
1208
+ }
1005
1209
  const fp = join(dir, ent.name);
1006
1210
  if (ent.isDirectory()) {
1007
- if (SKIP_DIRS.has(ent.name)) continue;
1211
+ if (SKIP_DIRS.has(ent.name)) {
1212
+ continue;
1213
+ }
1008
1214
  walkDir(fp, targetName);
1009
1215
  } else if (ent.isFile() && CODE_EXTS.has(extname(ent.name).toLowerCase())) {
1010
- if (seenPaths.has(fp)) continue;
1216
+ if (seenPaths.has(fp)) {
1217
+ continue;
1218
+ }
1011
1219
  seenPaths.add(fp);
1012
1220
  try {
1013
1221
  const st = statSync(fp);
1014
- if (st.size > 512 * 1024) continue; // 跳过 > 512KB
1222
+ if (st.size > 512 * 1024) {
1223
+ continue; // 跳过 > 512KB
1224
+ }
1015
1225
  const content = readFileSync(fp, 'utf8');
1016
- if (content.split('\n').length < 5) continue; // 跳过微小文件
1226
+ if (content.split('\n').length < 5) {
1227
+ continue; // 跳过微小文件
1228
+ }
1017
1229
  allFiles.push({
1018
1230
  name: ent.name,
1019
1231
  path: fp,
@@ -1021,7 +1233,9 @@ export class SpmService {
1021
1233
  content,
1022
1234
  targetName,
1023
1235
  });
1024
- } catch { /* unreadable */ }
1236
+ } catch {
1237
+ /* unreadable */
1238
+ }
1025
1239
  }
1026
1240
  }
1027
1241
  };
@@ -1039,16 +1253,28 @@ export class SpmService {
1039
1253
  }
1040
1254
  }
1041
1255
 
1042
- this.#logger.info(`[SpmService] scanProject: ${allFiles.length} unique files from ${allTargets?.length || 0} targets`);
1256
+ this.#logger.info(
1257
+ `[SpmService] scanProject: ${allFiles.length} unique files from ${allTargets?.length || 0} targets`
1258
+ );
1043
1259
 
1044
1260
  if (allFiles.length === 0) {
1045
- return { targets: (allTargets || []).map(t => t.name), recipes: [], guardAudit: null, scannedFiles: [], message: 'No readable source files' };
1261
+ return {
1262
+ targets: (allTargets || []).map((t) => t.name),
1263
+ recipes: [],
1264
+ guardAudit: null,
1265
+ scannedFiles: [],
1266
+ message: 'No readable source files',
1267
+ };
1046
1268
  }
1047
1269
 
1048
- const scannedFiles = allFiles.map(f => ({ name: f.name, path: f.relativePath, targetName: f.targetName }));
1270
+ const scannedFiles = allFiles.map((f) => ({
1271
+ name: f.name,
1272
+ path: f.relativePath,
1273
+ targetName: f.targetName,
1274
+ }));
1049
1275
 
1050
1276
  // 3. AI 提取 Recipes(全局批量,分批避免 token 超限)— 通过 ChatAgent 统一入口
1051
- let allRecipes = [];
1277
+ const allRecipes = [];
1052
1278
  const PER_BATCH_TIMEOUT = options.batchTimeout || 90000; // 每批最多 90 秒
1053
1279
  const startTime = Date.now();
1054
1280
  const TOTAL_TIMEOUT = options.totalTimeout || 540000; // 总超时 9 分钟(留 1 分钟给 Guard)
@@ -1061,7 +1287,9 @@ export class SpmService {
1061
1287
  // 通过 ChatAgent extract_recipes 工具(内置 fallback)
1062
1288
  for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
1063
1289
  if (Date.now() - startTime > TOTAL_TIMEOUT) {
1064
- this.#logger.warn(`[SpmService] scanProject: total timeout reached after ${Math.floor((Date.now() - startTime) / 1000)}s, returning partial results`);
1290
+ this.#logger.warn(
1291
+ `[SpmService] scanProject: total timeout reached after ${Math.floor((Date.now() - startTime) / 1000)}s, returning partial results`
1292
+ );
1065
1293
  timedOut = true;
1066
1294
  break;
1067
1295
  }
@@ -1069,47 +1297,75 @@ export class SpmService {
1069
1297
  const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`;
1070
1298
  try {
1071
1299
  const result = await Promise.race([
1072
- this.#chatAgent.executeTool('extract_recipes', { targetName: batchLabel, files: batch }),
1073
- new Promise((_, reject) => setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)),
1300
+ this.#chatAgent.executeTool('extract_recipes', {
1301
+ targetName: batchLabel,
1302
+ files: batch,
1303
+ }),
1304
+ new Promise((_, reject) =>
1305
+ setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)
1306
+ ),
1074
1307
  ]);
1075
- if (Array.isArray(result?.recipes)) allRecipes.push(...result.recipes);
1308
+ if (Array.isArray(result?.recipes)) {
1309
+ allRecipes.push(...result.recipes);
1310
+ }
1076
1311
  } catch (err) {
1077
- this.#logger.warn(`[SpmService] scanProject ChatAgent batch ${batchLabel} failed: ${err.message}`);
1312
+ this.#logger.warn(
1313
+ `[SpmService] scanProject ChatAgent batch ${batchLabel} failed: ${err.message}`
1314
+ );
1078
1315
  }
1079
1316
  }
1080
1317
  } else {
1081
1318
  // 降级: 直接使用 aiFactory(兼容未注入 chatAgent 的场景)
1082
- const { getProviderWithFallback, isGeoOrProviderError, getAvailableFallbacks, createProvider } = this.#aiFactory;
1319
+ const {
1320
+ getProviderWithFallback,
1321
+ isGeoOrProviderError,
1322
+ getAvailableFallbacks,
1323
+ createProvider,
1324
+ } = this.#aiFactory;
1083
1325
  let ai = await getProviderWithFallback();
1084
1326
 
1085
1327
  if (ai) {
1086
1328
  for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
1087
1329
  if (Date.now() - startTime > TOTAL_TIMEOUT) {
1088
- this.#logger.warn(`[SpmService] scanProject: total timeout reached, returning partial results`);
1330
+ this.#logger.warn(
1331
+ `[SpmService] scanProject: total timeout reached, returning partial results`
1332
+ );
1089
1333
  timedOut = true;
1090
1334
  break;
1091
1335
  }
1092
1336
  const batch = allFiles.slice(i, i + BATCH_SIZE);
1093
1337
  const batchLabel = `project-batch-${Math.floor(i / BATCH_SIZE) + 1}`;
1094
1338
  try {
1095
- let recipes = await Promise.race([
1339
+ const recipes = await Promise.race([
1096
1340
  ai.extractRecipes(batchLabel, batch),
1097
- new Promise((_, reject) => setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)),
1341
+ new Promise((_, reject) =>
1342
+ setTimeout(() => reject(new Error('batch timeout')), PER_BATCH_TIMEOUT)
1343
+ ),
1098
1344
  ]);
1099
- if (Array.isArray(recipes)) allRecipes.push(...recipes);
1345
+ if (Array.isArray(recipes)) {
1346
+ allRecipes.push(...recipes);
1347
+ }
1100
1348
  } catch (err) {
1101
1349
  if (isGeoOrProviderError(err)) {
1102
- const fallbacks = getAvailableFallbacks((process.env.ASD_AI_PROVIDER || 'google').toLowerCase());
1350
+ const fallbacks = getAvailableFallbacks(
1351
+ (process.env.ASD_AI_PROVIDER || 'google').toLowerCase()
1352
+ );
1103
1353
  for (const fb of fallbacks) {
1104
1354
  try {
1105
1355
  ai = createProvider({ provider: fb });
1106
- let recipes = await ai.extractRecipes(batchLabel, batch);
1107
- if (Array.isArray(recipes)) allRecipes.push(...recipes);
1356
+ const recipes = await ai.extractRecipes(batchLabel, batch);
1357
+ if (Array.isArray(recipes)) {
1358
+ allRecipes.push(...recipes);
1359
+ }
1108
1360
  break;
1109
- } catch { /* next fallback */ }
1361
+ } catch {
1362
+ /* next fallback */
1363
+ }
1110
1364
  }
1111
1365
  } else {
1112
- this.#logger.warn(`[SpmService] scanProject AI batch ${batchLabel} failed: ${err.message}`);
1366
+ this.#logger.warn(
1367
+ `[SpmService] scanProject AI batch ${batchLabel} failed: ${err.message}`
1368
+ );
1113
1369
  }
1114
1370
  }
1115
1371
  }
@@ -1124,7 +1380,7 @@ export class SpmService {
1124
1380
  let guardAudit = null;
1125
1381
  if (this.#guardCheckEngine) {
1126
1382
  try {
1127
- const guardFiles = allFiles.map(f => ({ path: f.path, content: f.content }));
1383
+ const guardFiles = allFiles.map((f) => ({ path: f.path, content: f.content }));
1128
1384
  guardAudit = this.#guardCheckEngine.auditFiles(guardFiles, { scope: 'project' });
1129
1385
 
1130
1386
  // 将有违反的文件写入 ViolationsStore
@@ -1144,10 +1400,12 @@ export class SpmService {
1144
1400
  }
1145
1401
  }
1146
1402
 
1147
- this.#logger.info(`[SpmService] scanProject complete: ${allRecipes.length} recipes, ${guardAudit?.summary?.totalViolations || 0} violations${timedOut ? ' (partial — timed out)' : ''}`);
1403
+ this.#logger.info(
1404
+ `[SpmService] scanProject complete: ${allRecipes.length} recipes, ${guardAudit?.summary?.totalViolations || 0} violations${timedOut ? ' (partial — timed out)' : ''}`
1405
+ );
1148
1406
 
1149
1407
  return {
1150
- targets: allTargets.map(t => t.name),
1408
+ targets: allTargets.map((t) => t.name),
1151
1409
  recipes: allRecipes,
1152
1410
  guardAudit,
1153
1411
  scannedFiles,