autosnippet 3.3.0 → 3.3.3
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-BJ2mUBi8.js +1 -0
- package/dashboard/dist/assets/index-B659K9t5.js +128 -0
- package/dashboard/dist/assets/index-NCm40PMD.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +284 -142
- package/dist/lib/agent/context/ExplorationTracker.d.ts +2 -0
- package/dist/lib/agent/context/ExplorationTracker.js +21 -3
- package/dist/lib/agent/core/ToolExecutionPipeline.d.ts +3 -1
- package/dist/lib/agent/core/ToolExecutionPipeline.js +8 -1
- package/dist/lib/agent/forge/DynamicComposer.d.ts +58 -0
- package/dist/lib/agent/forge/DynamicComposer.js +99 -0
- package/dist/lib/agent/forge/SandboxRunner.d.ts +60 -0
- package/dist/lib/agent/forge/SandboxRunner.js +251 -0
- package/dist/lib/agent/forge/TemporaryToolRegistry.d.ts +76 -0
- package/dist/lib/agent/forge/TemporaryToolRegistry.js +154 -0
- package/dist/lib/agent/forge/ToolForge.d.ts +92 -0
- package/dist/lib/agent/forge/ToolForge.js +239 -0
- package/dist/lib/agent/forge/ToolRequirementAnalyzer.d.ts +44 -0
- package/dist/lib/agent/forge/ToolRequirementAnalyzer.js +119 -0
- package/dist/lib/agent/tools/ToolRegistry.d.ts +2 -0
- package/dist/lib/agent/tools/ToolRegistry.js +4 -0
- package/dist/lib/agent/tools/composite.js +0 -1
- package/dist/lib/agent/tools/index.d.ts +2 -50
- package/dist/lib/agent/tools/index.js +2 -3
- package/dist/lib/agent/tools/lifecycle.d.ts +1 -58
- package/dist/lib/agent/tools/lifecycle.js +2 -75
- package/dist/lib/cli/KnowledgeSyncService.d.ts +26 -0
- package/dist/lib/cli/KnowledgeSyncService.js +33 -1
- package/dist/lib/cli/deploy/FileManifest.d.ts +0 -21
- package/dist/lib/cli/deploy/FileManifest.js +0 -11
- package/dist/lib/domain/knowledge/KnowledgeEntry.d.ts +10 -0
- package/dist/lib/domain/knowledge/KnowledgeEntry.js +2 -0
- package/dist/lib/domain/knowledge/Lifecycle.d.ts +19 -2
- package/dist/lib/domain/knowledge/Lifecycle.js +32 -6
- package/dist/lib/domain/knowledge/UnifiedValidator.d.ts +1 -5
- package/dist/lib/domain/knowledge/UnifiedValidator.js +7 -44
- package/dist/lib/domain/knowledge/values/Stats.d.ts +29 -0
- package/dist/lib/domain/knowledge/values/Stats.js +41 -0
- package/dist/lib/external/mcp/McpServer.d.ts +19 -38
- package/dist/lib/external/mcp/McpServer.js +145 -117
- package/dist/lib/external/mcp/autoApproveInjector.js +0 -2
- package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.d.ts +26 -1
- package/dist/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +41 -0
- package/dist/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +49 -0
- package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.d.ts +3 -0
- package/dist/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +27 -0
- package/dist/lib/external/mcp/handlers/bootstrap/skills.js +1 -1
- package/dist/lib/external/mcp/handlers/bootstrap-external.js +1 -0
- package/dist/lib/external/mcp/handlers/bootstrap-internal.js +2 -0
- package/dist/lib/external/mcp/handlers/browse.d.ts +1 -0
- package/dist/lib/external/mcp/handlers/browse.js +2 -1
- package/dist/lib/external/mcp/handlers/consolidated.d.ts +117 -6
- package/dist/lib/external/mcp/handlers/consolidated.js +251 -71
- package/dist/lib/external/mcp/handlers/guard.d.ts +150 -0
- package/dist/lib/external/mcp/handlers/guard.js +239 -5
- package/dist/lib/external/mcp/handlers/knowledge.d.ts +0 -29
- package/dist/lib/external/mcp/handlers/knowledge.js +1 -76
- package/dist/lib/external/mcp/handlers/panorama.d.ts +36 -0
- package/dist/lib/external/mcp/handlers/panorama.js +156 -0
- package/dist/lib/external/mcp/handlers/system.d.ts +2 -54
- package/dist/lib/external/mcp/handlers/system.js +3 -113
- package/dist/lib/external/mcp/handlers/task.d.ts +13 -24
- package/dist/lib/external/mcp/handlers/task.js +218 -557
- package/dist/lib/external/mcp/handlers/types.d.ts +91 -8
- package/dist/lib/external/mcp/handlers/types.js +18 -1
- package/dist/lib/external/mcp/handlers/wiki-external.d.ts +18 -1
- package/dist/lib/external/mcp/handlers/wiki-external.js +16 -1
- package/dist/lib/external/mcp/tools.d.ts +18 -24
- package/dist/lib/external/mcp/tools.js +132 -159
- package/dist/lib/http/HttpServer.js +52 -0
- package/dist/lib/http/middleware/validate.js +7 -3
- package/dist/lib/http/routes/audit.d.ts +8 -0
- package/dist/lib/http/routes/audit.js +51 -0
- package/dist/lib/http/routes/guardReport.d.ts +10 -0
- package/dist/lib/http/routes/guardReport.js +143 -0
- package/dist/lib/http/routes/knowledge.js +32 -1
- package/dist/lib/http/routes/panorama.d.ts +11 -0
- package/dist/lib/http/routes/panorama.js +322 -0
- package/dist/lib/http/routes/signals.d.ts +10 -0
- package/dist/lib/http/routes/signals.js +104 -0
- package/dist/lib/http/routes/task.d.ts +2 -3
- package/dist/lib/http/routes/task.js +17 -347
- package/dist/lib/http/routes/violations.js +1 -1
- package/dist/lib/infrastructure/audit/AuditLogger.d.ts +6 -1
- package/dist/lib/infrastructure/audit/AuditLogger.js +14 -1
- package/dist/lib/infrastructure/database/drizzle/schema.d.ts +202 -504
- package/dist/lib/infrastructure/database/drizzle/schema.js +38 -69
- package/dist/lib/infrastructure/database/migrations/004_evolution_proposals.d.ts +8 -0
- package/dist/lib/infrastructure/database/migrations/004_evolution_proposals.js +43 -0
- package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.d.ts +9 -0
- package/dist/lib/infrastructure/database/migrations/005_recipe_source_refs.js +24 -0
- package/dist/lib/infrastructure/logging/Logger.d.ts +2 -0
- package/dist/lib/infrastructure/logging/Logger.js +34 -7
- package/dist/lib/infrastructure/monitoring/ErrorTracker.js +3 -1
- package/dist/lib/infrastructure/monitoring/PerformanceMonitor.d.ts +2 -2
- package/dist/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -10
- package/dist/lib/infrastructure/notification/LarkNotifier.d.ts +24 -0
- package/dist/lib/infrastructure/notification/LarkNotifier.js +97 -0
- package/dist/lib/infrastructure/report/ReportStore.d.ts +45 -0
- package/dist/lib/infrastructure/report/ReportStore.js +133 -0
- package/dist/lib/infrastructure/signal/SignalAggregator.d.ts +18 -0
- package/dist/lib/infrastructure/signal/SignalAggregator.js +84 -0
- package/dist/lib/infrastructure/signal/SignalBridge.d.ts +13 -0
- package/dist/lib/infrastructure/signal/SignalBridge.js +20 -0
- package/dist/lib/infrastructure/signal/SignalBus.d.ts +63 -0
- package/dist/lib/infrastructure/signal/SignalBus.js +106 -0
- package/dist/lib/infrastructure/signal/SignalTraceWriter.d.ts +36 -0
- package/dist/lib/infrastructure/signal/SignalTraceWriter.js +130 -0
- package/dist/lib/infrastructure/vector/HnswVectorAdapter.js +18 -2
- package/dist/lib/injection/ServiceContainer.js +8 -0
- package/dist/lib/injection/ServiceMap.d.ts +16 -10
- package/dist/lib/injection/modules/AgentModule.d.ts +1 -1
- package/dist/lib/injection/modules/AgentModule.js +7 -1
- package/dist/lib/injection/modules/AppModule.d.ts +1 -1
- package/dist/lib/injection/modules/AppModule.js +4 -13
- package/dist/lib/injection/modules/GuardModule.js +27 -2
- package/dist/lib/injection/modules/InfraModule.d.ts +0 -1
- package/dist/lib/injection/modules/InfraModule.js +9 -7
- package/dist/lib/injection/modules/KnowledgeModule.d.ts +5 -0
- package/dist/lib/injection/modules/KnowledgeModule.js +131 -0
- package/dist/lib/injection/modules/PanoramaModule.d.ts +18 -0
- package/dist/lib/injection/modules/PanoramaModule.js +76 -0
- package/dist/lib/injection/modules/SignalModule.d.ts +10 -0
- package/dist/lib/injection/modules/SignalModule.js +84 -0
- package/dist/lib/repository/knowledge/KnowledgeRepository.impl.d.ts +1 -0
- package/dist/lib/repository/knowledge/KnowledgeRepository.impl.js +6 -0
- package/dist/lib/service/bootstrap/BootstrapTaskManager.d.ts +3 -1
- package/dist/lib/service/bootstrap/BootstrapTaskManager.js +20 -1
- package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +45 -0
- package/dist/lib/service/bootstrap/UiStartupTasks.js +101 -0
- package/dist/lib/service/delivery/AgentInstructionsGenerator.js +4 -5
- package/dist/lib/service/delivery/CursorDeliveryPipeline.d.ts +3 -1
- package/dist/lib/service/delivery/CursorDeliveryPipeline.js +13 -10
- package/dist/lib/service/delivery/RulesGenerator.js +3 -2
- package/dist/lib/service/evolution/ConsolidationAdvisor.d.ts +114 -0
- package/dist/lib/service/evolution/ConsolidationAdvisor.js +542 -0
- package/dist/lib/service/evolution/ContradictionDetector.d.ts +54 -0
- package/dist/lib/service/evolution/ContradictionDetector.js +253 -0
- package/dist/lib/service/evolution/DecayDetector.d.ts +71 -0
- package/dist/lib/service/evolution/DecayDetector.js +244 -0
- package/dist/lib/service/evolution/EnhancementSuggester.d.ts +38 -0
- package/dist/lib/service/evolution/EnhancementSuggester.js +220 -0
- package/dist/lib/service/evolution/KnowledgeMetabolism.d.ts +82 -0
- package/dist/lib/service/evolution/KnowledgeMetabolism.js +167 -0
- package/dist/lib/service/evolution/RedundancyAnalyzer.d.ts +53 -0
- package/dist/lib/service/evolution/RedundancyAnalyzer.js +210 -0
- package/dist/lib/service/evolution/StagingManager.d.ts +57 -0
- package/dist/lib/service/evolution/StagingManager.js +201 -0
- package/dist/lib/service/guard/ComplianceReporter.d.ts +42 -2
- package/dist/lib/service/guard/ComplianceReporter.js +43 -5
- package/dist/lib/service/guard/CoverageAnalyzer.d.ts +54 -0
- package/dist/lib/service/guard/CoverageAnalyzer.js +149 -0
- package/dist/lib/service/guard/GuardCheckEngine.d.ts +42 -0
- package/dist/lib/service/guard/GuardCheckEngine.js +465 -14
- package/dist/lib/service/guard/GuardFeedbackLoop.d.ts +3 -0
- package/dist/lib/service/guard/GuardFeedbackLoop.js +9 -0
- package/dist/lib/service/guard/ReverseGuard.d.ts +73 -0
- package/dist/lib/service/guard/ReverseGuard.js +256 -0
- package/dist/lib/service/guard/RuleLearner.d.ts +12 -0
- package/dist/lib/service/guard/RuleLearner.js +38 -0
- package/dist/lib/service/guard/UncertaintyCollector.d.ts +83 -0
- package/dist/lib/service/guard/UncertaintyCollector.js +149 -0
- package/dist/lib/service/guard/ViolationsStore.d.ts +1 -0
- package/dist/lib/service/guard/ViolationsStore.js +33 -3
- package/dist/lib/service/knowledge/ConfidenceRouter.d.ts +13 -0
- package/dist/lib/service/knowledge/ConfidenceRouter.js +14 -0
- package/dist/lib/service/knowledge/KnowledgeService.js +22 -4
- package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +68 -0
- package/dist/lib/service/knowledge/SourceRefReconciler.js +309 -0
- package/dist/lib/service/panorama/CouplingAnalyzer.d.ts +27 -0
- package/dist/lib/service/panorama/CouplingAnalyzer.js +192 -0
- package/dist/lib/service/panorama/DimensionAnalyzer.d.ts +28 -0
- package/dist/lib/service/panorama/DimensionAnalyzer.js +320 -0
- package/dist/lib/service/panorama/LayerInferrer.d.ts +19 -0
- package/dist/lib/service/panorama/LayerInferrer.js +182 -0
- package/dist/lib/service/panorama/ModuleDiscoverer.d.ts +24 -0
- package/dist/lib/service/panorama/ModuleDiscoverer.js +185 -0
- package/dist/lib/service/panorama/PanoramaAggregator.d.ts +29 -0
- package/dist/lib/service/panorama/PanoramaAggregator.js +228 -0
- package/dist/lib/service/panorama/PanoramaScanner.d.ts +52 -0
- package/dist/lib/service/panorama/PanoramaScanner.js +188 -0
- package/dist/lib/service/panorama/PanoramaService.d.ts +125 -0
- package/dist/lib/service/panorama/PanoramaService.js +363 -0
- package/dist/lib/service/panorama/PanoramaTypes.d.ts +134 -0
- package/dist/lib/service/panorama/PanoramaTypes.js +6 -0
- package/dist/lib/service/panorama/RoleRefiner.d.ts +48 -0
- package/dist/lib/service/panorama/RoleRefiner.js +535 -0
- package/dist/lib/service/search/BM25Scorer.d.ts +2 -2
- package/dist/lib/service/search/CoarseRanker.d.ts +7 -6
- package/dist/lib/service/search/CoarseRanker.js +11 -10
- package/dist/lib/service/search/FieldWeightedScorer.d.ts +81 -0
- package/dist/lib/service/search/FieldWeightedScorer.js +318 -0
- package/dist/lib/service/search/MultiSignalRanker.d.ts +3 -2
- package/dist/lib/service/search/MultiSignalRanker.js +17 -1
- package/dist/lib/service/search/SearchEngine.d.ts +9 -7
- package/dist/lib/service/search/SearchEngine.js +67 -10
- package/dist/lib/service/search/SearchTypes.d.ts +25 -3
- package/dist/lib/service/search/SearchTypes.js +6 -1
- package/dist/lib/service/signal/HitRecorder.d.ts +68 -0
- package/dist/lib/service/signal/HitRecorder.js +173 -0
- package/dist/lib/service/skills/SignalCollector.d.ts +3 -1
- package/dist/lib/service/skills/SignalCollector.js +31 -1
- package/dist/lib/service/task/IntentExtractor.d.ts +66 -0
- package/dist/lib/service/task/IntentExtractor.js +256 -0
- package/dist/lib/service/task/PrimeSearchPipeline.d.ts +54 -0
- package/dist/lib/service/task/PrimeSearchPipeline.js +113 -0
- package/dist/lib/service/vector/VectorService.d.ts +3 -0
- package/dist/lib/service/vector/VectorService.js +38 -4
- package/dist/lib/shared/schemas/mcp-tools.d.ts +41 -96
- package/dist/lib/shared/schemas/mcp-tools.js +59 -119
- package/dist/scripts/analyze-signals.d.ts +20 -0
- package/dist/scripts/analyze-signals.js +155 -0
- package/dist/scripts/diagnose-mcp.js +1 -1
- package/package.json +1 -1
- package/skills/autosnippet-create/SKILL.md +98 -89
- package/skills/autosnippet-devdocs/SKILL.md +55 -57
- package/templates/claude-code/hooks/autosnippet-session.sh +10 -15
- package/templates/cursor-hooks/hooks/session-start.sh +1 -1
- package/templates/guard-ci.yml +2 -2
- package/templates/instructions/agent-static.md +2 -1
- package/templates/instructions/conventions.md +5 -6
- package/templates/recipes-setup/README.md +1 -2
- package/templates/recipes-setup/_template.md +39 -39
- package/dashboard/dist/assets/icons-BofcEZ3f.js +0 -1
- package/dashboard/dist/assets/index-D0whuycy.css +0 -1
- package/dashboard/dist/assets/index-SiN1GChm.js +0 -128
- package/dist/lib/domain/task/Task.d.ts +0 -140
- package/dist/lib/domain/task/Task.js +0 -254
- package/dist/lib/domain/task/TaskDependency.d.ts +0 -23
- package/dist/lib/domain/task/TaskDependency.js +0 -34
- package/dist/lib/domain/task/TaskIdGenerator.d.ts +0 -40
- package/dist/lib/domain/task/TaskIdGenerator.js +0 -75
- package/dist/lib/domain/task/index.d.ts +0 -4
- package/dist/lib/domain/task/index.js +0 -4
- package/dist/lib/infrastructure/database/migrations/002_add_tasks.d.ts +0 -11
- package/dist/lib/infrastructure/database/migrations/002_add_tasks.js +0 -86
- package/dist/lib/repository/task/TaskRepository.impl.d.ts +0 -171
- package/dist/lib/repository/task/TaskRepository.impl.js +0 -347
- package/dist/lib/service/task/TaskGraphService.d.ts +0 -222
- package/dist/lib/service/task/TaskGraphService.js +0 -597
- package/dist/lib/service/task/TaskKnowledgeBridge.d.ts +0 -95
- package/dist/lib/service/task/TaskKnowledgeBridge.js +0 -298
- package/dist/lib/service/task/TaskReadyEngine.d.ts +0 -84
- package/dist/lib/service/task/TaskReadyEngine.js +0 -115
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PanoramaService — 全景服务主入口
|
|
3
|
+
*
|
|
4
|
+
* 提供 4 个 operation:
|
|
5
|
+
* overview — 项目骨架 + 层级 + token 预算截断
|
|
6
|
+
* module — 单模块详情 + Recipe 覆盖率
|
|
7
|
+
* gaps — 知识空白区 (有代码无 Recipe)
|
|
8
|
+
* health — 全景健康度 (覆盖率 + 耦合度 + 衰退)
|
|
9
|
+
*
|
|
10
|
+
* 模块发现委托给 ModuleDiscoverer(SRP)。
|
|
11
|
+
* 内存缓存 + 24h 过期策略。
|
|
12
|
+
*
|
|
13
|
+
* @module PanoramaService
|
|
14
|
+
*/
|
|
15
|
+
import { ModuleDiscoverer } from './ModuleDiscoverer.js';
|
|
16
|
+
/* ═══ Constants ═══════════════════════════════════════════ */
|
|
17
|
+
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000; // 24h
|
|
18
|
+
/* ═══ PanoramaService Class ═══════════════════════════════ */
|
|
19
|
+
export class PanoramaService {
|
|
20
|
+
#aggregator;
|
|
21
|
+
#db;
|
|
22
|
+
#projectRoot;
|
|
23
|
+
#scanner;
|
|
24
|
+
#moduleDiscoverer;
|
|
25
|
+
#signalBus;
|
|
26
|
+
#cache = null;
|
|
27
|
+
#scanPromise = null;
|
|
28
|
+
#lastOverview = null;
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.#aggregator = opts.aggregator;
|
|
31
|
+
this.#db = opts.db;
|
|
32
|
+
this.#projectRoot = opts.projectRoot;
|
|
33
|
+
this.#scanner = opts.scanner ?? null;
|
|
34
|
+
this.#moduleDiscoverer =
|
|
35
|
+
opts.moduleDiscoverer ?? new ModuleDiscoverer(opts.db, opts.projectRoot);
|
|
36
|
+
this.#signalBus = opts.signalBus ?? null;
|
|
37
|
+
// Phase 2: 订阅信号标记缓存失效
|
|
38
|
+
if (this.#signalBus) {
|
|
39
|
+
this.#signalBus.subscribe('guard|lifecycle|usage', () => {
|
|
40
|
+
this.#cache = null;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/* ─── Public API ────────────────────────────────── */
|
|
45
|
+
/**
|
|
46
|
+
* 获取项目全景概览
|
|
47
|
+
*/
|
|
48
|
+
getOverview() {
|
|
49
|
+
const result = this.#getOrCompute();
|
|
50
|
+
const isStale = Date.now() - result.computedAt > STALE_THRESHOLD_MS;
|
|
51
|
+
let totalFiles = 0;
|
|
52
|
+
for (const [, mod] of result.modules) {
|
|
53
|
+
totalFiles += mod.fileCount;
|
|
54
|
+
}
|
|
55
|
+
// 使用项目级 recipe 总数,而非 per-module 之和
|
|
56
|
+
// 因为大多数 recipe scope 为 universal,无法匹配到具体模块
|
|
57
|
+
const totalRecipes = result.projectRecipeCount;
|
|
58
|
+
const overview = {
|
|
59
|
+
projectRoot: this.#projectRoot,
|
|
60
|
+
moduleCount: result.modules.size,
|
|
61
|
+
layerCount: result.layers.levels.length,
|
|
62
|
+
totalFiles,
|
|
63
|
+
totalRecipes,
|
|
64
|
+
overallCoverage: totalFiles > 0 ? totalRecipes / totalFiles : 0,
|
|
65
|
+
layers: result.layers.levels.map((l) => ({
|
|
66
|
+
level: l.level,
|
|
67
|
+
name: l.name,
|
|
68
|
+
modules: l.modules.map((mName) => {
|
|
69
|
+
const mod = result.modules.get(mName);
|
|
70
|
+
return {
|
|
71
|
+
name: mName,
|
|
72
|
+
role: mod?.refinedRole ?? 'feature',
|
|
73
|
+
fileCount: mod?.fileCount ?? 0,
|
|
74
|
+
recipeCount: mod?.recipeCount ?? 0,
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
})),
|
|
78
|
+
cycleCount: result.cycles.length,
|
|
79
|
+
gapCount: result.gaps.length,
|
|
80
|
+
healthRadar: result.healthRadar,
|
|
81
|
+
computedAt: result.computedAt,
|
|
82
|
+
stale: isStale,
|
|
83
|
+
};
|
|
84
|
+
// Phase 3: 发射 panorama 信号 — 覆盖率/健康度变化检测
|
|
85
|
+
if (this.#signalBus && this.#lastOverview) {
|
|
86
|
+
if (Math.abs(overview.overallCoverage - this.#lastOverview.overallCoverage) >= 0.05) {
|
|
87
|
+
this.#signalBus.send('panorama', 'PanoramaService.coverage', overview.overallCoverage, {
|
|
88
|
+
metadata: {
|
|
89
|
+
oldCoverage: this.#lastOverview.overallCoverage,
|
|
90
|
+
newCoverage: overview.overallCoverage,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.#lastOverview = overview;
|
|
96
|
+
return overview;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 获取单模块详情 (enriched with file groups, recipes, and summary)
|
|
100
|
+
*/
|
|
101
|
+
getModule(moduleName) {
|
|
102
|
+
const result = this.#getOrCompute();
|
|
103
|
+
const mod = result.modules.get(moduleName);
|
|
104
|
+
if (!mod) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
// Layer name: derive from module's own refinedRole (more accurate than level vote)
|
|
108
|
+
const layerName = PanoramaService.#roleToLayer(mod.refinedRole || mod.inferredRole);
|
|
109
|
+
// File groups: group by immediate subdirectory within the module
|
|
110
|
+
const fileGroups = PanoramaService.#groupFilesBySubdir(mod.files);
|
|
111
|
+
// Matched recipes from DB
|
|
112
|
+
const recipes = this.#findModuleRecipes(moduleName, mod);
|
|
113
|
+
// Uncovered file count estimate
|
|
114
|
+
const coveredFileCount = Math.min(recipes.length * 2, mod.fileCount); // rough heuristic
|
|
115
|
+
const uncoveredFileCount = Math.max(0, mod.fileCount - coveredFileCount);
|
|
116
|
+
// Neighbors from DB edges
|
|
117
|
+
const neighbors = [];
|
|
118
|
+
const outNeighbors = this.#db
|
|
119
|
+
.prepare(`SELECT DISTINCT to_id, weight FROM knowledge_edges
|
|
120
|
+
WHERE from_id = ? AND from_type = 'module' AND relation = 'depends_on'`)
|
|
121
|
+
.all(moduleName);
|
|
122
|
+
for (const n of outNeighbors) {
|
|
123
|
+
neighbors.push({
|
|
124
|
+
name: n.to_id,
|
|
125
|
+
direction: 'out',
|
|
126
|
+
weight: Number(n.weight ?? 1),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const inNeighbors = this.#db
|
|
130
|
+
.prepare(`SELECT DISTINCT from_id, weight FROM knowledge_edges
|
|
131
|
+
WHERE to_id = ? AND to_type = 'module' AND relation = 'depends_on'`)
|
|
132
|
+
.all(moduleName);
|
|
133
|
+
for (const n of inNeighbors) {
|
|
134
|
+
neighbors.push({
|
|
135
|
+
name: n.from_id,
|
|
136
|
+
direction: 'in',
|
|
137
|
+
weight: Number(n.weight ?? 1),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Generate summary
|
|
141
|
+
const summary = PanoramaService.#generateModuleSummary(mod, layerName, fileGroups, recipes, neighbors);
|
|
142
|
+
return { module: mod, layerName, neighbors, fileGroups, recipes, uncoveredFileCount, summary };
|
|
143
|
+
}
|
|
144
|
+
/* ─── Module detail helpers ─────────────────────── */
|
|
145
|
+
/** Role → layer name mapping (consistent with PanoramaAggregator) */
|
|
146
|
+
static #roleToLayer(role) {
|
|
147
|
+
const map = {
|
|
148
|
+
core: 'Foundation',
|
|
149
|
+
foundation: 'Foundation',
|
|
150
|
+
model: 'Model',
|
|
151
|
+
service: 'Service',
|
|
152
|
+
networking: 'Infrastructure',
|
|
153
|
+
storage: 'Infrastructure',
|
|
154
|
+
ui: 'UI',
|
|
155
|
+
feature: 'Feature',
|
|
156
|
+
config: 'Configuration',
|
|
157
|
+
test: 'Test',
|
|
158
|
+
app: 'Application',
|
|
159
|
+
};
|
|
160
|
+
return map[role] ?? 'Feature';
|
|
161
|
+
}
|
|
162
|
+
/** Group file paths by their immediate subdirectory within the module */
|
|
163
|
+
static #groupFilesBySubdir(files) {
|
|
164
|
+
if (files.length === 0) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
// Find common prefix to determine module root
|
|
168
|
+
const prefix = PanoramaService.#commonPathPrefix(files);
|
|
169
|
+
const groups = new Map();
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
const relative = f.slice(prefix.length);
|
|
172
|
+
const firstSlash = relative.indexOf('/');
|
|
173
|
+
const group = firstSlash > 0 ? relative.slice(0, firstSlash) : '(root)';
|
|
174
|
+
if (!groups.has(group)) {
|
|
175
|
+
groups.set(group, []);
|
|
176
|
+
}
|
|
177
|
+
groups.get(group).push(f);
|
|
178
|
+
}
|
|
179
|
+
return [...groups.entries()]
|
|
180
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
181
|
+
.map(([group, groupFiles]) => ({ group, files: groupFiles, count: groupFiles.length }));
|
|
182
|
+
}
|
|
183
|
+
static #commonPathPrefix(paths) {
|
|
184
|
+
if (paths.length === 0) {
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
let prefix = paths[0];
|
|
188
|
+
for (const p of paths) {
|
|
189
|
+
while (!p.startsWith(prefix)) {
|
|
190
|
+
const lastSlash = prefix.lastIndexOf('/');
|
|
191
|
+
if (lastSlash < 0) {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
prefix = prefix.slice(0, lastSlash + 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return prefix;
|
|
198
|
+
}
|
|
199
|
+
/** Find recipes related to this module by category, trigger, or title match */
|
|
200
|
+
#findModuleRecipes(moduleName, mod) {
|
|
201
|
+
try {
|
|
202
|
+
// Map refined role to typical recipe categories
|
|
203
|
+
const roleCategories = {
|
|
204
|
+
networking: ['Network', 'API', 'Http'],
|
|
205
|
+
storage: ['Storage', 'Database', 'Cache'],
|
|
206
|
+
ui: ['UI', 'View', 'Component'],
|
|
207
|
+
service: ['Service', 'Manager'],
|
|
208
|
+
model: ['Model', 'Entity'],
|
|
209
|
+
core: ['Core', 'Foundation', 'Utility'],
|
|
210
|
+
foundation: ['Core', 'Foundation', 'Utility'],
|
|
211
|
+
feature: ['Feature'],
|
|
212
|
+
};
|
|
213
|
+
const categories = roleCategories[mod.refinedRole] ?? [];
|
|
214
|
+
// Build a LIKE query that matches module name or related categories
|
|
215
|
+
const conditions = [];
|
|
216
|
+
const params = [];
|
|
217
|
+
// Match by module name in title or trigger
|
|
218
|
+
conditions.push('(title LIKE ? OR trigger LIKE ?)');
|
|
219
|
+
params.push(`%${moduleName}%`, `%${moduleName}%`);
|
|
220
|
+
// Match by category
|
|
221
|
+
for (const cat of categories) {
|
|
222
|
+
conditions.push('category = ?');
|
|
223
|
+
params.push(cat);
|
|
224
|
+
}
|
|
225
|
+
const whereClause = conditions.join(' OR ');
|
|
226
|
+
const rows = this.#db
|
|
227
|
+
.prepare(`SELECT id, title, trigger, kind FROM knowledge_entries
|
|
228
|
+
WHERE lifecycle IN ('active', 'staging', 'pending')
|
|
229
|
+
AND (${whereClause})
|
|
230
|
+
ORDER BY lifecycle ASC
|
|
231
|
+
LIMIT 20`)
|
|
232
|
+
.all(...params);
|
|
233
|
+
return rows.map((r) => ({
|
|
234
|
+
id: String(r.id ?? ''),
|
|
235
|
+
title: String(r.title ?? ''),
|
|
236
|
+
trigger: String(r.trigger ?? ''),
|
|
237
|
+
kind: String(r.kind ?? ''),
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Generate a structural summary for the agent */
|
|
245
|
+
static #generateModuleSummary(mod, layerName, fileGroups, recipes, neighbors) {
|
|
246
|
+
const lines = [];
|
|
247
|
+
// Identity
|
|
248
|
+
lines.push(`${mod.name} is a ${layerName} layer module (role: ${mod.refinedRole}, confidence: ${(mod.roleConfidence * 100).toFixed(0)}%).`);
|
|
249
|
+
// Structure
|
|
250
|
+
const groupDesc = fileGroups.map((g) => `${g.group}(${g.count})`).join(', ');
|
|
251
|
+
lines.push(`Contains ${mod.fileCount} files in ${fileGroups.length} groups: ${groupDesc}.`);
|
|
252
|
+
// Dependencies
|
|
253
|
+
const dependsOn = neighbors.filter((n) => n.direction === 'out').map((n) => n.name);
|
|
254
|
+
const usedBy = neighbors.filter((n) => n.direction === 'in').map((n) => n.name);
|
|
255
|
+
if (dependsOn.length > 0) {
|
|
256
|
+
lines.push(`Depends on: ${dependsOn.join(', ')}.`);
|
|
257
|
+
}
|
|
258
|
+
if (usedBy.length > 0) {
|
|
259
|
+
lines.push(`Used by: ${usedBy.join(', ')}.`);
|
|
260
|
+
}
|
|
261
|
+
if (dependsOn.length === 0 && usedBy.length === 0) {
|
|
262
|
+
lines.push('No dependency edges recorded (consider running a full bootstrap scan).');
|
|
263
|
+
}
|
|
264
|
+
// Knowledge coverage
|
|
265
|
+
lines.push(`Knowledge coverage: ${recipes.length} recipes matched, ${(mod.coverageRatio * 100).toFixed(0)}% estimated coverage.`);
|
|
266
|
+
if (recipes.length > 0) {
|
|
267
|
+
const recipeList = recipes
|
|
268
|
+
.slice(0, 5)
|
|
269
|
+
.map((r) => r.title)
|
|
270
|
+
.join('; ');
|
|
271
|
+
lines.push(`Key recipes: ${recipeList}.`);
|
|
272
|
+
}
|
|
273
|
+
if (mod.coverageRatio < 0.5) {
|
|
274
|
+
lines.push('Coverage is below 50% — consider submitting knowledge for uncovered file groups.');
|
|
275
|
+
}
|
|
276
|
+
return lines.join(' ');
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 获取知识空白区
|
|
280
|
+
*/
|
|
281
|
+
getGaps() {
|
|
282
|
+
const result = this.#getOrCompute();
|
|
283
|
+
return result.gaps;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 获取全景健康度
|
|
287
|
+
*/
|
|
288
|
+
getHealth() {
|
|
289
|
+
const result = this.#getOrCompute();
|
|
290
|
+
let totalCoupling = 0;
|
|
291
|
+
let count = 0;
|
|
292
|
+
for (const [, mod] of result.modules) {
|
|
293
|
+
totalCoupling += mod.fanIn + mod.fanOut;
|
|
294
|
+
count++;
|
|
295
|
+
}
|
|
296
|
+
const avgCoupling = count > 0 ? totalCoupling / count : 0;
|
|
297
|
+
const highPriorityGaps = result.gaps.filter((g) => g.priority === 'high').length;
|
|
298
|
+
const radar = result.healthRadar;
|
|
299
|
+
// 健康分: 100 分制 (基于维度覆盖率 + 结构健康)
|
|
300
|
+
// 维度覆盖 60 分 + 无循环 20 分 + 无高优空白 10 分 + 耦合度适中 10 分
|
|
301
|
+
let healthScore = radar.overallScore * 0.6;
|
|
302
|
+
healthScore += result.cycles.length === 0 ? 20 : Math.max(0, 20 - result.cycles.length * 5);
|
|
303
|
+
healthScore += highPriorityGaps === 0 ? 10 : Math.max(0, 10 - highPriorityGaps * 2);
|
|
304
|
+
healthScore += avgCoupling < 10 ? 10 : Math.max(0, 10 - (avgCoupling - 10));
|
|
305
|
+
healthScore = Math.round(Math.max(0, Math.min(100, healthScore)));
|
|
306
|
+
return {
|
|
307
|
+
healthRadar: radar,
|
|
308
|
+
avgCoupling,
|
|
309
|
+
cycleCount: result.cycles.length,
|
|
310
|
+
gapCount: result.gaps.length,
|
|
311
|
+
highPriorityGaps,
|
|
312
|
+
moduleCount: count,
|
|
313
|
+
healthScore,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 获取完整 PanoramaResult(内部使用或 Bootstrap 注入)
|
|
318
|
+
*/
|
|
319
|
+
getResult() {
|
|
320
|
+
return this.#getOrCompute();
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* 确保全景数据已就绪(无数据时自动扫描)
|
|
324
|
+
* MCP handler / HTTP route 应在返回数据前调用此方法
|
|
325
|
+
*/
|
|
326
|
+
async ensureData() {
|
|
327
|
+
if (!this.#scanner) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!this.#scanPromise) {
|
|
331
|
+
this.#scanPromise = this.#scanner.ensureData().then(() => {
|
|
332
|
+
this.#cache = null; // 扫描后清除缓存以触发重新计算
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
await this.#scanPromise;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 强制刷新缓存
|
|
339
|
+
*/
|
|
340
|
+
invalidate() {
|
|
341
|
+
this.#cache = null;
|
|
342
|
+
this.#scanPromise = null;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 强制重新扫描(invalidate + 重置 scanner)
|
|
346
|
+
*/
|
|
347
|
+
async rescan() {
|
|
348
|
+
this.invalidate();
|
|
349
|
+
if (this.#scanner) {
|
|
350
|
+
this.#scanner.reset();
|
|
351
|
+
await this.ensureData();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/* ─── Cache + Compute ───────────────────────────── */
|
|
355
|
+
#getOrCompute() {
|
|
356
|
+
if (this.#cache) {
|
|
357
|
+
return this.#cache;
|
|
358
|
+
}
|
|
359
|
+
const candidates = this.#moduleDiscoverer.discover();
|
|
360
|
+
this.#cache = this.#aggregator.compute(candidates);
|
|
361
|
+
return this.#cache;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PanoramaTypes — 全景服务共享类型定义
|
|
3
|
+
*
|
|
4
|
+
* @module PanoramaTypes
|
|
5
|
+
*/
|
|
6
|
+
export interface CeDbLike {
|
|
7
|
+
getDb?: () => CeDbLike;
|
|
8
|
+
transaction(fn: () => void): () => void;
|
|
9
|
+
exec(sql: string): void;
|
|
10
|
+
prepare(sql: string): {
|
|
11
|
+
run(...params: unknown[]): {
|
|
12
|
+
changes: number;
|
|
13
|
+
};
|
|
14
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
15
|
+
all(...params: unknown[]): Record<string, unknown>[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface Edge {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
weight: number;
|
|
22
|
+
relation: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PanoramaModule {
|
|
25
|
+
name: string;
|
|
26
|
+
inferredRole: string;
|
|
27
|
+
refinedRole: string;
|
|
28
|
+
roleConfidence: number;
|
|
29
|
+
layer: number;
|
|
30
|
+
fanIn: number;
|
|
31
|
+
fanOut: number;
|
|
32
|
+
files: string[];
|
|
33
|
+
fileCount: number;
|
|
34
|
+
recipeCount: number;
|
|
35
|
+
coverageRatio: number;
|
|
36
|
+
}
|
|
37
|
+
export interface LayerLevel {
|
|
38
|
+
level: number;
|
|
39
|
+
name: string;
|
|
40
|
+
modules: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface LayerViolation {
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
fromLayer: number;
|
|
46
|
+
toLayer: number;
|
|
47
|
+
relation: string;
|
|
48
|
+
}
|
|
49
|
+
export interface LayerHierarchy {
|
|
50
|
+
levels: LayerLevel[];
|
|
51
|
+
violations: LayerViolation[];
|
|
52
|
+
}
|
|
53
|
+
export interface CyclicDependency {
|
|
54
|
+
cycle: string[];
|
|
55
|
+
severity: 'error' | 'warning';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 知识维度定义 (灵感来源: ISO/IEC 25010 质量模型 + ThoughtWorks Tech Radar)
|
|
59
|
+
*
|
|
60
|
+
* 每个维度代表项目应具备知识规范的一个方向,
|
|
61
|
+
* score 反映该方向上 Recipe 的丰厚程度。
|
|
62
|
+
*/
|
|
63
|
+
export interface HealthDimension {
|
|
64
|
+
/** 维度 ID */
|
|
65
|
+
id: string;
|
|
66
|
+
/** 人类可读名称 */
|
|
67
|
+
name: string;
|
|
68
|
+
/** 维度说明 */
|
|
69
|
+
description: string;
|
|
70
|
+
/** 该维度匹配到的 recipe 数 */
|
|
71
|
+
recipeCount: number;
|
|
72
|
+
/** 得分 0-100 */
|
|
73
|
+
score: number;
|
|
74
|
+
/** 状态: strong(≥5) / adequate(2-4) / weak(1) / missing(0) */
|
|
75
|
+
status: 'strong' | 'adequate' | 'weak' | 'missing';
|
|
76
|
+
/** 雷达环级: adopt / trial / assess / hold */
|
|
77
|
+
level: 'adopt' | 'trial' | 'assess' | 'hold';
|
|
78
|
+
/** 该维度下 recipe 标题示例 (最多 3 个) */
|
|
79
|
+
topRecipes: string[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 项目知识健康雷达图
|
|
83
|
+
*/
|
|
84
|
+
export interface HealthRadar {
|
|
85
|
+
/** 各维度得分 */
|
|
86
|
+
dimensions: HealthDimension[];
|
|
87
|
+
/** 综合健康分 0-100 (加权平均) */
|
|
88
|
+
overallScore: number;
|
|
89
|
+
/** 活跃 recipe 总数 */
|
|
90
|
+
totalRecipes: number;
|
|
91
|
+
/** 已覆盖维度数 (recipeCount > 0) */
|
|
92
|
+
coveredDimensions: number;
|
|
93
|
+
/** 总维度数 */
|
|
94
|
+
totalDimensions: number;
|
|
95
|
+
/** 维度覆盖率 */
|
|
96
|
+
dimensionCoverage: number;
|
|
97
|
+
}
|
|
98
|
+
export interface KnowledgeGap {
|
|
99
|
+
/** 空白维度 ID */
|
|
100
|
+
dimension: string;
|
|
101
|
+
/** 空白维度名称 */
|
|
102
|
+
dimensionName: string;
|
|
103
|
+
/** 该维度已有 recipe 数 */
|
|
104
|
+
recipeCount: number;
|
|
105
|
+
/** 空白状态 */
|
|
106
|
+
status: 'weak' | 'missing';
|
|
107
|
+
/** 优先级 */
|
|
108
|
+
priority: 'high' | 'medium' | 'low';
|
|
109
|
+
/** 建议补充的主题方向 */
|
|
110
|
+
suggestedTopics: string[];
|
|
111
|
+
/** 受影响的模块角色 */
|
|
112
|
+
affectedRoles: string[];
|
|
113
|
+
}
|
|
114
|
+
export interface CallFlowSummary {
|
|
115
|
+
topCalledMethods: Array<{
|
|
116
|
+
id: string;
|
|
117
|
+
callCount: number;
|
|
118
|
+
}>;
|
|
119
|
+
entryPoints: string[];
|
|
120
|
+
dataProducers: string[];
|
|
121
|
+
dataConsumers: string[];
|
|
122
|
+
}
|
|
123
|
+
export interface PanoramaResult {
|
|
124
|
+
modules: Map<string, PanoramaModule>;
|
|
125
|
+
layers: LayerHierarchy;
|
|
126
|
+
cycles: CyclicDependency[];
|
|
127
|
+
gaps: KnowledgeGap[];
|
|
128
|
+
/** 多维度知识健康雷达 */
|
|
129
|
+
healthRadar: HealthRadar;
|
|
130
|
+
callFlowSummary: CallFlowSummary;
|
|
131
|
+
/** 项目级活跃 recipe 总数(不限模块匹配) */
|
|
132
|
+
projectRecipeCount: number;
|
|
133
|
+
computedAt: number;
|
|
134
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoleRefiner — 四重信号融合角色精化
|
|
3
|
+
*
|
|
4
|
+
* 将 TargetClassifier 的正则推断 (~65% 准确率) 提升到 ≥90%,
|
|
5
|
+
* 通过融合 AST 结构、CallGraph 行为、DataFlow 数据流、EntityGraph 拓扑四重信号。
|
|
6
|
+
*
|
|
7
|
+
* 信号权重:
|
|
8
|
+
* AST 结构 0.30 继承链/协议/import/后缀
|
|
9
|
+
* CallGraph 行为 0.30 被调用分析/扇入扇出比/调用类型
|
|
10
|
+
* DataFlow 数据流 0.15 源汇分析/转换检测
|
|
11
|
+
* EntityGraph 拓扑 0.10 入度分析/模式检测
|
|
12
|
+
* 正则基线 0.15 TargetClassifier 结果
|
|
13
|
+
*
|
|
14
|
+
* @module RoleRefiner
|
|
15
|
+
*/
|
|
16
|
+
import type { CeDbLike } from './PanoramaTypes.js';
|
|
17
|
+
export type ModuleRole = 'core' | 'service' | 'ui' | 'networking' | 'storage' | 'test' | 'app' | 'routing' | 'utility' | 'model' | 'auth' | 'config' | 'feature';
|
|
18
|
+
export interface RoleSignal {
|
|
19
|
+
role: ModuleRole;
|
|
20
|
+
confidence: number;
|
|
21
|
+
weight: number;
|
|
22
|
+
source: string;
|
|
23
|
+
}
|
|
24
|
+
export type RoleResolution = 'clear' | 'uncertain' | 'fallback';
|
|
25
|
+
export interface RefinedRole {
|
|
26
|
+
refinedRole: ModuleRole;
|
|
27
|
+
confidence: number;
|
|
28
|
+
resolution: RoleResolution;
|
|
29
|
+
alternatives?: Array<[string, number]>;
|
|
30
|
+
signals: RoleSignal[];
|
|
31
|
+
}
|
|
32
|
+
export interface ModuleCandidate {
|
|
33
|
+
name: string;
|
|
34
|
+
inferredRole: ModuleRole;
|
|
35
|
+
files: string[];
|
|
36
|
+
}
|
|
37
|
+
export declare class RoleRefiner {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(db: CeDbLike, projectRoot: string);
|
|
40
|
+
/**
|
|
41
|
+
* 精化单个模块的角色
|
|
42
|
+
*/
|
|
43
|
+
refineRole(module: ModuleCandidate): RefinedRole;
|
|
44
|
+
/**
|
|
45
|
+
* 批量精化所有模块
|
|
46
|
+
*/
|
|
47
|
+
refineAll(modules: ModuleCandidate[]): Map<string, RefinedRole>;
|
|
48
|
+
}
|