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.
Files changed (107) hide show
  1. package/dashboard/dist/assets/icons-FHns2ypa.js +1 -0
  2. package/dashboard/dist/assets/index-BRJv5Y3r.js +135 -0
  3. package/dashboard/dist/assets/index-DzoB7kxK.css +1 -0
  4. package/dashboard/dist/index.html +3 -3
  5. package/dist/bin/cli.js +1 -0
  6. package/dist/lib/agent/AgentRuntime.d.ts +2 -2
  7. package/dist/lib/agent/AgentRuntime.js +26 -18
  8. package/dist/lib/agent/domain/ChatAgentTasks.js +4 -0
  9. package/dist/lib/agent/forced-summary.js +7 -2
  10. package/dist/lib/cli/AiScanService.js +4 -4
  11. package/dist/lib/core/discovery/ConfigWatcher.d.ts +64 -0
  12. package/dist/lib/core/discovery/ConfigWatcher.js +336 -0
  13. package/dist/lib/core/discovery/CustomConfigDiscoverer.d.ts +30 -0
  14. package/dist/lib/core/discovery/CustomConfigDiscoverer.js +1305 -0
  15. package/dist/lib/core/discovery/DiscovererPreference.d.ts +44 -0
  16. package/dist/lib/core/discovery/DiscovererPreference.js +141 -0
  17. package/dist/lib/core/discovery/DiscovererRegistry.d.ts +10 -1
  18. package/dist/lib/core/discovery/DiscovererRegistry.js +42 -2
  19. package/dist/lib/core/discovery/ProjectDiscoverer.d.ts +19 -0
  20. package/dist/lib/core/discovery/index.d.ts +2 -0
  21. package/dist/lib/core/discovery/index.js +4 -0
  22. package/dist/lib/core/discovery/parsers/CMakeParser.d.ts +32 -0
  23. package/dist/lib/core/discovery/parsers/CMakeParser.js +148 -0
  24. package/dist/lib/core/discovery/parsers/GradleDslParser.d.ts +43 -0
  25. package/dist/lib/core/discovery/parsers/GradleDslParser.js +171 -0
  26. package/dist/lib/core/discovery/parsers/JsonConfigParser.d.ts +45 -0
  27. package/dist/lib/core/discovery/parsers/JsonConfigParser.js +122 -0
  28. package/dist/lib/core/discovery/parsers/RubyDslParser.d.ts +49 -0
  29. package/dist/lib/core/discovery/parsers/RubyDslParser.js +282 -0
  30. package/dist/lib/core/discovery/parsers/StarlarkParser.d.ts +33 -0
  31. package/dist/lib/core/discovery/parsers/StarlarkParser.js +229 -0
  32. package/dist/lib/core/discovery/parsers/YamlConfigParser.d.ts +37 -0
  33. package/dist/lib/core/discovery/parsers/YamlConfigParser.js +212 -0
  34. package/dist/lib/domain/knowledge/KnowledgeEntry.d.ts +7 -1
  35. package/dist/lib/domain/knowledge/KnowledgeEntry.js +17 -3
  36. package/dist/lib/external/ai/AiProvider.d.ts +12 -0
  37. package/dist/lib/external/ai/AiProvider.js +24 -0
  38. package/dist/lib/external/ai/AiProviderManager.d.ts +101 -0
  39. package/dist/lib/external/ai/AiProviderManager.js +193 -0
  40. package/dist/lib/external/ai/providers/ClaudeProvider.js +11 -0
  41. package/dist/lib/external/ai/providers/GoogleGeminiProvider.js +18 -0
  42. package/dist/lib/external/ai/providers/MockProvider.d.ts +21 -3
  43. package/dist/lib/external/ai/providers/MockProvider.js +290 -14
  44. package/dist/lib/external/ai/providers/OpenAiProvider.js +16 -0
  45. package/dist/lib/external/lark/LarkTransport.d.ts +5 -1
  46. package/dist/lib/external/lark/LarkTransport.js +10 -2
  47. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.d.ts +20 -0
  48. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/mock-pipeline.js +432 -0
  49. package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +16 -8
  50. package/dist/lib/external/mcp/handlers/bootstrap/refine.js +8 -0
  51. package/dist/lib/external/mcp/handlers/bootstrap-external.d.ts +9 -0
  52. package/dist/lib/external/mcp/handlers/bootstrap-external.js +2 -0
  53. package/dist/lib/external/mcp/handlers/bootstrap-internal.js +2 -0
  54. package/dist/lib/external/mcp/handlers/consolidated.js +2 -1
  55. package/dist/lib/external/mcp/handlers/dimension-complete-external.js +2 -1
  56. package/dist/lib/external/mcp/handlers/evolve-external.js +5 -2
  57. package/dist/lib/external/mcp/handlers/knowledge.js +5 -4
  58. package/dist/lib/http/routes/ai.js +111 -30
  59. package/dist/lib/http/routes/candidates.js +11 -4
  60. package/dist/lib/http/routes/commands.js +10 -1
  61. package/dist/lib/http/routes/health.js +11 -0
  62. package/dist/lib/http/routes/modules.js +27 -0
  63. package/dist/lib/http/routes/recipes.js +7 -0
  64. package/dist/lib/http/utils/routeHelpers.js +2 -1
  65. package/dist/lib/injection/ServiceContainer.d.ts +6 -5
  66. package/dist/lib/injection/ServiceContainer.js +11 -27
  67. package/dist/lib/injection/ServiceMap.d.ts +2 -0
  68. package/dist/lib/injection/modules/AiModule.d.ts +6 -9
  69. package/dist/lib/injection/modules/AiModule.js +82 -39
  70. package/dist/lib/injection/modules/PanoramaModule.js +1 -1
  71. package/dist/lib/service/cleanup/CleanupService.d.ts +54 -7
  72. package/dist/lib/service/cleanup/CleanupService.js +284 -37
  73. package/dist/lib/service/knowledge/CodeEntityGraph.d.ts +6 -0
  74. package/dist/lib/service/knowledge/CodeEntityGraph.js +16 -0
  75. package/dist/lib/service/knowledge/KnowledgeService.js +23 -10
  76. package/dist/lib/service/module/ModuleService.js +10 -19
  77. package/dist/lib/service/panorama/CouplingAnalyzer.d.ts +10 -1
  78. package/dist/lib/service/panorama/CouplingAnalyzer.js +44 -2
  79. package/dist/lib/service/panorama/DimensionAnalyzer.d.ts +1 -1
  80. package/dist/lib/service/panorama/DimensionAnalyzer.js +31 -17
  81. package/dist/lib/service/panorama/LayerInferrer.d.ts +16 -1
  82. package/dist/lib/service/panorama/LayerInferrer.js +118 -1
  83. package/dist/lib/service/panorama/ModuleDiscoverer.d.ts +9 -0
  84. package/dist/lib/service/panorama/ModuleDiscoverer.js +58 -2
  85. package/dist/lib/service/panorama/PanoramaAggregator.d.ts +6 -2
  86. package/dist/lib/service/panorama/PanoramaAggregator.js +84 -6
  87. package/dist/lib/service/panorama/PanoramaScanner.js +28 -0
  88. package/dist/lib/service/panorama/PanoramaService.js +10 -5
  89. package/dist/lib/service/panorama/PanoramaTypes.d.ts +38 -0
  90. package/dist/lib/service/panorama/RoleRefiner.d.ts +2 -0
  91. package/dist/lib/service/panorama/RoleRefiner.js +41 -0
  92. package/dist/lib/service/panorama/TechStackProfiler.d.ts +13 -0
  93. package/dist/lib/service/panorama/TechStackProfiler.js +191 -0
  94. package/dist/lib/service/skills/SignalCollector.d.ts +1 -0
  95. package/dist/lib/service/skills/SignalCollector.js +6 -5
  96. package/dist/lib/service/vector/ContextualEnricher.d.ts +1 -0
  97. package/dist/lib/service/vector/ContextualEnricher.js +4 -0
  98. package/dist/lib/shared/LanguageService.js +3 -0
  99. package/dist/lib/shared/developer-identity.d.ts +18 -0
  100. package/dist/lib/shared/developer-identity.js +62 -0
  101. package/dist/lib/shared/schemas/http-requests.d.ts +8 -17
  102. package/dist/lib/shared/schemas/http-requests.js +9 -6
  103. package/dist/lib/types/knowledge-wire.d.ts +1 -0
  104. package/package.json +1 -1
  105. package/dashboard/dist/assets/icons-D1aVZYFW.js +0 -1
  106. package/dashboard/dist/assets/index-CxHOu8Hd.css +0 -1
  107. 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
- await this.repository.update(id, {
525
- quality: JSON.stringify({
526
- completeness: result.dimensions.completeness,
527
- adaptation: result.dimensions.metadata,
528
- documentation: result.dimensions.format,
529
- overall: result.score,
530
- grade: result.grade,
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
- if (!this.#agentFactory) {
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
- // 检查是否因为 AI Provider mock 导致空结果
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
- if (this.#agentFactory) {
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();
@@ -16,7 +16,7 @@
16
16
  import type { CeDbLike, HealthRadar, KnowledgeGap } from './PanoramaTypes.js';
17
17
  export declare class DimensionAnalyzer {
18
18
  #private;
19
- constructor(db: CeDbLike);
19
+ constructor(db: CeDbLike, projectRoot: string);
20
20
  /**
21
21
  * 分析项目知识健康雷达
22
22
  *
@@ -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
- constructor(db) {
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 DIMENSION_DEFS) {
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 = DIMENSION_DEFS.map((def) => {
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 < DIMENSION_DEFS.length; i++) {
64
- weightedSum += dimensions[i].score * DIMENSION_DEFS[i].weight;
65
- weightTotal += DIMENSION_DEFS[i].weight;
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 = DIMENSION_DEFS[i];
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[]): PanoramaResult;
30
+ compute(moduleCandidates: ModuleCandidate[], options?: {
31
+ configLayers?: ConfigLayer[] | null;
32
+ }): PanoramaResult;
29
33
  }