autosnippet 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -324
- package/bin/api-server.js +1 -1
- package/bin/cli.js +204 -244
- package/bin/mcp-server.js +5 -3
- package/config/knowledge-base.config.js +132 -132
- package/dashboard/dist/assets/{icons-CEfgGaZi.js → icons-Cdq22n2i.js} +95 -100
- package/dashboard/dist/assets/index-ClkyPkDX.js +133 -0
- package/dashboard/dist/assets/index-t4QrJwv1.css +1 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +8 -8
- package/lib/cli/AiScanService.js +86 -40
- package/lib/cli/KnowledgeSyncService.js +113 -74
- package/lib/cli/SetupService.js +439 -277
- package/lib/cli/UpgradeService.js +63 -100
- package/lib/core/AstAnalyzer.js +276 -597
- package/lib/core/ast/ProjectGraph.js +101 -40
- package/lib/core/ast/ensure-grammars.js +232 -0
- package/lib/core/ast/index.js +115 -0
- package/lib/core/ast/lang-dart.js +661 -0
- package/lib/core/ast/lang-go.js +530 -0
- package/lib/core/ast/lang-java.js +435 -0
- package/lib/core/ast/lang-javascript.js +272 -0
- package/lib/core/ast/lang-kotlin.js +423 -0
- package/lib/core/ast/lang-objc.js +388 -0
- package/lib/core/ast/lang-python.js +371 -0
- package/lib/core/ast/lang-swift.js +337 -0
- package/lib/core/ast/lang-typescript.js +503 -0
- package/lib/core/capability/CapabilityProbe.js +18 -9
- package/lib/core/constitution/Constitution.js +2 -3
- package/lib/core/constitution/ConstitutionValidator.js +65 -24
- package/lib/core/discovery/DartDiscoverer.js +534 -0
- package/lib/core/discovery/DiscovererRegistry.js +83 -0
- package/lib/core/discovery/GenericDiscoverer.js +225 -0
- package/lib/core/discovery/GoDiscoverer.js +541 -0
- package/lib/core/discovery/JvmDiscoverer.js +506 -0
- package/lib/core/discovery/NodeDiscoverer.js +466 -0
- package/lib/core/discovery/ProjectDiscoverer.js +93 -0
- package/lib/core/discovery/PythonDiscoverer.js +338 -0
- package/lib/core/discovery/SpmDiscoverer.js +5 -0
- package/lib/core/discovery/index.js +53 -0
- package/lib/core/enhancement/EnhancementPack.js +71 -0
- package/lib/core/enhancement/EnhancementRegistry.js +47 -0
- package/lib/core/enhancement/android-enhancement.js +102 -0
- package/lib/core/enhancement/django-enhancement.js +70 -0
- package/lib/core/enhancement/fastapi-enhancement.js +63 -0
- package/lib/core/enhancement/go-grpc-enhancement.js +152 -0
- package/lib/core/enhancement/go-web-enhancement.js +201 -0
- package/lib/core/enhancement/index.js +65 -0
- package/lib/core/enhancement/node-server-enhancement.js +88 -0
- package/lib/core/enhancement/react-enhancement.js +86 -0
- package/lib/core/enhancement/spring-enhancement.js +112 -0
- package/lib/core/enhancement/vue-enhancement.js +96 -0
- package/lib/core/gateway/Gateway.js +8 -9
- package/lib/core/gateway/GatewayActionRegistry.js +1 -1
- package/lib/core/permission/PermissionManager.js +12 -8
- package/lib/domain/index.js +13 -9
- package/lib/domain/knowledge/KnowledgeEntry.js +111 -101
- package/lib/domain/knowledge/KnowledgeRepository.js +0 -1
- package/lib/domain/knowledge/Lifecycle.js +22 -22
- package/lib/domain/knowledge/index.js +9 -12
- package/lib/domain/knowledge/values/Constraints.js +31 -21
- package/lib/domain/knowledge/values/Content.js +21 -13
- package/lib/domain/knowledge/values/Quality.js +31 -18
- package/lib/domain/knowledge/values/Reasoning.js +20 -12
- package/lib/domain/knowledge/values/Relations.js +37 -25
- package/lib/domain/knowledge/values/Stats.js +18 -12
- package/lib/domain/knowledge/values/index.js +4 -3
- package/lib/domain/snippet/Snippet.js +35 -10
- package/lib/external/ai/AiFactory.js +48 -16
- package/lib/external/ai/AiProvider.js +184 -90
- package/lib/external/ai/providers/ClaudeProvider.js +25 -12
- package/lib/external/ai/providers/GoogleGeminiProvider.js +59 -30
- package/lib/external/ai/providers/MockProvider.js +9 -3
- package/lib/external/ai/providers/OpenAiProvider.js +51 -29
- package/lib/external/mcp/McpServer.js +66 -36
- package/lib/external/mcp/errorHandler.js +23 -11
- package/lib/external/mcp/handlers/LanguageExtensions.js +138 -53
- package/lib/external/mcp/handlers/TargetClassifier.js +52 -16
- package/lib/external/mcp/handlers/bootstrap/pipeline/BootstrapSnapshot.js +81 -20
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +71 -42
- package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +9 -17
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +14 -9
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +15 -7
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +352 -153
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +52 -12
- package/lib/external/mcp/handlers/bootstrap/skills.js +143 -39
- package/lib/external/mcp/handlers/bootstrap.js +691 -168
- package/lib/external/mcp/handlers/browse.js +66 -22
- package/lib/external/mcp/handlers/candidate.js +118 -35
- package/lib/external/mcp/handlers/consolidated.js +49 -17
- package/lib/external/mcp/handlers/guard.js +104 -39
- package/lib/external/mcp/handlers/knowledge.js +60 -36
- package/lib/external/mcp/handlers/search.js +43 -14
- package/lib/external/mcp/handlers/skill.js +120 -45
- package/lib/external/mcp/handlers/structure.js +240 -86
- package/lib/external/mcp/handlers/system.js +42 -12
- package/lib/external/mcp/handlers/wiki.js +58 -33
- package/lib/external/mcp/tools.js +306 -123
- package/lib/http/HttpServer.js +72 -47
- package/lib/http/middleware/RateLimiter.js +5 -3
- package/lib/http/middleware/errorHandler.js +6 -1
- package/lib/http/middleware/requestLogger.js +14 -3
- package/lib/http/middleware/roleResolver.js +30 -23
- package/lib/http/routes/ai.js +387 -265
- package/lib/http/routes/auth.js +81 -61
- package/lib/http/routes/candidates.js +430 -320
- package/lib/http/routes/commands.js +289 -189
- package/lib/http/routes/extract.js +158 -125
- package/lib/http/routes/guardRules.js +309 -217
- package/lib/http/routes/knowledge.js +213 -154
- package/lib/http/routes/modules.js +578 -0
- package/lib/http/routes/monitoring.js +6 -6
- package/lib/http/routes/recipes.js +104 -93
- package/lib/http/routes/search.js +361 -305
- package/lib/http/routes/skills.js +145 -98
- package/lib/http/routes/snippets.js +42 -30
- package/lib/http/routes/spm.js +3 -405
- package/lib/http/routes/violations.js +113 -93
- package/lib/http/routes/wiki.js +211 -170
- package/lib/http/utils/routeHelpers.js +3 -1
- package/lib/http/utils/sse-sessions.js +16 -6
- package/lib/http/utils/sse.js +15 -5
- package/lib/infrastructure/audit/AuditLogger.js +5 -2
- package/lib/infrastructure/audit/AuditStore.js +10 -7
- package/lib/infrastructure/cache/CacheService.js +3 -1
- package/lib/infrastructure/cache/GraphCache.js +8 -4
- package/lib/infrastructure/cache/UnifiedCacheAdapter.js +1 -1
- package/lib/infrastructure/config/ConfigLoader.js +9 -5
- package/lib/infrastructure/config/Defaults.js +30 -10
- package/lib/infrastructure/config/Paths.js +28 -8
- package/lib/infrastructure/config/TriggerSymbol.js +22 -10
- package/lib/infrastructure/database/DatabaseConnection.js +15 -10
- package/lib/infrastructure/database/migrations/001_initial_schema.js +0 -1
- package/lib/infrastructure/external/ClipboardManager.js +6 -2
- package/lib/infrastructure/external/NativeUi.js +50 -43
- package/lib/infrastructure/external/OpenBrowser.js +14 -17
- package/lib/infrastructure/external/XcodeAutomation.js +14 -258
- package/lib/infrastructure/logging/Logger.js +46 -30
- package/lib/infrastructure/monitoring/ErrorTracker.js +7 -5
- package/lib/infrastructure/monitoring/PerformanceMonitor.js +12 -4
- package/lib/infrastructure/paths/HeaderResolver.js +25 -9
- package/lib/infrastructure/paths/PathFinder.js +34 -12
- package/lib/infrastructure/plugin/PluginManager.js +26 -8
- package/lib/infrastructure/realtime/RealtimeService.js +2 -2
- package/lib/infrastructure/vector/Chunker.js +22 -7
- package/lib/infrastructure/vector/IndexingPipeline.js +46 -22
- package/lib/infrastructure/vector/JsonVectorAdapter.js +90 -53
- package/lib/infrastructure/vector/VectorStore.js +28 -10
- package/lib/injection/ServiceContainer.js +247 -93
- package/lib/platform/ios/index.js +63 -0
- package/lib/platform/ios/routes/spm.js +437 -0
- package/lib/platform/ios/snippet/PlaceholderConverter.js +55 -0
- package/lib/platform/ios/snippet/XcodeCodec.js +112 -0
- package/lib/{service → platform/ios}/spm/DependencyGraph.js +41 -17
- package/lib/{service → platform/ios}/spm/PackageSwiftParser.js +41 -14
- package/lib/{service → platform/ios}/spm/PolicyEngine.js +9 -4
- package/lib/platform/ios/spm/SpmDiscoverer.js +122 -0
- package/lib/{service → platform/ios}/spm/SpmService.js +385 -127
- package/lib/{service/automation → platform/ios/xcode}/SaveEventFilter.js +8 -7
- package/lib/platform/ios/xcode/XcodeAutomation.js +350 -0
- package/lib/{service/automation → platform/ios/xcode}/XcodeIntegration.js +325 -145
- package/lib/repository/base/BaseRepository.js +7 -9
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +98 -75
- package/lib/repository/token/TokenUsageStore.js +4 -2
- package/lib/service/automation/ActionPipeline.js +1 -1
- package/lib/service/automation/AutomationOrchestrator.js +8 -4
- package/lib/service/automation/ContextCollector.js +7 -5
- package/lib/service/automation/DirectiveDetector.js +23 -16
- package/lib/service/automation/FileWatcher.js +112 -56
- package/lib/service/automation/TriggerResolver.js +6 -4
- package/lib/service/automation/handlers/AlinkHandler.js +24 -12
- package/lib/service/automation/handlers/CreateHandler.js +19 -20
- package/lib/service/automation/handlers/DraftHandler.js +14 -8
- package/lib/service/automation/handlers/GuardHandler.js +93 -63
- package/lib/service/automation/handlers/HeaderHandler.js +1 -6
- package/lib/service/automation/handlers/SearchHandler.js +155 -88
- package/lib/service/bootstrap/BootstrapTaskManager.js +77 -35
- package/lib/service/candidate/SimilarityService.js +25 -9
- package/lib/service/chat/AnalystAgent.js +50 -24
- package/lib/service/chat/CandidateGuardrail.js +143 -17
- package/lib/service/chat/ChatAgent.js +759 -243
- package/lib/service/chat/ContextWindow.js +116 -71
- package/lib/service/chat/ConversationStore.js +77 -36
- package/lib/service/chat/EpisodicConsolidator.js +47 -23
- package/lib/service/chat/HandoffProtocol.js +98 -22
- package/lib/service/chat/Memory.js +34 -14
- package/lib/service/chat/ProducerAgent.js +40 -20
- package/lib/service/chat/ProjectSemanticMemory.js +109 -78
- package/lib/service/chat/ReasoningLayer.js +148 -70
- package/lib/service/chat/ReasoningTrace.js +44 -32
- package/lib/service/chat/TaskPipeline.js +39 -19
- package/lib/service/chat/ToolRegistry.js +48 -29
- package/lib/service/chat/WorkingMemory.js +44 -18
- package/lib/service/chat/tools.js +1096 -494
- package/lib/service/context/RecipeExtractor.js +132 -51
- package/lib/service/cursor/CursorDeliveryPipeline.js +82 -37
- package/lib/service/cursor/KnowledgeCompressor.js +25 -22
- package/lib/service/cursor/RulesGenerator.js +13 -7
- package/lib/service/cursor/SkillsSyncer.js +77 -27
- package/lib/service/cursor/TokenBudget.js +2 -2
- package/lib/service/cursor/TopicClassifier.js +54 -20
- package/lib/service/guard/ComplianceReporter.js +55 -43
- package/lib/service/guard/ExclusionManager.js +67 -29
- package/lib/service/guard/GuardCheckEngine.js +381 -86
- package/lib/service/guard/GuardFeedbackLoop.js +22 -10
- package/lib/service/guard/GuardService.js +29 -19
- package/lib/service/guard/RuleLearner.js +55 -23
- package/lib/service/guard/SourceFileCollector.js +27 -20
- package/lib/service/guard/ViolationsStore.js +43 -38
- package/lib/service/knowledge/CodeEntityGraph.js +147 -82
- package/lib/service/knowledge/ConfidenceRouter.js +12 -10
- package/lib/service/knowledge/KnowledgeFileWriter.js +147 -56
- package/lib/service/knowledge/KnowledgeGraphService.js +81 -34
- package/lib/service/knowledge/KnowledgeService.js +222 -112
- package/lib/service/module/ModuleService.js +969 -0
- package/lib/service/quality/FeedbackCollector.js +27 -15
- package/lib/service/quality/QualityScorer.js +78 -24
- package/lib/service/recipe/RecipeCandidateValidator.js +110 -44
- package/lib/service/recipe/RecipeParser.js +78 -45
- package/lib/service/search/CoarseRanker.js +43 -28
- package/lib/service/search/CrossEncoderReranker.js +32 -21
- package/lib/service/search/InvertedIndex.js +21 -7
- package/lib/service/search/MultiSignalRanker.js +90 -28
- package/lib/service/search/RetrievalFunnel.js +45 -24
- package/lib/service/search/SearchEngine.js +255 -103
- package/lib/service/skills/EventAggregator.js +32 -15
- package/lib/service/skills/SignalCollector.js +140 -64
- package/lib/service/skills/SkillAdvisor.js +79 -42
- package/lib/service/skills/SkillHooks.js +16 -14
- package/lib/service/snippet/PlaceholderConverter.js +5 -0
- package/lib/service/snippet/SnippetFactory.js +116 -99
- package/lib/service/snippet/SnippetInstaller.js +234 -62
- package/lib/service/snippet/codecs/SnippetCodec.js +67 -0
- package/lib/service/snippet/codecs/VSCodeCodec.js +102 -0
- package/lib/service/snippet/codecs/XcodeCodec.js +5 -0
- package/lib/service/wiki/WikiGenerator.js +637 -263
- package/lib/shared/DimensionCopyRegistry.js +472 -0
- package/lib/shared/LanguageService.js +399 -0
- package/lib/shared/PathGuard.js +45 -28
- package/lib/shared/RecipeReadinessChecker.js +72 -12
- package/lib/shared/constants.js +41 -41
- package/lib/shared/errors/BaseError.js +2 -2
- package/lib/shared/errors/index.js +4 -4
- package/lib/shared/similarity.js +25 -8
- package/lib/shared/token-utils.js +6 -2
- package/lib/shared/utils/common.js +12 -4
- package/package.json +49 -13
- package/scripts/bench-real-projects.mjs +256 -0
- package/scripts/build-native-ui.js +30 -30
- package/scripts/clear-old-vector-index.js +5 -35
- package/scripts/clear-vector-cache.js +7 -37
- package/scripts/collect-test-project-stats.mjs +160 -0
- package/scripts/diagnose-mcp.js +41 -32
- package/scripts/ensure-parse-package.js +6 -9
- package/scripts/generate-recipe-drafts.js +116 -77
- package/scripts/init-db.js +3 -20
- package/scripts/init-snippets.js +305 -0
- package/scripts/init-vector-db.js +173 -170
- package/scripts/install-cursor-skill.js +148 -104
- package/scripts/install-full.js +8 -21
- package/scripts/install-vscode-copilot.js +146 -145
- package/scripts/migrate-md-to-knowledge.mjs +139 -151
- package/scripts/postinstall-safe.js +5 -17
- package/scripts/recipe-audit.js +106 -82
- package/scripts/release.js +283 -323
- package/scripts/setup-mcp-config.js +60 -52
- package/scripts/verify-context-api.js +20 -20
- package/skills/autosnippet-analysis/SKILL.md +10 -6
- package/skills/autosnippet-candidates/SKILL.md +27 -26
- package/skills/autosnippet-coldstart/SKILL.md +555 -38
- package/skills/autosnippet-concepts/SKILL.md +349 -337
- package/skills/autosnippet-create/SKILL.md +5 -5
- package/skills/autosnippet-reference-dart/SKILL.md +543 -0
- package/skills/autosnippet-reference-go/SKILL.md +539 -0
- package/skills/autosnippet-reference-java/SKILL.md +534 -0
- package/skills/autosnippet-reference-jsts/SKILL.md +41 -9
- package/skills/autosnippet-reference-kotlin/SKILL.md +526 -0
- package/skills/autosnippet-reference-objc/SKILL.md +29 -6
- package/skills/autosnippet-reference-python/SKILL.md +800 -0
- package/skills/autosnippet-reference-swift/SKILL.md +70 -14
- package/skills/autosnippet-structure/SKILL.md +4 -4
- package/templates/cursor-rules/autosnippet-conventions.mdc +2 -2
- package/templates/recipes-setup/README.md +2 -2
- package/templates/recipes-setup/_template.md +1 -1
- package/dashboard/dist/assets/index-Bun3ld_J.css +0 -1
- package/dashboard/dist/assets/index-_Sk_Dmg3.js +0 -143
- package/resources/asd-entry/main.swift +0 -159
- package/scripts/build-asd-entry.js +0 -51
- package/scripts/init-xcode-snippets.js +0 -311
- package/template.json +0 -39
|
@@ -25,21 +25,44 @@ const SKILL_NAME_MAP = {
|
|
|
25
25
|
'project-objc-deep-scan': 'autosnippet-objc-deep-scan',
|
|
26
26
|
'project-category-scan': 'autosnippet-category-scan',
|
|
27
27
|
'project-best-practice': 'autosnippet-best-practice',
|
|
28
|
+
// 新语言维度
|
|
29
|
+
'project-module-exports': 'autosnippet-module-exports',
|
|
30
|
+
'project-framework-conventions': 'autosnippet-framework-conventions',
|
|
31
|
+
'project-python-structure': 'autosnippet-python-structure',
|
|
32
|
+
'project-jvm-annotations': 'autosnippet-jvm-annotations',
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
36
|
* 用途描述模板(英文,Cursor 优先)
|
|
32
37
|
*/
|
|
33
38
|
const SKILL_DESC_MAP = {
|
|
34
|
-
'autosnippet-architecture':
|
|
35
|
-
|
|
36
|
-
'autosnippet-
|
|
37
|
-
|
|
38
|
-
'autosnippet-
|
|
39
|
-
|
|
40
|
-
'autosnippet-
|
|
41
|
-
|
|
42
|
-
'autosnippet-
|
|
39
|
+
'autosnippet-architecture':
|
|
40
|
+
'Architecture patterns, module boundaries, and dependency rules for {project}. Use when creating new modules, reviewing architecture, or understanding dependencies.',
|
|
41
|
+
'autosnippet-code-standard':
|
|
42
|
+
'Coding standards and style conventions for {project}. Use when writing new code, reviewing formatting, or enforcing naming conventions.',
|
|
43
|
+
'autosnippet-profile':
|
|
44
|
+
'Project overview and profile for {project}. Use when needing background on the project, its tech stack, or structure.',
|
|
45
|
+
'autosnippet-guidelines':
|
|
46
|
+
'Agent interaction guidelines for {project}. Use when understanding how to work with this specific project.',
|
|
47
|
+
'autosnippet-data-flow':
|
|
48
|
+
'Event and data flow patterns for {project}. Use when working with events, state management, or data pipelines.',
|
|
49
|
+
'autosnippet-code-pattern':
|
|
50
|
+
'Common code patterns and idioms for {project}. Use when implementing features following project conventions.',
|
|
51
|
+
'autosnippet-objc-deep-scan':
|
|
52
|
+
'Objective-C deep scan results for {project}. Use when working with Objective-C code, method swizzling, or runtime features.',
|
|
53
|
+
'autosnippet-category-scan':
|
|
54
|
+
'Category and extension analysis for {project}. Use when working with categories or finding existing utility methods.',
|
|
55
|
+
'autosnippet-best-practice':
|
|
56
|
+
'Best practices and proven patterns for {project}. Use when making design decisions or code review.',
|
|
57
|
+
// 新语言维度
|
|
58
|
+
'autosnippet-module-exports':
|
|
59
|
+
'Module export structure, barrel exports, and public API surface for {project}. Use when working with imports/exports or module boundaries.',
|
|
60
|
+
'autosnippet-framework-conventions':
|
|
61
|
+
'Framework-specific conventions (component structure, routing, state management) for {project}. Use when following framework patterns.',
|
|
62
|
+
'autosnippet-python-structure':
|
|
63
|
+
'Python package structure, __init__.py exports, import patterns and type hint coverage for {project}. Use when working with Python modules.',
|
|
64
|
+
'autosnippet-jvm-annotations':
|
|
65
|
+
'Annotation patterns (DI, ORM, API, custom) and meta-programming for {project}. Use when working with Spring, Jakarta, or framework annotations.',
|
|
43
66
|
};
|
|
44
67
|
|
|
45
68
|
export class SkillsSyncer {
|
|
@@ -69,9 +92,10 @@ export class SkillsSyncer {
|
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
// 扫描源目录
|
|
72
|
-
const skillDirs = fs
|
|
73
|
-
.
|
|
74
|
-
.
|
|
95
|
+
const skillDirs = fs
|
|
96
|
+
.readdirSync(this.sourceDir, { withFileTypes: true })
|
|
97
|
+
.filter((d) => d.isDirectory())
|
|
98
|
+
.map((d) => d.name);
|
|
75
99
|
|
|
76
100
|
for (const dirName of skillDirs) {
|
|
77
101
|
try {
|
|
@@ -81,7 +105,8 @@ export class SkillsSyncer {
|
|
|
81
105
|
continue;
|
|
82
106
|
}
|
|
83
107
|
|
|
84
|
-
const targetName =
|
|
108
|
+
const targetName =
|
|
109
|
+
SKILL_NAME_MAP[dirName] || `autosnippet-${dirName.replace(/^project-/, '')}`;
|
|
85
110
|
const targetSkillDir = path.join(this.targetDir, targetName);
|
|
86
111
|
|
|
87
112
|
// 创建目标目录
|
|
@@ -118,7 +143,9 @@ export class SkillsSyncer {
|
|
|
118
143
|
const body = bodyMatch ? bodyMatch[1].trim() : source.trim();
|
|
119
144
|
|
|
120
145
|
// 获取描述
|
|
121
|
-
const descTemplate =
|
|
146
|
+
const descTemplate =
|
|
147
|
+
SKILL_DESC_MAP[targetName] ||
|
|
148
|
+
`Knowledge and patterns from {project}. Use when working with ${sourceDirName.replace(/^project-/, '')} related code.`;
|
|
122
149
|
const description = descTemplate.replace(/\{project\}/g, this.projectName);
|
|
123
150
|
|
|
124
151
|
// 构建 Cursor 标准格式
|
|
@@ -147,7 +174,7 @@ export class SkillsSyncer {
|
|
|
147
174
|
'',
|
|
148
175
|
'See `references/RECIPES.md` for related recipe summaries.',
|
|
149
176
|
];
|
|
150
|
-
return lines.join('\n')
|
|
177
|
+
return `${lines.join('\n')}\n`;
|
|
151
178
|
}
|
|
152
179
|
|
|
153
180
|
/**
|
|
@@ -168,7 +195,9 @@ export class SkillsSyncer {
|
|
|
168
195
|
{ page: 1, pageSize: 50 }
|
|
169
196
|
);
|
|
170
197
|
recipes = result?.items || result?.data || [];
|
|
171
|
-
if (Array.isArray(result))
|
|
198
|
+
if (Array.isArray(result)) {
|
|
199
|
+
recipes = result;
|
|
200
|
+
}
|
|
172
201
|
} catch {
|
|
173
202
|
// 忽略查询错误
|
|
174
203
|
}
|
|
@@ -176,10 +205,7 @@ export class SkillsSyncer {
|
|
|
176
205
|
|
|
177
206
|
// 生成 RECIPES.md
|
|
178
207
|
const dimensionLabel = sourceDirName.replace(/^project-/, '').replace(/-/g, ' ');
|
|
179
|
-
const lines = [
|
|
180
|
-
`# ${this._capitalizeWords(dimensionLabel)} Recipes`,
|
|
181
|
-
'',
|
|
182
|
-
];
|
|
208
|
+
const lines = [`# ${this._capitalizeWords(dimensionLabel)} Recipes`, ''];
|
|
183
209
|
|
|
184
210
|
if (recipes.length > 0) {
|
|
185
211
|
lines.push('| Title | Trigger | Summary |');
|
|
@@ -187,7 +213,9 @@ export class SkillsSyncer {
|
|
|
187
213
|
for (const entry of recipes.slice(0, 20)) {
|
|
188
214
|
const title = (entry.title || '').replace(/\|/g, '/');
|
|
189
215
|
const trigger = entry.trigger || '-';
|
|
190
|
-
const summary = (entry.summaryCn || entry.description || '')
|
|
216
|
+
const summary = (entry.summaryCn || entry.description || '')
|
|
217
|
+
.replace(/\|/g, '/')
|
|
218
|
+
.slice(0, 80);
|
|
191
219
|
lines.push(`| ${title} | ${trigger} | ${summary} |`);
|
|
192
220
|
}
|
|
193
221
|
} else {
|
|
@@ -197,7 +225,7 @@ export class SkillsSyncer {
|
|
|
197
225
|
lines.push('');
|
|
198
226
|
lines.push(`For full content, use: \`autosnippet_search("${dimensionLabel}")\``);
|
|
199
227
|
|
|
200
|
-
fs.writeFileSync(path.join(refsDir, 'RECIPES.md'), lines.join('\n')
|
|
228
|
+
fs.writeFileSync(path.join(refsDir, 'RECIPES.md'), `${lines.join('\n')}\n`, 'utf8');
|
|
201
229
|
}
|
|
202
230
|
|
|
203
231
|
/**
|
|
@@ -250,18 +278,40 @@ export class SkillsSyncer {
|
|
|
250
278
|
'- Code review and quality improvements',
|
|
251
279
|
'- Choosing between implementation approaches',
|
|
252
280
|
],
|
|
281
|
+
'project-module-exports': [
|
|
282
|
+
'- Working with module imports and exports',
|
|
283
|
+
'- Understanding the public API surface',
|
|
284
|
+
'- Refactoring barrel exports or re-export chains',
|
|
285
|
+
],
|
|
286
|
+
'project-framework-conventions': [
|
|
287
|
+
'- Following framework-specific patterns (React/Vue/Angular)',
|
|
288
|
+
'- Organizing components and routes',
|
|
289
|
+
'- Implementing state management patterns',
|
|
290
|
+
],
|
|
291
|
+
'project-python-structure': [
|
|
292
|
+
'- Working with Python modules and packages',
|
|
293
|
+
'- Understanding import patterns and __init__.py exports',
|
|
294
|
+
'- Adding type hints or decorators',
|
|
295
|
+
],
|
|
296
|
+
'project-jvm-annotations': [
|
|
297
|
+
'- Working with dependency injection annotations',
|
|
298
|
+
'- Configuring ORM entities and API endpoints',
|
|
299
|
+
'- Using or creating custom annotations',
|
|
300
|
+
],
|
|
253
301
|
};
|
|
254
|
-
return
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
302
|
+
return (
|
|
303
|
+
casesMap[sourceDirName] || [
|
|
304
|
+
'- Working with code related to this dimension',
|
|
305
|
+
'- Need guidance on project-specific patterns',
|
|
306
|
+
]
|
|
307
|
+
);
|
|
258
308
|
}
|
|
259
309
|
|
|
260
310
|
/**
|
|
261
311
|
* @private
|
|
262
312
|
*/
|
|
263
313
|
_capitalizeWords(str) {
|
|
264
|
-
return str.replace(/\b\w/g, c => c.toUpperCase());
|
|
314
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
265
315
|
}
|
|
266
316
|
}
|
|
267
317
|
|
|
@@ -10,10 +10,10 @@ export { estimateTokens };
|
|
|
10
10
|
|
|
11
11
|
/** 默认预算配置 */
|
|
12
12
|
export const BUDGET = {
|
|
13
|
-
CHANNEL_A_MAX: 400,
|
|
13
|
+
CHANNEL_A_MAX: 400, // Always-On Rules 最大 token
|
|
14
14
|
CHANNEL_B_MAX_PER_FILE: 750, // Smart Rules 每个主题文件最大 token
|
|
15
15
|
CHANNEL_B_MAX_PATTERNS: 5, // Smart Rules 每个主题最多模式数
|
|
16
|
-
CHANNEL_A_MAX_RULES: 8,
|
|
16
|
+
CHANNEL_A_MAX_RULES: 8, // Always-On Rules 最多规则数
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -14,23 +14,28 @@
|
|
|
14
14
|
const TOPIC_MAP = {
|
|
15
15
|
networking: {
|
|
16
16
|
dimensions: ['event-and-data-flow'],
|
|
17
|
-
descriptionKeywords:
|
|
17
|
+
descriptionKeywords:
|
|
18
|
+
'network, HTTP, API, request, response, URL, fetch, socket, REST, download, upload, error handling, retry, timeout',
|
|
18
19
|
},
|
|
19
20
|
ui: {
|
|
20
21
|
dimensions: ['code-pattern'],
|
|
21
|
-
descriptionKeywords:
|
|
22
|
+
descriptionKeywords:
|
|
23
|
+
'view, controller, UI, layout, animation, cell, table, collection, button, scroll, navigation, auto layout, gesture, storyboard',
|
|
22
24
|
},
|
|
23
25
|
data: {
|
|
24
26
|
dimensions: ['code-pattern', 'architecture'],
|
|
25
|
-
descriptionKeywords:
|
|
27
|
+
descriptionKeywords:
|
|
28
|
+
'model, storage, database, cache, CoreData, Realm, SQLite, keychain, UserDefaults, JSON, parsing, serialization, persistence',
|
|
26
29
|
},
|
|
27
30
|
architecture: {
|
|
28
31
|
dimensions: ['architecture', 'best-practice'],
|
|
29
|
-
descriptionKeywords:
|
|
32
|
+
descriptionKeywords:
|
|
33
|
+
'singleton, delegate, factory, observer, protocol, manager, service, dependency injection, module, MVVM, MVC, coordinator, router, design pattern',
|
|
30
34
|
},
|
|
31
35
|
conventions: {
|
|
32
36
|
dimensions: ['code-standard'],
|
|
33
|
-
descriptionKeywords:
|
|
37
|
+
descriptionKeywords:
|
|
38
|
+
'naming, format, style, import, header, prefix, convention, documentation, file organization, constants, enum, typedef, pragma mark',
|
|
34
39
|
},
|
|
35
40
|
};
|
|
36
41
|
|
|
@@ -54,7 +59,9 @@ export class TopicClassifier {
|
|
|
54
59
|
for (const entry of entries) {
|
|
55
60
|
const topic = this._classifyEntry(entry);
|
|
56
61
|
if (topic) {
|
|
57
|
-
if (!grouped[topic])
|
|
62
|
+
if (!grouped[topic]) {
|
|
63
|
+
grouped[topic] = [];
|
|
64
|
+
}
|
|
58
65
|
grouped[topic].push(entry);
|
|
59
66
|
} else {
|
|
60
67
|
unmatched.push(entry);
|
|
@@ -79,13 +86,13 @@ export class TopicClassifier {
|
|
|
79
86
|
const topicDef = TOPIC_MAP[topic];
|
|
80
87
|
const baseKeywords = topicDef
|
|
81
88
|
? topicDef.descriptionKeywords
|
|
82
|
-
: entries
|
|
89
|
+
: entries
|
|
90
|
+
.map((e) => e.title || '')
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join(', ');
|
|
83
93
|
|
|
84
94
|
// 从 entries 提取额外关键词
|
|
85
|
-
const entryKeywords = entries
|
|
86
|
-
.map(e => this._extractKeywords(e))
|
|
87
|
-
.flat()
|
|
88
|
-
.filter(Boolean);
|
|
95
|
+
const entryKeywords = entries.flatMap((e) => this._extractKeywords(e)).filter(Boolean);
|
|
89
96
|
const unique = [...new Set(entryKeywords)].slice(0, 10);
|
|
90
97
|
const extra = unique.length > 0 ? `, ${unique.join(', ')}` : '';
|
|
91
98
|
|
|
@@ -97,7 +104,7 @@ export class TopicClassifier {
|
|
|
97
104
|
* @private
|
|
98
105
|
*/
|
|
99
106
|
_classifyEntry(entry) {
|
|
100
|
-
return entry.topicHint || null;
|
|
107
|
+
return entry.topicHint || null; // AI 没给 → null → 归入 general
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
/**
|
|
@@ -105,12 +112,10 @@ export class TopicClassifier {
|
|
|
105
112
|
* @private
|
|
106
113
|
*/
|
|
107
114
|
_extractKeywords(entry) {
|
|
108
|
-
const text =
|
|
115
|
+
const text = `${entry.title || ''} ${entry.description || ''}`;
|
|
109
116
|
// 提取英文关键词(3+ 字母)
|
|
110
117
|
const words = text.match(/[a-zA-Z]{3,}/g) || [];
|
|
111
|
-
const filtered = words
|
|
112
|
-
.map(w => w.toLowerCase())
|
|
113
|
-
.filter(w => !STOP_WORDS.has(w));
|
|
118
|
+
const filtered = words.map((w) => w.toLowerCase()).filter((w) => !STOP_WORDS.has(w));
|
|
114
119
|
return [...new Set(filtered)].slice(0, 5);
|
|
115
120
|
}
|
|
116
121
|
|
|
@@ -132,10 +137,39 @@ export class TopicClassifier {
|
|
|
132
137
|
|
|
133
138
|
/** @type {Set<string>} */
|
|
134
139
|
const STOP_WORDS = new Set([
|
|
135
|
-
'the',
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
140
|
+
'the',
|
|
141
|
+
'and',
|
|
142
|
+
'for',
|
|
143
|
+
'this',
|
|
144
|
+
'that',
|
|
145
|
+
'with',
|
|
146
|
+
'from',
|
|
147
|
+
'use',
|
|
148
|
+
'using',
|
|
149
|
+
'when',
|
|
150
|
+
'not',
|
|
151
|
+
'all',
|
|
152
|
+
'are',
|
|
153
|
+
'has',
|
|
154
|
+
'have',
|
|
155
|
+
'been',
|
|
156
|
+
'will',
|
|
157
|
+
'can',
|
|
158
|
+
'should',
|
|
159
|
+
'must',
|
|
160
|
+
'may',
|
|
161
|
+
'each',
|
|
162
|
+
'which',
|
|
163
|
+
'their',
|
|
164
|
+
'your',
|
|
165
|
+
'its',
|
|
166
|
+
'project',
|
|
167
|
+
'code',
|
|
168
|
+
'file',
|
|
169
|
+
'class',
|
|
170
|
+
'method',
|
|
171
|
+
'function',
|
|
172
|
+
'bootstrap',
|
|
139
173
|
]);
|
|
140
174
|
|
|
141
175
|
export default TopicClassifier;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import Logger from '../../infrastructure/logging/Logger.js';
|
|
16
|
-
import { collectSourceFilesWithContent } from './SourceFileCollector.js';
|
|
17
16
|
import { COMPLIANCE_SCORING, QUALITY_GATE } from '../../shared/constants.js';
|
|
17
|
+
import { collectSourceFilesWithContent } from './SourceFileCollector.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Quality Gate 评分算法
|
|
@@ -30,11 +30,15 @@ function computeScore(summary, ruleHealth = []) {
|
|
|
30
30
|
// 加分:规则平均 F1 > 阈值加分
|
|
31
31
|
if (ruleHealth.length > 0) {
|
|
32
32
|
const avgF1 = ruleHealth.reduce((s, r) => s + (r.f1 || 0), 0) / ruleHealth.length;
|
|
33
|
-
if (avgF1 > COMPLIANCE_SCORING.HIGH_F1_THRESHOLD)
|
|
33
|
+
if (avgF1 > COMPLIANCE_SCORING.HIGH_F1_THRESHOLD) {
|
|
34
|
+
score += COMPLIANCE_SCORING.HIGH_F1_BONUS;
|
|
35
|
+
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// 扣分:高误报规则每条扣分
|
|
37
|
-
const problematic = ruleHealth.filter(
|
|
39
|
+
const problematic = ruleHealth.filter(
|
|
40
|
+
(r) => (r.precision || 1) < COMPLIANCE_SCORING.LOW_PRECISION_THRESHOLD
|
|
41
|
+
);
|
|
38
42
|
score -= problematic.length * COMPLIANCE_SCORING.PROBLEMATIC_RULE_PENALTY;
|
|
39
43
|
|
|
40
44
|
return Math.max(0, Math.min(100, Math.round(score)));
|
|
@@ -44,11 +48,21 @@ function computeScore(summary, ruleHealth = []) {
|
|
|
44
48
|
* 判定 Quality Gate 状态
|
|
45
49
|
*/
|
|
46
50
|
function evaluateGate(summary, score, thresholds) {
|
|
47
|
-
const {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
const {
|
|
52
|
+
maxErrors = QUALITY_GATE.MAX_ERRORS,
|
|
53
|
+
maxWarnings = QUALITY_GATE.MAX_WARNINGS,
|
|
54
|
+
minScore = QUALITY_GATE.MIN_SCORE,
|
|
55
|
+
} = thresholds;
|
|
56
|
+
|
|
57
|
+
if (summary.errors > maxErrors) {
|
|
58
|
+
return 'FAIL';
|
|
59
|
+
}
|
|
60
|
+
if (score < minScore) {
|
|
61
|
+
return 'FAIL';
|
|
62
|
+
}
|
|
63
|
+
if (summary.warnings > maxWarnings) {
|
|
64
|
+
return 'WARN';
|
|
65
|
+
}
|
|
52
66
|
return 'PASS';
|
|
53
67
|
}
|
|
54
68
|
|
|
@@ -60,7 +74,13 @@ export class ComplianceReporter {
|
|
|
60
74
|
* @param {import('./ExclusionManager.js').ExclusionManager} exclusionManager
|
|
61
75
|
* @param {object} qualityGateConfig - { maxErrors, maxWarnings, minScore }
|
|
62
76
|
*/
|
|
63
|
-
constructor(
|
|
77
|
+
constructor(
|
|
78
|
+
guardCheckEngine,
|
|
79
|
+
violationsStore,
|
|
80
|
+
ruleLearner,
|
|
81
|
+
exclusionManager,
|
|
82
|
+
qualityGateConfig = {}
|
|
83
|
+
) {
|
|
64
84
|
this.engine = guardCheckEngine;
|
|
65
85
|
this.violationsStore = violationsStore;
|
|
66
86
|
this.ruleLearner = ruleLearner;
|
|
@@ -96,11 +116,15 @@ export class ComplianceReporter {
|
|
|
96
116
|
// 3. 通过 ExclusionManager 过滤被排除的项
|
|
97
117
|
const filteredFiles = [];
|
|
98
118
|
for (const fileResult of auditResult.files || []) {
|
|
99
|
-
if (this.exclusionManager?.isPathExcluded?.(fileResult.filePath))
|
|
119
|
+
if (this.exclusionManager?.isPathExcluded?.(fileResult.filePath)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
100
122
|
|
|
101
|
-
const filteredViolations = fileResult.violations.filter(v => {
|
|
123
|
+
const filteredViolations = fileResult.violations.filter((v) => {
|
|
102
124
|
// isRuleExcluded 内部已检查全局排除
|
|
103
|
-
if (this.exclusionManager?.isRuleExcluded?.(v.ruleId, fileResult.filePath))
|
|
125
|
+
if (this.exclusionManager?.isRuleExcluded?.(v.ruleId, fileResult.filePath)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
104
128
|
return true;
|
|
105
129
|
});
|
|
106
130
|
|
|
@@ -109,9 +133,9 @@ export class ComplianceReporter {
|
|
|
109
133
|
violations: filteredViolations,
|
|
110
134
|
summary: {
|
|
111
135
|
total: filteredViolations.length,
|
|
112
|
-
errors: filteredViolations.filter(v => v.severity === 'error').length,
|
|
113
|
-
warnings: filteredViolations.filter(v => v.severity === 'warning').length,
|
|
114
|
-
infos: filteredViolations.filter(v => v.severity === 'info').length,
|
|
136
|
+
errors: filteredViolations.filter((v) => v.severity === 'error').length,
|
|
137
|
+
warnings: filteredViolations.filter((v) => v.severity === 'warning').length,
|
|
138
|
+
infos: filteredViolations.filter((v) => v.severity === 'info').length,
|
|
115
139
|
},
|
|
116
140
|
});
|
|
117
141
|
}
|
|
@@ -151,14 +175,14 @@ export class ComplianceReporter {
|
|
|
151
175
|
}
|
|
152
176
|
|
|
153
177
|
const topViolations = [...ruleAgg.values()]
|
|
154
|
-
.map(v => ({ ...v, fileCount: v.fileCount.size }))
|
|
178
|
+
.map((v) => ({ ...v, fileCount: v.fileCount.size }))
|
|
155
179
|
.sort((a, b) => b.occurrences - a.occurrences)
|
|
156
180
|
.slice(0, 20);
|
|
157
181
|
|
|
158
182
|
// 6. 文件热点
|
|
159
183
|
const fileHotspots = filteredFiles
|
|
160
|
-
.filter(f => f.summary.total > 0)
|
|
161
|
-
.map(f => ({
|
|
184
|
+
.filter((f) => f.summary.total > 0)
|
|
185
|
+
.map((f) => ({
|
|
162
186
|
filePath: f.filePath,
|
|
163
187
|
violationCount: f.summary.total,
|
|
164
188
|
errorCount: f.summary.errors,
|
|
@@ -201,8 +225,8 @@ export class ComplianceReporter {
|
|
|
201
225
|
// 10. 写入 ViolationsStore(记录本次运行)
|
|
202
226
|
try {
|
|
203
227
|
if (this.violationsStore?.appendRun) {
|
|
204
|
-
const allViolations = filteredFiles.flatMap(f =>
|
|
205
|
-
f.violations.map(v => ({ ...v, filePath: f.filePath }))
|
|
228
|
+
const allViolations = filteredFiles.flatMap((f) =>
|
|
229
|
+
f.violations.map((v) => ({ ...v, filePath: f.filePath }))
|
|
206
230
|
);
|
|
207
231
|
this.violationsStore.appendRun({
|
|
208
232
|
filePath: projectRoot,
|
|
@@ -239,7 +263,6 @@ export class ComplianceReporter {
|
|
|
239
263
|
const { format = 'text' } = options;
|
|
240
264
|
|
|
241
265
|
if (format === 'json') {
|
|
242
|
-
console.log(JSON.stringify(report, null, 2));
|
|
243
266
|
return;
|
|
244
267
|
}
|
|
245
268
|
|
|
@@ -253,38 +276,27 @@ export class ComplianceReporter {
|
|
|
253
276
|
}
|
|
254
277
|
|
|
255
278
|
_printText(report) {
|
|
256
|
-
const { qualityGate,
|
|
279
|
+
const { qualityGate, topViolations, fileHotspots, trend } = report;
|
|
257
280
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
console.log(` 🛡️ Guard Compliance Report`);
|
|
261
|
-
console.log(`${'═'.repeat(60)}`);
|
|
262
|
-
console.log(` ${gateIcon} Quality Gate: ${qualityGate.status} (Score: ${qualityGate.score}/100)`);
|
|
263
|
-
console.log(` 📁 Files Scanned: ${summary.filesScanned}`);
|
|
264
|
-
console.log(` 📊 Violations: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos || 0} infos`);
|
|
281
|
+
const _gateIcon =
|
|
282
|
+
qualityGate.status === 'PASS' ? '✅' : qualityGate.status === 'WARN' ? '⚠️' : '❌';
|
|
265
283
|
|
|
266
284
|
if (trend.hasHistory) {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
285
|
+
const _errTrend = trend.errorsChange > 0 ? `+${trend.errorsChange}` : `${trend.errorsChange}`;
|
|
286
|
+
const _warnTrend =
|
|
287
|
+
trend.warningsChange > 0 ? `+${trend.warningsChange}` : `${trend.warningsChange}`;
|
|
270
288
|
}
|
|
271
289
|
|
|
272
290
|
if (topViolations.length > 0) {
|
|
273
|
-
console.log(`\n Top Violations:`);
|
|
274
291
|
for (const v of topViolations.slice(0, 10)) {
|
|
275
|
-
const
|
|
276
|
-
console.log(` [${v.severity}] ${v.ruleId} — ${v.occurrences} hits in ${v.fileCount} files${fix}`);
|
|
292
|
+
const _fix = v.fixRecipeId ? ` → 🔧 recipe:${v.fixRecipeId}` : '';
|
|
277
293
|
}
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
if (fileHotspots.length > 0) {
|
|
281
|
-
|
|
282
|
-
for (const f of fileHotspots.slice(0, 10)) {
|
|
283
|
-
console.log(` 📄 ${f.filePath} — ${f.violationCount} violations (${f.errorCount} errors)`);
|
|
297
|
+
for (const _f of fileHotspots.slice(0, 10)) {
|
|
284
298
|
}
|
|
285
299
|
}
|
|
286
|
-
|
|
287
|
-
console.log(`${'═'.repeat(60)}\n`);
|
|
288
300
|
}
|
|
289
301
|
|
|
290
302
|
_printMarkdown(report) {
|
|
@@ -303,7 +315,9 @@ export class ComplianceReporter {
|
|
|
303
315
|
|
|
304
316
|
if (trend.hasHistory) {
|
|
305
317
|
lines.push(`| Errors Trend | ${trend.errorsChange > 0 ? '+' : ''}${trend.errorsChange} |`);
|
|
306
|
-
lines.push(
|
|
318
|
+
lines.push(
|
|
319
|
+
`| Warnings Trend | ${trend.warningsChange > 0 ? '+' : ''}${trend.warningsChange} |`
|
|
320
|
+
);
|
|
307
321
|
}
|
|
308
322
|
|
|
309
323
|
if (topViolations.length > 0) {
|
|
@@ -328,8 +342,6 @@ export class ComplianceReporter {
|
|
|
328
342
|
lines.push(`| ${f.filePath} | ${f.violationCount} | ${f.errorCount} |`);
|
|
329
343
|
}
|
|
330
344
|
}
|
|
331
|
-
|
|
332
|
-
console.log(lines.join('\n'));
|
|
333
345
|
}
|
|
334
346
|
}
|
|
335
347
|
|