autosnippet 3.3.6 → 3.3.7
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/dashboard/dist/assets/icons-FHns2ypa.js +1 -0
- package/dashboard/dist/assets/index-BRJv5Y3r.js +135 -0
- package/dashboard/dist/assets/index-DzoB7kxK.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/dist/bin/cli.js +1 -0
- package/dist/lib/agent/AgentRuntime.d.ts +2 -2
- package/dist/lib/agent/AgentRuntime.js +26 -18
- package/dist/lib/agent/domain/ChatAgentTasks.js +4 -0
- package/dist/lib/agent/forced-summary.js +7 -2
- package/dist/lib/cli/AiScanService.js +4 -4
- package/dist/lib/core/discovery/ConfigWatcher.d.ts +64 -0
- package/dist/lib/core/discovery/ConfigWatcher.js +336 -0
- package/dist/lib/core/discovery/CustomConfigDiscoverer.d.ts +30 -0
- package/dist/lib/core/discovery/CustomConfigDiscoverer.js +1305 -0
- package/dist/lib/core/discovery/DiscovererPreference.d.ts +44 -0
- package/dist/lib/core/discovery/DiscovererPreference.js +141 -0
- package/dist/lib/core/discovery/DiscovererRegistry.d.ts +10 -1
- package/dist/lib/core/discovery/DiscovererRegistry.js +42 -2
- package/dist/lib/core/discovery/ProjectDiscoverer.d.ts +19 -0
- package/dist/lib/core/discovery/index.d.ts +2 -0
- package/dist/lib/core/discovery/index.js +4 -0
- package/dist/lib/core/discovery/parsers/CMakeParser.d.ts +32 -0
- package/dist/lib/core/discovery/parsers/CMakeParser.js +148 -0
- package/dist/lib/core/discovery/parsers/GradleDslParser.d.ts +43 -0
- package/dist/lib/core/discovery/parsers/GradleDslParser.js +171 -0
- package/dist/lib/core/discovery/parsers/JsonConfigParser.d.ts +45 -0
- package/dist/lib/core/discovery/parsers/JsonConfigParser.js +122 -0
- package/dist/lib/core/discovery/parsers/RubyDslParser.d.ts +49 -0
- package/dist/lib/core/discovery/parsers/RubyDslParser.js +282 -0
- package/dist/lib/core/discovery/parsers/StarlarkParser.d.ts +33 -0
- package/dist/lib/core/discovery/parsers/StarlarkParser.js +229 -0
- package/dist/lib/core/discovery/parsers/YamlConfigParser.d.ts +37 -0
- package/dist/lib/core/discovery/parsers/YamlConfigParser.js +212 -0
- package/dist/lib/domain/knowledge/KnowledgeEntry.d.ts +7 -1
- package/dist/lib/domain/knowledge/KnowledgeEntry.js +17 -3
- package/dist/lib/external/ai/AiProvider.d.ts +12 -0
- package/dist/lib/external/ai/AiProvider.js +24 -0
- package/dist/lib/external/ai/AiProviderManager.d.ts +101 -0
- package/dist/lib/external/ai/AiProviderManager.js +193 -0
- package/dist/lib/external/ai/providers/ClaudeProvider.js +11 -0
- package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +18 -0
- package/dist/lib/external/ai/providers/MockProvider.d.ts +21 -3
- package/dist/lib/external/ai/providers/MockProvider.js +290 -14
- package/dist/lib/external/ai/providers/OpenAiProvider.js +16 -0
- package/dist/lib/external/lark/LarkTransport.d.ts +5 -1
- package/dist/lib/external/lark/LarkTransport.js +10 -2
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.d.ts +20 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.js +432 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +16 -8
- package/dist/lib/external/mcp/handlers/bootstrap/refine.js +8 -0
- package/dist/lib/external/mcp/handlers/bootstrap-external.d.ts +9 -0
- package/dist/lib/external/mcp/handlers/bootstrap-external.js +2 -0
- package/dist/lib/external/mcp/handlers/bootstrap-internal.js +2 -0
- package/dist/lib/external/mcp/handlers/consolidated.js +2 -1
- package/dist/lib/external/mcp/handlers/dimension-complete-external.js +2 -1
- package/dist/lib/external/mcp/handlers/evolve-external.js +5 -2
- package/dist/lib/external/mcp/handlers/knowledge.js +5 -4
- package/dist/lib/http/routes/ai.js +111 -30
- package/dist/lib/http/routes/candidates.js +11 -4
- package/dist/lib/http/routes/commands.js +10 -1
- package/dist/lib/http/routes/health.js +11 -0
- package/dist/lib/http/routes/modules.js +27 -0
- package/dist/lib/http/routes/recipes.js +7 -0
- package/dist/lib/http/utils/routeHelpers.js +2 -1
- package/dist/lib/injection/ServiceContainer.d.ts +6 -5
- package/dist/lib/injection/ServiceContainer.js +11 -27
- package/dist/lib/injection/ServiceMap.d.ts +2 -0
- package/dist/lib/injection/modules/AiModule.d.ts +6 -9
- package/dist/lib/injection/modules/AiModule.js +82 -39
- package/dist/lib/injection/modules/PanoramaModule.js +1 -1
- package/dist/lib/service/cleanup/CleanupService.d.ts +54 -7
- package/dist/lib/service/cleanup/CleanupService.js +284 -37
- package/dist/lib/service/knowledge/CodeEntityGraph.d.ts +6 -0
- package/dist/lib/service/knowledge/CodeEntityGraph.js +16 -0
- package/dist/lib/service/knowledge/KnowledgeService.js +23 -10
- package/dist/lib/service/module/ModuleService.js +10 -19
- package/dist/lib/service/panorama/CouplingAnalyzer.d.ts +10 -1
- package/dist/lib/service/panorama/CouplingAnalyzer.js +44 -2
- package/dist/lib/service/panorama/DimensionAnalyzer.d.ts +1 -1
- package/dist/lib/service/panorama/DimensionAnalyzer.js +31 -17
- package/dist/lib/service/panorama/LayerInferrer.d.ts +16 -1
- package/dist/lib/service/panorama/LayerInferrer.js +118 -1
- package/dist/lib/service/panorama/ModuleDiscoverer.d.ts +9 -0
- package/dist/lib/service/panorama/ModuleDiscoverer.js +58 -2
- package/dist/lib/service/panorama/PanoramaAggregator.d.ts +6 -2
- package/dist/lib/service/panorama/PanoramaAggregator.js +84 -6
- package/dist/lib/service/panorama/PanoramaScanner.js +28 -0
- package/dist/lib/service/panorama/PanoramaService.js +10 -5
- package/dist/lib/service/panorama/PanoramaTypes.d.ts +38 -0
- package/dist/lib/service/panorama/RoleRefiner.d.ts +2 -0
- package/dist/lib/service/panorama/RoleRefiner.js +41 -0
- package/dist/lib/service/panorama/TechStackProfiler.d.ts +13 -0
- package/dist/lib/service/panorama/TechStackProfiler.js +191 -0
- package/dist/lib/service/skills/SignalCollector.d.ts +1 -0
- package/dist/lib/service/skills/SignalCollector.js +6 -5
- package/dist/lib/service/vector/ContextualEnricher.d.ts +1 -0
- package/dist/lib/service/vector/ContextualEnricher.js +4 -0
- package/dist/lib/shared/LanguageService.js +3 -0
- package/dist/lib/shared/developer-identity.d.ts +18 -0
- package/dist/lib/shared/developer-identity.js +62 -0
- package/dist/lib/shared/schemas/http-requests.d.ts +8 -17
- package/dist/lib/shared/schemas/http-requests.js +9 -6
- package/dist/lib/types/knowledge-wire.d.ts +1 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/icons-D1aVZYFW.js +0 -1
- package/dashboard/dist/assets/index-CxHOu8Hd.css +0 -1
- package/dashboard/dist/assets/index-DDdAOpYT.js +0 -128
|
@@ -169,10 +169,26 @@ export class CodeEntityGraph {
|
|
|
169
169
|
name: nodeObj.label || nodeObj.id || String(node),
|
|
170
170
|
metadata: {
|
|
171
171
|
nodeType: nodeObj.type || 'module',
|
|
172
|
+
...(nodeObj.layer != null ? { layer: nodeObj.layer } : {}),
|
|
173
|
+
...(nodeObj.version != null ? { version: nodeObj.version } : {}),
|
|
174
|
+
...(nodeObj.group != null ? { group: nodeObj.group } : {}),
|
|
175
|
+
...(nodeObj.fullPath != null ? { fullPath: nodeObj.fullPath } : {}),
|
|
176
|
+
...(nodeObj.indirect != null ? { indirect: nodeObj.indirect } : {}),
|
|
172
177
|
},
|
|
173
178
|
});
|
|
174
179
|
entities++;
|
|
175
180
|
}
|
|
181
|
+
// 存储 layers 元数据(如果存在)到特殊实体
|
|
182
|
+
const layers = depGraphData.layers;
|
|
183
|
+
if (layers?.length) {
|
|
184
|
+
this.#upsertEntity({
|
|
185
|
+
entityId: '__config_layers__',
|
|
186
|
+
entityType: 'config',
|
|
187
|
+
name: 'Config Layers',
|
|
188
|
+
metadata: { layers },
|
|
189
|
+
});
|
|
190
|
+
entities++;
|
|
191
|
+
}
|
|
176
192
|
});
|
|
177
193
|
run();
|
|
178
194
|
const result = { entitiesUpserted: entities, edgesCreated: 0, durationMs: Date.now() - t0 };
|
|
@@ -520,17 +520,28 @@ export class KnowledgeService {
|
|
|
520
520
|
// 为 QualityScorer 适配输入字段
|
|
521
521
|
const scorerInput = this._adaptForScorer(entry);
|
|
522
522
|
const result = this._qualityScorer.score(scorerInput);
|
|
523
|
-
// 更新 Quality
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
523
|
+
// 更新 Quality 值对象;同步计算 authority(0‑5)
|
|
524
|
+
const qualityJson = {
|
|
525
|
+
completeness: result.dimensions.completeness,
|
|
526
|
+
adaptation: result.dimensions.metadata,
|
|
527
|
+
documentation: result.dimensions.format,
|
|
528
|
+
overall: result.score,
|
|
529
|
+
grade: result.grade,
|
|
530
|
+
};
|
|
531
|
+
// 当 authority 从未手动设置(仍为 0)时,从 quality.overall 自动推导
|
|
532
|
+
const currentAuthority = entry.stats?.authority ?? 0;
|
|
533
|
+
const updatePayload = {
|
|
534
|
+
quality: JSON.stringify(qualityJson),
|
|
532
535
|
updatedAt: Math.floor(Date.now() / 1000),
|
|
533
|
-
}
|
|
536
|
+
};
|
|
537
|
+
if (currentAuthority === 0 && result.score > 0) {
|
|
538
|
+
const statsObj = entry.stats?.toJSON?.() ?? (typeof entry.stats === 'object' ? { ...entry.stats } : {});
|
|
539
|
+
updatePayload.stats = JSON.stringify({
|
|
540
|
+
...statsObj,
|
|
541
|
+
authority: Math.round(result.score * 5),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
await this.repository.update(id, updatePayload);
|
|
534
545
|
if (context.userId) {
|
|
535
546
|
await this._audit('update_knowledge_quality', id, context.userId, {
|
|
536
547
|
score: result.score,
|
|
@@ -565,6 +576,8 @@ export class KnowledgeService {
|
|
|
565
576
|
detail: `Lifecycle ${method} failed for ${id}`,
|
|
566
577
|
});
|
|
567
578
|
}
|
|
579
|
+
// 标记操作人到最后一条 lifecycleHistory 条目
|
|
580
|
+
entry.stampLastTransition(context.userId);
|
|
568
581
|
// 构建 DB 更新
|
|
569
582
|
// 注意: 不在此处 JSON.stringify — repository.update() 内部
|
|
570
583
|
// 通过 _entityToRow() 统一执行序列化, 传入原始值即可
|
|
@@ -352,8 +352,10 @@ export class ModuleService {
|
|
|
352
352
|
}
|
|
353
353
|
const scannedFiles = files.map((f) => ({ name: f.name, path: f.relativePath }));
|
|
354
354
|
this.#logger.info(`[ModuleService] scanTarget: ${targetName}, ${files.length} files`);
|
|
355
|
-
// 3. AI 提取
|
|
356
|
-
|
|
355
|
+
// 3. AI 提取 — mock 模式或无 agentFactory 时直接跳过
|
|
356
|
+
const aiManager = this.#container?.singletons
|
|
357
|
+
?._aiProviderManager;
|
|
358
|
+
if (!this.#agentFactory || aiManager?.isMock) {
|
|
357
359
|
return {
|
|
358
360
|
recipes: [],
|
|
359
361
|
scannedFiles,
|
|
@@ -375,21 +377,7 @@ export class ModuleService {
|
|
|
375
377
|
this.#enrichRecipes(recipes);
|
|
376
378
|
const result = { recipes, scannedFiles };
|
|
377
379
|
if (recipes.length === 0) {
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
const singletons = this.#container?.singletons;
|
|
381
|
-
const aiProvider = singletons?.aiProvider;
|
|
382
|
-
if (!aiProvider || aiProvider.name === 'mock') {
|
|
383
|
-
result.noAi = true;
|
|
384
|
-
result.message = 'AI 未配置,已跳过智能提取。请在 .env 中设置 API Key 后重试。';
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`;
|
|
392
|
-
}
|
|
380
|
+
result.message = `AI 提取完成,但未发现可复用的代码模式(${targetName}, ${files.length} 个文件)`;
|
|
393
381
|
}
|
|
394
382
|
onProgress?.({
|
|
395
383
|
type: 'scan:completed',
|
|
@@ -464,13 +452,15 @@ export class ModuleService {
|
|
|
464
452
|
path: f.relativePath,
|
|
465
453
|
targetName: f.targetName,
|
|
466
454
|
}));
|
|
467
|
-
// 3. AI 提取 Recipes
|
|
455
|
+
// 3. AI 提取 Recipes — mock 模式跳过
|
|
468
456
|
const allRecipes = [];
|
|
469
457
|
const PER_BATCH_TIMEOUT = options.batchTimeout || 90000;
|
|
470
458
|
const startTime = Date.now();
|
|
471
459
|
const TOTAL_TIMEOUT = options.totalTimeout || 540000;
|
|
472
460
|
let timedOut = false;
|
|
473
|
-
|
|
461
|
+
const scanAiMgr = this.#container?.singletons
|
|
462
|
+
?._aiProviderManager;
|
|
463
|
+
if (this.#agentFactory && !scanAiMgr?.isMock) {
|
|
474
464
|
const BATCH_SIZE = options.batchSize || 20;
|
|
475
465
|
for (let i = 0; i < allFiles.length; i += BATCH_SIZE) {
|
|
476
466
|
if (Date.now() - startTime > TOTAL_TIMEOUT) {
|
|
@@ -613,6 +603,7 @@ export class ModuleService {
|
|
|
613
603
|
go: 'go',
|
|
614
604
|
jvm: 'java',
|
|
615
605
|
python: 'python',
|
|
606
|
+
customConfig: 'swift',
|
|
616
607
|
generic: 'unknown',
|
|
617
608
|
};
|
|
618
609
|
return map[id] || 'unknown';
|
|
@@ -11,10 +11,18 @@ export interface CouplingMetrics {
|
|
|
11
11
|
fanIn: number;
|
|
12
12
|
fanOut: number;
|
|
13
13
|
}
|
|
14
|
+
export interface ExternalDepMetrics {
|
|
15
|
+
name: string;
|
|
16
|
+
fanIn: number;
|
|
17
|
+
/** 依赖此外部库的本地模块列表 */
|
|
18
|
+
dependedBy: string[];
|
|
19
|
+
}
|
|
14
20
|
export interface CouplingResult {
|
|
15
21
|
cycles: CyclicDependency[];
|
|
16
22
|
metrics: Map<string, CouplingMetrics>;
|
|
17
23
|
edges: Edge[];
|
|
24
|
+
/** 外部依赖 fan-in 统计(按 fan-in 降序排列) */
|
|
25
|
+
externalDeps: ExternalDepMetrics[];
|
|
18
26
|
}
|
|
19
27
|
export declare class CouplingAnalyzer {
|
|
20
28
|
#private;
|
|
@@ -22,6 +30,7 @@ export declare class CouplingAnalyzer {
|
|
|
22
30
|
/**
|
|
23
31
|
* 分析模块间耦合关系
|
|
24
32
|
* @param moduleFiles - Map<moduleName, filePaths[]>
|
|
33
|
+
* @param externalModules - 外部模块名集合(无源码但参与依赖图)
|
|
25
34
|
*/
|
|
26
|
-
analyze(moduleFiles: Map<string, string[]>): CouplingResult;
|
|
35
|
+
analyze(moduleFiles: Map<string, string[]>, externalModules?: Set<string>): CouplingResult;
|
|
27
36
|
}
|
|
@@ -23,8 +23,9 @@ export class CouplingAnalyzer {
|
|
|
23
23
|
/**
|
|
24
24
|
* 分析模块间耦合关系
|
|
25
25
|
* @param moduleFiles - Map<moduleName, filePaths[]>
|
|
26
|
+
* @param externalModules - 外部模块名集合(无源码但参与依赖图)
|
|
26
27
|
*/
|
|
27
|
-
analyze(moduleFiles) {
|
|
28
|
+
analyze(moduleFiles, externalModules) {
|
|
28
29
|
// 1. 构建 file → module 反向索引
|
|
29
30
|
const fileToModule = new Map();
|
|
30
31
|
for (const [mod, files] of moduleFiles) {
|
|
@@ -66,9 +67,11 @@ export class CouplingAnalyzer {
|
|
|
66
67
|
toM.fanIn++;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
// 5.5 外部依赖 fan-in 统计
|
|
71
|
+
const externalDeps = this.#computeExternalFanIn(moduleFiles, externalModules);
|
|
69
72
|
// 去重边 (同 from→to 聚合)
|
|
70
73
|
const dedupEdges = this.#deduplicateEdges(edges);
|
|
71
|
-
return { cycles, metrics, edges: dedupEdges };
|
|
74
|
+
return { cycles, metrics, edges: dedupEdges, externalDeps };
|
|
72
75
|
}
|
|
73
76
|
/* ─── Internal helpers ──────────────────────────── */
|
|
74
77
|
#buildModuleEdges(moduleFiles, fileToModule) {
|
|
@@ -174,6 +177,45 @@ export class CouplingAnalyzer {
|
|
|
174
177
|
severity: cycle.length > 3 ? 'error' : 'warning',
|
|
175
178
|
}));
|
|
176
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* 统计外部依赖的 fan-in(被多少本地模块依赖)
|
|
182
|
+
* 数据来源:knowledge_edges 中 from_type='module' AND to_type='module' 且 to 不在 moduleFiles 中
|
|
183
|
+
*/
|
|
184
|
+
#computeExternalFanIn(moduleFiles, externalModules) {
|
|
185
|
+
const fanInMap = new Map();
|
|
186
|
+
// 从 DB 查询 module-to-module depends_on 边
|
|
187
|
+
const rows = this.#db
|
|
188
|
+
.prepare(`SELECT from_id, to_id FROM knowledge_edges
|
|
189
|
+
WHERE relation = 'depends_on' AND from_type = 'module' AND to_type = 'module'`)
|
|
190
|
+
.all();
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
const fromId = row.from_id;
|
|
193
|
+
const toId = row.to_id;
|
|
194
|
+
// from 必须是本地模块, to 必须是外部模块
|
|
195
|
+
if (!moduleFiles.has(fromId)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (moduleFiles.has(toId)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// 如果提供了 externalModules 集合,检查 toId 是否在其中
|
|
202
|
+
if (externalModules && !externalModules.has(toId)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!fanInMap.has(toId)) {
|
|
206
|
+
fanInMap.set(toId, new Set());
|
|
207
|
+
}
|
|
208
|
+
fanInMap.get(toId).add(fromId);
|
|
209
|
+
}
|
|
210
|
+
// 转换为排序数组(按 fan-in 降序)
|
|
211
|
+
return [...fanInMap.entries()]
|
|
212
|
+
.map(([name, deps]) => ({
|
|
213
|
+
name,
|
|
214
|
+
fanIn: deps.size,
|
|
215
|
+
dependedBy: [...deps].sort(),
|
|
216
|
+
}))
|
|
217
|
+
.sort((a, b) => b.fanIn - a.fanIn);
|
|
218
|
+
}
|
|
177
219
|
#deduplicateEdges(edges) {
|
|
178
220
|
const key = (e) => `${e.from}→${e.to}`;
|
|
179
221
|
const map = new Map();
|
|
@@ -13,19 +13,14 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module DimensionAnalyzer
|
|
15
15
|
*/
|
|
16
|
-
import { classifyRecipeToDimension, DIMENSION_REGISTRY } from '#domain/dimension/index.js';
|
|
17
|
-
/* ═══ 维度定义 — 从统一注册表派生 ═══════════════════════ */
|
|
18
|
-
/**
|
|
19
|
-
* Panorama 使用全量维度注册表进行健康评估。
|
|
20
|
-
* 所有维度(含语言/框架条件维度)都参与评估 —
|
|
21
|
-
* 若该语言未激活但有 Recipe → 仍计入(只是不生成 gap 建议)。
|
|
22
|
-
*/
|
|
23
|
-
const DIMENSION_DEFS = DIMENSION_REGISTRY;
|
|
16
|
+
import { classifyRecipeToDimension, DIMENSION_REGISTRY, resolveActiveDimensions, } from '#domain/dimension/index.js';
|
|
24
17
|
/* ═══ DimensionAnalyzer Class ═════════════════════════════ */
|
|
25
18
|
export class DimensionAnalyzer {
|
|
26
19
|
#db;
|
|
27
|
-
|
|
20
|
+
#projectRoot;
|
|
21
|
+
constructor(db, projectRoot) {
|
|
28
22
|
this.#db = db;
|
|
23
|
+
this.#projectRoot = projectRoot;
|
|
29
24
|
}
|
|
30
25
|
/**
|
|
31
26
|
* 分析项目知识健康雷达
|
|
@@ -33,11 +28,13 @@ export class DimensionAnalyzer {
|
|
|
33
28
|
* @param moduleRoles — 项目中存在的模块角色 (用于 gap 优先级推断)
|
|
34
29
|
*/
|
|
35
30
|
analyze(moduleRoles) {
|
|
31
|
+
// 0. 按项目语言过滤活跃维度(排除无关语言/框架维度)
|
|
32
|
+
const activeDims = this.#resolveActiveDims();
|
|
36
33
|
// 1. 从 DB 获取所有活跃 recipe 的维度分类信息
|
|
37
34
|
const recipes = this.#fetchRecipeMetadata();
|
|
38
35
|
// 2. 将每条 recipe 映射到维度
|
|
39
36
|
const dimensionCounts = new Map();
|
|
40
|
-
for (const def of
|
|
37
|
+
for (const def of activeDims) {
|
|
41
38
|
dimensionCounts.set(def.id, { count: 0, titles: [] });
|
|
42
39
|
}
|
|
43
40
|
let totalRecipes = 0;
|
|
@@ -53,16 +50,16 @@ export class DimensionAnalyzer {
|
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
// 3. 计算各维度得分与状态
|
|
56
|
-
const dimensions =
|
|
53
|
+
const dimensions = activeDims.map((def) => {
|
|
57
54
|
const entry = dimensionCounts.get(def.id);
|
|
58
55
|
return this.#scoreDimension(def, entry.count, entry.titles);
|
|
59
56
|
});
|
|
60
57
|
// 4. 加权平均健康分
|
|
61
58
|
let weightedSum = 0;
|
|
62
59
|
let weightTotal = 0;
|
|
63
|
-
for (let i = 0; i <
|
|
64
|
-
weightedSum += dimensions[i].score *
|
|
65
|
-
weightTotal +=
|
|
60
|
+
for (let i = 0; i < activeDims.length; i++) {
|
|
61
|
+
weightedSum += dimensions[i].score * activeDims[i].weight;
|
|
62
|
+
weightTotal += activeDims[i].weight;
|
|
66
63
|
}
|
|
67
64
|
const overallScore = weightTotal > 0 ? Math.round(weightedSum / weightTotal) : 0;
|
|
68
65
|
// 5. 统计覆盖
|
|
@@ -78,9 +75,26 @@ export class DimensionAnalyzer {
|
|
|
78
75
|
};
|
|
79
76
|
// 6. 生成维度空白 (gaps)
|
|
80
77
|
const roleSet = new Set(moduleRoles);
|
|
81
|
-
const gaps = this.#detectDimensionGaps(dimensions, roleSet);
|
|
78
|
+
const gaps = this.#detectDimensionGaps(dimensions, activeDims, roleSet);
|
|
82
79
|
return { radar, gaps };
|
|
83
80
|
}
|
|
81
|
+
/* ─── 按项目语言解析活跃维度 ───────────────────── */
|
|
82
|
+
#resolveActiveDims() {
|
|
83
|
+
try {
|
|
84
|
+
const row = this.#db
|
|
85
|
+
.prepare(`SELECT primary_lang FROM bootstrap_snapshots
|
|
86
|
+
WHERE project_root = ? ORDER BY created_at DESC LIMIT 1`)
|
|
87
|
+
.get(this.#projectRoot);
|
|
88
|
+
const primaryLang = row?.primary_lang;
|
|
89
|
+
if (primaryLang) {
|
|
90
|
+
return resolveActiveDimensions(primaryLang);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// 无 bootstrap 数据 → 回退全量维度
|
|
95
|
+
}
|
|
96
|
+
return DIMENSION_REGISTRY;
|
|
97
|
+
}
|
|
84
98
|
/* ─── 从 DB 获取 recipe 元数据 ─────────────────── */
|
|
85
99
|
#fetchRecipeMetadata() {
|
|
86
100
|
try {
|
|
@@ -153,11 +167,11 @@ export class DimensionAnalyzer {
|
|
|
153
167
|
};
|
|
154
168
|
}
|
|
155
169
|
/* ─── 维度空白检测 ─────────────────────────────── */
|
|
156
|
-
#detectDimensionGaps(dimensions, moduleRoles) {
|
|
170
|
+
#detectDimensionGaps(dimensions, activeDims, moduleRoles) {
|
|
157
171
|
const gaps = [];
|
|
158
172
|
for (let i = 0; i < dimensions.length; i++) {
|
|
159
173
|
const dim = dimensions[i];
|
|
160
|
-
const def =
|
|
174
|
+
const def = activeDims[i];
|
|
161
175
|
if (dim.status !== 'missing' && dim.status !== 'weak') {
|
|
162
176
|
continue;
|
|
163
177
|
}
|
|
@@ -4,9 +4,23 @@
|
|
|
4
4
|
* 基于模块依赖图,通过去环 + 拓扑排序 + 最长路径法推断架构层级 (L0-Ln)。
|
|
5
5
|
* 底层 (L0) = Foundation/Core,顶层 = App/UI。
|
|
6
6
|
*
|
|
7
|
+
* 当配置文件声明了明确的层级结构时(如 Boxfile 的 layer 定义),
|
|
8
|
+
* 优先使用配置层级,仅对未覆盖的模块做拓扑推断。
|
|
9
|
+
*
|
|
7
10
|
* @module LayerInferrer
|
|
8
11
|
*/
|
|
9
12
|
import type { CyclicDependency, Edge, LayerHierarchy } from './PanoramaTypes.js';
|
|
13
|
+
export interface ConfigLayer {
|
|
14
|
+
name: string;
|
|
15
|
+
order: number;
|
|
16
|
+
accessibleLayers: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface InferOptions {
|
|
19
|
+
/** 来自配置文件的层级定义(如 Boxfile、Project.swift 等) */
|
|
20
|
+
configLayers?: ConfigLayer[] | null;
|
|
21
|
+
/** 模块 → 配置层级名映射 */
|
|
22
|
+
moduleLayerMap?: Map<string, string>;
|
|
23
|
+
}
|
|
10
24
|
export declare class LayerInferrer {
|
|
11
25
|
#private;
|
|
12
26
|
/**
|
|
@@ -14,6 +28,7 @@ export declare class LayerInferrer {
|
|
|
14
28
|
* @param edges - 模块间依赖边 (from depends_on/calls/data_flow to)
|
|
15
29
|
* @param modules - 所有模块名
|
|
16
30
|
* @param cycles - 已检测到的循环依赖
|
|
31
|
+
* @param options - 可选配置:configLayers(配置声明的层级)和 moduleLayerMap(模块→层级映射)
|
|
17
32
|
*/
|
|
18
|
-
infer(edges: Edge[], modules: string[], cycles: CyclicDependency[]): LayerHierarchy;
|
|
33
|
+
infer(edges: Edge[], modules: string[], cycles: CyclicDependency[], options?: InferOptions): LayerHierarchy;
|
|
19
34
|
}
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* 基于模块依赖图,通过去环 + 拓扑排序 + 最长路径法推断架构层级 (L0-Ln)。
|
|
5
5
|
* 底层 (L0) = Foundation/Core,顶层 = App/UI。
|
|
6
6
|
*
|
|
7
|
+
* 当配置文件声明了明确的层级结构时(如 Boxfile 的 layer 定义),
|
|
8
|
+
* 优先使用配置层级,仅对未覆盖的模块做拓扑推断。
|
|
9
|
+
*
|
|
7
10
|
* @module LayerInferrer
|
|
8
11
|
*/
|
|
9
12
|
/* ═══ Constants ═══════════════════════════════════════════ */
|
|
@@ -26,8 +29,122 @@ export class LayerInferrer {
|
|
|
26
29
|
* @param edges - 模块间依赖边 (from depends_on/calls/data_flow to)
|
|
27
30
|
* @param modules - 所有模块名
|
|
28
31
|
* @param cycles - 已检测到的循环依赖
|
|
32
|
+
* @param options - 可选配置:configLayers(配置声明的层级)和 moduleLayerMap(模块→层级映射)
|
|
29
33
|
*/
|
|
30
|
-
infer(edges, modules, cycles) {
|
|
34
|
+
infer(edges, modules, cycles, options) {
|
|
35
|
+
// 如果有配置文件声明的层级且覆盖了大部分模块,优先使用
|
|
36
|
+
if (options?.configLayers?.length && options.moduleLayerMap?.size) {
|
|
37
|
+
const coverage = this.#computeConfigCoverage(modules, options.moduleLayerMap);
|
|
38
|
+
if (coverage >= 0.5) {
|
|
39
|
+
return this.#inferFromConfig(edges, modules, options.configLayers, options.moduleLayerMap);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return this.#inferFromTopology(edges, modules, cycles);
|
|
43
|
+
}
|
|
44
|
+
/* ─── Config-based layer inference ──────────────── */
|
|
45
|
+
/**
|
|
46
|
+
* 基于配置文件声明的层级直接分配
|
|
47
|
+
* 未被配置覆盖的模块附加到最近匹配的层级
|
|
48
|
+
*/
|
|
49
|
+
#inferFromConfig(edges, modules, configLayers, moduleLayerMap) {
|
|
50
|
+
// 按 order 排序层级
|
|
51
|
+
const sortedLayers = [...configLayers].sort((a, b) => a.order - b.order);
|
|
52
|
+
// 构建层级名 → level 映射 (底层 = L0, 反序: order 最大的是底层)
|
|
53
|
+
// 配置中 order=0 是最高层 (如 Accessories), order=N 是最底层 (如 Vendors)
|
|
54
|
+
const maxOrder = sortedLayers.length > 0 ? sortedLayers[sortedLayers.length - 1].order : 0;
|
|
55
|
+
const layerNameToLevel = new Map();
|
|
56
|
+
for (const cl of sortedLayers) {
|
|
57
|
+
// 反转: 配置中 order 越大越底层 → level 越小
|
|
58
|
+
layerNameToLevel.set(cl.name, maxOrder - cl.order);
|
|
59
|
+
}
|
|
60
|
+
// 分配每个模块到层级
|
|
61
|
+
const moduleLevels = new Map();
|
|
62
|
+
const uncovered = [];
|
|
63
|
+
for (const mod of modules) {
|
|
64
|
+
const layerName = moduleLayerMap.get(mod);
|
|
65
|
+
if (layerName && layerNameToLevel.has(layerName)) {
|
|
66
|
+
moduleLevels.set(mod, layerNameToLevel.get(layerName));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
uncovered.push(mod);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 未覆盖的模块: 基于依赖关系推断最可能的层级
|
|
73
|
+
for (const mod of uncovered) {
|
|
74
|
+
const depEdges = edges.filter((e) => e.from === mod);
|
|
75
|
+
let bestLevel = maxOrder + 1; // 默认最高层
|
|
76
|
+
for (const e of depEdges) {
|
|
77
|
+
const targetLevel = moduleLevels.get(e.to);
|
|
78
|
+
if (targetLevel !== undefined) {
|
|
79
|
+
bestLevel = Math.min(bestLevel, targetLevel + 1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 如果没有依赖线索, 查看谁依赖它
|
|
83
|
+
if (bestLevel > maxOrder) {
|
|
84
|
+
const reverseEdges = edges.filter((e) => e.to === mod);
|
|
85
|
+
for (const e of reverseEdges) {
|
|
86
|
+
const sourceLevel = moduleLevels.get(e.from);
|
|
87
|
+
if (sourceLevel !== undefined) {
|
|
88
|
+
bestLevel = Math.min(bestLevel, Math.max(0, sourceLevel - 1));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 兜底: 放到最高层
|
|
93
|
+
if (bestLevel > maxOrder) {
|
|
94
|
+
bestLevel = maxOrder;
|
|
95
|
+
}
|
|
96
|
+
moduleLevels.set(mod, bestLevel);
|
|
97
|
+
}
|
|
98
|
+
// 聚合: 同层模块分组
|
|
99
|
+
const layerGroups = new Map();
|
|
100
|
+
for (const [mod, level] of moduleLevels) {
|
|
101
|
+
if (!layerGroups.has(level)) {
|
|
102
|
+
layerGroups.set(level, []);
|
|
103
|
+
}
|
|
104
|
+
layerGroups.get(level).push(mod);
|
|
105
|
+
}
|
|
106
|
+
// 构建 levelEntries, 使用配置中的层级名
|
|
107
|
+
const levelToConfigName = new Map();
|
|
108
|
+
for (const cl of sortedLayers) {
|
|
109
|
+
levelToConfigName.set(maxOrder - cl.order, cl.name);
|
|
110
|
+
}
|
|
111
|
+
const sortedLevels = [...layerGroups.entries()].sort((a, b) => a[0] - b[0]);
|
|
112
|
+
const levelEntries = sortedLevels.map(([level, mods]) => ({
|
|
113
|
+
level,
|
|
114
|
+
name: levelToConfigName.get(level) ?? this.#inferLayerName(mods, level, sortedLevels.length),
|
|
115
|
+
modules: mods.sort(),
|
|
116
|
+
}));
|
|
117
|
+
// 检测层级违规(基于 access 规则)
|
|
118
|
+
const violations = [];
|
|
119
|
+
for (const edge of edges) {
|
|
120
|
+
const fromLevel = moduleLevels.get(edge.from);
|
|
121
|
+
const toLevel = moduleLevels.get(edge.to);
|
|
122
|
+
if (fromLevel !== undefined && toLevel !== undefined && fromLevel < toLevel) {
|
|
123
|
+
violations.push({
|
|
124
|
+
from: edge.from,
|
|
125
|
+
to: edge.to,
|
|
126
|
+
fromLayer: fromLevel,
|
|
127
|
+
toLayer: toLevel,
|
|
128
|
+
relation: edge.relation,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { levels: levelEntries, violations };
|
|
133
|
+
}
|
|
134
|
+
#computeConfigCoverage(modules, moduleLayerMap) {
|
|
135
|
+
if (modules.length === 0) {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
let covered = 0;
|
|
139
|
+
for (const mod of modules) {
|
|
140
|
+
if (moduleLayerMap.has(mod)) {
|
|
141
|
+
covered++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return covered / modules.length;
|
|
145
|
+
}
|
|
146
|
+
/* ─── Topology-based layer inference ────────────── */
|
|
147
|
+
#inferFromTopology(edges, modules, cycles) {
|
|
31
148
|
// 1. 建图 (邻接表: from → to[])
|
|
32
149
|
const adjacency = new Map();
|
|
33
150
|
const reverseAdj = new Map();
|
|
@@ -21,4 +21,13 @@ export declare class ModuleDiscoverer {
|
|
|
21
21
|
* 若无 module 实体,返回空数组(让调用侧决定是否重新扫描)。
|
|
22
22
|
*/
|
|
23
23
|
discover(): ModuleCandidate[];
|
|
24
|
+
/**
|
|
25
|
+
* 读取 config layers 元数据(如果存在)
|
|
26
|
+
* @returns 从 `__config_layers__` 实体中恢复的层级定义
|
|
27
|
+
*/
|
|
28
|
+
readConfigLayers(): Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
order: number;
|
|
31
|
+
accessibleLayers: string[];
|
|
32
|
+
}> | null;
|
|
24
33
|
}
|
|
@@ -58,10 +58,11 @@ export class ModuleDiscoverer {
|
|
|
58
58
|
* 若无 module 实体,返回空数组(让调用侧决定是否重新扫描)。
|
|
59
59
|
*/
|
|
60
60
|
discover() {
|
|
61
|
-
// 从 code_entities 查 entity_type = 'module'
|
|
61
|
+
// 从 code_entities 查 entity_type = 'module'(排除 external/host 节点)
|
|
62
62
|
const moduleEntities = this.#db
|
|
63
63
|
.prepare(`SELECT DISTINCT entity_id, name FROM code_entities
|
|
64
|
-
WHERE entity_type = 'module' AND project_root =
|
|
64
|
+
WHERE entity_type = 'module' AND project_root = ?
|
|
65
|
+
AND COALESCE(json_extract(metadata_json, '$.nodeType'), 'local') NOT IN ('external', 'host')`)
|
|
65
66
|
.all(this.#projectRoot);
|
|
66
67
|
if (moduleEntities.length === 0) {
|
|
67
68
|
return [];
|
|
@@ -90,13 +91,68 @@ export class ModuleDiscoverer {
|
|
|
90
91
|
if (totalFileCount === 0) {
|
|
91
92
|
this.#enrichModuleFiles(moduleFiles);
|
|
92
93
|
}
|
|
94
|
+
// 读取模块 metadata 中的 configLayer 信息
|
|
95
|
+
const moduleLayerMap = this.#readModuleLayerMetadata(moduleEntities);
|
|
93
96
|
return [...moduleFiles.entries()].map(([name, files]) => ({
|
|
94
97
|
name,
|
|
95
98
|
inferredRole: inferTargetRole(name),
|
|
96
99
|
files: [...files],
|
|
100
|
+
configLayer: moduleLayerMap.get(name),
|
|
97
101
|
}));
|
|
98
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* 读取 config layers 元数据(如果存在)
|
|
105
|
+
* @returns 从 `__config_layers__` 实体中恢复的层级定义
|
|
106
|
+
*/
|
|
107
|
+
readConfigLayers() {
|
|
108
|
+
try {
|
|
109
|
+
const row = this.#db
|
|
110
|
+
.prepare(`SELECT metadata_json FROM code_entities
|
|
111
|
+
WHERE entity_id = '__config_layers__' AND entity_type = 'config' AND project_root = ?
|
|
112
|
+
LIMIT 1`)
|
|
113
|
+
.get(this.#projectRoot);
|
|
114
|
+
if (!row?.metadata_json) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const meta = typeof row.metadata_json === 'string' ? JSON.parse(row.metadata_json) : row.metadata_json;
|
|
118
|
+
if (Array.isArray(meta.layers) && meta.layers.length > 0) {
|
|
119
|
+
return meta.layers;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* skip parse error */
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
99
127
|
/* ─── 策略 1.5: 模块文件充填 ───────────────────── */
|
|
128
|
+
/**
|
|
129
|
+
* 从 code_entities metadata 中读取每个模块的 layer 信息
|
|
130
|
+
*/
|
|
131
|
+
#readModuleLayerMetadata(moduleEntities) {
|
|
132
|
+
const result = new Map();
|
|
133
|
+
for (const me of moduleEntities) {
|
|
134
|
+
const moduleName = me.entity_id;
|
|
135
|
+
try {
|
|
136
|
+
const row = this.#db
|
|
137
|
+
.prepare(`SELECT metadata_json FROM code_entities
|
|
138
|
+
WHERE entity_id = ? AND entity_type = 'module' AND project_root = ?
|
|
139
|
+
LIMIT 1`)
|
|
140
|
+
.get(moduleName, this.#projectRoot);
|
|
141
|
+
if (row?.metadata_json) {
|
|
142
|
+
const meta = typeof row.metadata_json === 'string'
|
|
143
|
+
? JSON.parse(row.metadata_json)
|
|
144
|
+
: row.metadata_json;
|
|
145
|
+
if (meta.layer && typeof meta.layer === 'string') {
|
|
146
|
+
result.set(moduleName, meta.layer);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* skip parse error */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
100
156
|
/**
|
|
101
157
|
* 为已知模块名填充文件路径:
|
|
102
158
|
* a. 文件系统扫描(递归 4 层找模块同名目录)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { CouplingAnalyzer } from './CouplingAnalyzer.js';
|
|
10
10
|
import { DimensionAnalyzer } from './DimensionAnalyzer.js';
|
|
11
|
-
import type { LayerInferrer } from './LayerInferrer.js';
|
|
11
|
+
import type { ConfigLayer, LayerInferrer } from './LayerInferrer.js';
|
|
12
12
|
import type { CeDbLike, PanoramaResult } from './PanoramaTypes.js';
|
|
13
13
|
import type { ModuleCandidate, RoleRefiner } from './RoleRefiner.js';
|
|
14
14
|
export interface PanoramaAggregatorOptions {
|
|
@@ -24,6 +24,10 @@ export declare class PanoramaAggregator {
|
|
|
24
24
|
constructor(opts: PanoramaAggregatorOptions);
|
|
25
25
|
/**
|
|
26
26
|
* 计算完整全景数据
|
|
27
|
+
* @param moduleCandidates 模块候选列表
|
|
28
|
+
* @param options.configLayers 来自配置文件的层级声明(如 Boxfile layer 定义)
|
|
27
29
|
*/
|
|
28
|
-
compute(moduleCandidates: ModuleCandidate[]
|
|
30
|
+
compute(moduleCandidates: ModuleCandidate[], options?: {
|
|
31
|
+
configLayers?: ConfigLayer[] | null;
|
|
32
|
+
}): PanoramaResult;
|
|
29
33
|
}
|