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.
- package/bin/cli.js +7 -0
- package/dashboard/dist/assets/index-D5jiDBQG.css +1 -0
- package/dashboard/dist/assets/{index-DfHY_3ln.js → index-e5OKj-Ni.js} +38 -38
- package/dashboard/dist/index.html +2 -2
- package/lib/cli/AiScanService.js +3 -3
- package/lib/core/AstAnalyzer.js +26 -4
- package/lib/core/analysis/CallEdgeResolver.js +402 -0
- package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
- package/lib/core/analysis/CallSiteExtractor.js +629 -0
- package/lib/core/analysis/DataFlowInferrer.js +57 -0
- package/lib/core/analysis/ImportPathResolver.js +189 -0
- package/lib/core/analysis/ImportRecord.js +105 -0
- package/lib/core/analysis/SymbolTableBuilder.js +211 -0
- package/lib/core/ast/ProjectGraph.js +8 -0
- package/lib/core/ast/lang-dart.js +352 -5
- package/lib/core/ast/lang-go.js +212 -10
- package/lib/core/ast/lang-java.js +205 -1
- package/lib/core/ast/lang-kotlin.js +330 -1
- package/lib/core/ast/lang-python.js +31 -2
- package/lib/core/ast/lang-rust.js +284 -3
- package/lib/core/ast/lang-swift.js +180 -1
- package/lib/core/ast/lang-typescript.js +290 -1
- package/lib/external/mcp/McpServer.js +1 -0
- package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +21 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +5 -4
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +70 -4
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +95 -1
- package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
- package/lib/external/mcp/handlers/bootstrap-internal.js +17 -6
- package/lib/external/mcp/handlers/consolidated.js +9 -0
- package/lib/external/mcp/handlers/guard.js +3 -3
- package/lib/external/mcp/handlers/structure.js +62 -0
- package/lib/external/mcp/handlers/wiki-external.js +66 -3
- package/lib/external/mcp/tools.js +36 -1
- package/lib/http/routes/remote.js +15 -15
- package/lib/injection/ServiceContainer.js +6 -11
- package/lib/platform/ios/index.js +2 -2
- package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
- package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
- package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
- package/lib/service/chat/ChatAgent.js +1 -1
- package/lib/service/chat/ChatAgentPrompts.js +13 -1
- package/lib/service/chat/ExplorationTracker.js +52 -8
- package/lib/service/chat/HandoffProtocol.js +19 -1
- package/lib/service/chat/WorkingMemory.js +3 -1
- package/lib/service/chat/memory/ActiveContext.js +3 -1
- package/lib/service/chat/memory/SessionStore.js +4 -3
- package/lib/service/chat/tools/ast-graph.js +229 -32
- package/lib/service/chat/tools/index.js +6 -1
- package/lib/service/chat/tools/infrastructure.js +5 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
- package/lib/service/knowledge/CodeEntityGraph.js +327 -2
- package/lib/service/knowledge/KnowledgeService.js +5 -1
- package/lib/service/module/ModuleService.js +9 -0
- package/lib/service/wiki/WikiGenerator.js +1 -1
- package/lib/shared/PathGuard.js +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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('[
|
|
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
|
-
`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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
|
-
`[
|
|
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(`[
|
|
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
|
-
`[
|
|
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(`[
|
|
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(`[
|
|
510
|
+
this.#logger.info(`[SpmHelper] 已添加包级依赖: .package(path: "${relPath}")`);
|
|
540
511
|
return { changed: true, content: patched };
|
|
541
512
|
}
|
|
542
513
|
|
|
543
|
-
this.#logger.warn(`[
|
|
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
|
-
//
|
|
579
|
-
|
|
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(`[
|
|
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
|
}
|