autosnippet 2.6.0 → 2.7.0

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 (66) hide show
  1. package/bin/cli.js +1 -1
  2. package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-Cq4-iQhP.js} +148 -88
  3. package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
  4. package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -0
  5. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  6. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  7. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  8. package/dashboard/dist/index.html +6 -6
  9. package/lib/bootstrap.js +1 -1
  10. package/lib/cli/SetupService.js +33 -8
  11. package/lib/cli/UpgradeService.js +139 -2
  12. package/lib/core/ast/ProjectGraph.js +599 -0
  13. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  14. package/lib/domain/recipe/Recipe.js +3 -0
  15. package/lib/external/ai/AiProvider.js +83 -20
  16. package/lib/external/ai/providers/ClaudeProvider.js +197 -0
  17. package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
  18. package/lib/external/ai/providers/OpenAiProvider.js +131 -0
  19. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  20. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
  21. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
  22. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  23. package/lib/external/mcp/handlers/bootstrap.js +151 -1634
  24. package/lib/external/mcp/handlers/browse.js +1 -1
  25. package/lib/external/mcp/handlers/candidate.js +1 -33
  26. package/lib/external/mcp/handlers/skill.js +54 -17
  27. package/lib/external/mcp/tools.js +4 -3
  28. package/lib/http/middleware/requestLogger.js +23 -4
  29. package/lib/http/routes/ai.js +3 -1
  30. package/lib/http/routes/auth.js +3 -2
  31. package/lib/http/routes/candidates.js +49 -25
  32. package/lib/http/routes/commands.js +0 -8
  33. package/lib/http/routes/guardRules.js +1 -16
  34. package/lib/http/routes/recipes.js +4 -17
  35. package/lib/http/routes/search.js +11 -19
  36. package/lib/http/routes/skills.js +2 -0
  37. package/lib/http/routes/snippets.js +0 -33
  38. package/lib/http/routes/spm.js +37 -63
  39. package/lib/http/utils/routeHelpers.js +31 -0
  40. package/lib/infrastructure/config/Paths.js +9 -0
  41. package/lib/infrastructure/logging/Logger.js +86 -3
  42. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  43. package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
  44. package/lib/injection/ServiceContainer.js +55 -2
  45. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  46. package/lib/service/candidate/CandidateFileWriter.js +68 -27
  47. package/lib/service/candidate/CandidateService.js +156 -10
  48. package/lib/service/chat/AnalystAgent.js +216 -0
  49. package/lib/service/chat/CandidateGuardrail.js +134 -0
  50. package/lib/service/chat/ChatAgent.js +1036 -167
  51. package/lib/service/chat/ContextWindow.js +730 -0
  52. package/lib/service/chat/HandoffProtocol.js +180 -0
  53. package/lib/service/chat/ProducerAgent.js +240 -0
  54. package/lib/service/chat/ToolRegistry.js +149 -5
  55. package/lib/service/chat/tools.js +1397 -61
  56. package/lib/service/recipe/RecipeFileWriter.js +12 -1
  57. package/lib/service/skills/SignalCollector.js +31 -6
  58. package/lib/service/skills/SkillAdvisor.js +2 -1
  59. package/lib/service/skills/SkillHooks.js +13 -5
  60. package/lib/service/spm/SpmService.js +2 -2
  61. package/package.json +1 -1
  62. package/templates/copilot-instructions.md +20 -3
  63. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  64. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  65. package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
  66. package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
@@ -0,0 +1,468 @@
1
+ /**
2
+ * orchestrator.js — AI-First Bootstrap 管线
3
+ *
4
+ * 核心架构: Analyst → Gate → Producer (双 Agent 模式)
5
+ *
6
+ * 1. Analyst Agent 自由探索代码 (AST 工具 + 文件搜索)
7
+ * 2. HandoffProtocol 质量门控
8
+ * 3. Producer Agent 格式化输出 (submit_candidate)
9
+ * 4. TierScheduler 分层并行执行
10
+ *
11
+ * @module pipeline/orchestrator
12
+ */
13
+
14
+ import path from 'node:path';
15
+ import { AnalystAgent } from '../../../../../service/chat/AnalystAgent.js';
16
+ import { ProducerAgent } from '../../../../../service/chat/ProducerAgent.js';
17
+ import { TierScheduler } from './tier-scheduler.js';
18
+ import { DimensionContext, parseDimensionDigest } from './dimension-context.js';
19
+ import Logger from '../../../../../infrastructure/logging/Logger.js';
20
+
21
+ const logger = Logger.getInstance();
22
+
23
+ // ──────────────────────────────────────────────────────────────────
24
+ // v3.0 维度配置 (增加 focusAreas 用于 Analyst prompt)
25
+ // ──────────────────────────────────────────────────────────────────
26
+
27
+ const DIMENSION_CONFIGS_V3 = {
28
+ 'project-profile': {
29
+ label: '项目概貌',
30
+ guide: '分析项目的整体结构、技术栈、模块划分和入口点。',
31
+ focusAreas: [
32
+ '项目结构和模块划分',
33
+ '技术栈和框架依赖',
34
+ '核心入口点和启动流程',
35
+ ],
36
+ outputType: 'dual',
37
+ allowedKnowledgeTypes: ['architecture'],
38
+ },
39
+ 'objc-deep-scan': {
40
+ label: '深度扫描(常量/Hook)',
41
+ guide: '扫描 #define 宏、extern/static 常量、Method Swizzling hook。',
42
+ focusAreas: [
43
+ '#define 值宏和函数宏',
44
+ 'extern/static 常量定义',
45
+ 'Method Swizzling hook 和 load/initialize 方法',
46
+ ],
47
+ outputType: 'dual',
48
+ allowedKnowledgeTypes: ['code-standard', 'code-pattern'],
49
+ },
50
+ 'category-scan': {
51
+ label: '基础类分类方法扫描',
52
+ guide: '扫描 Foundation/UIKit 的 Category/Extension 方法及其实现。',
53
+ focusAreas: [
54
+ 'NSString/NSArray/NSDictionary 等基础类的 Category',
55
+ 'UIView/UIColor/UIImage 等 UI 组件的 Category',
56
+ '各 Category 方法的使用场景和频率',
57
+ ],
58
+ outputType: 'dual',
59
+ allowedKnowledgeTypes: ['code-standard', 'code-pattern'],
60
+ },
61
+ 'code-standard': {
62
+ label: '代码规范',
63
+ guide: '分析项目的命名约定、注释风格、文件组织方式。',
64
+ focusAreas: [
65
+ '类名前缀和命名约定 (BD/BDUIKit 等)',
66
+ '方法签名风格和 API 命名',
67
+ '注释风格 (语言/格式/MARK 分段)',
68
+ '文件组织和目录规范',
69
+ ],
70
+ outputType: 'dual',
71
+ allowedKnowledgeTypes: ['code-standard', 'code-style'],
72
+ },
73
+ 'architecture': {
74
+ label: '架构模式',
75
+ guide: '分析项目的分层架构、模块职责和依赖关系。',
76
+ focusAreas: [
77
+ '分层架构 (MVC/MVVM/其他)',
78
+ '模块间通信方式 (Protocol/Notification/Target-Action)',
79
+ '依赖管理和服务注册',
80
+ '模块边界约束',
81
+ ],
82
+ outputType: 'dual',
83
+ allowedKnowledgeTypes: ['architecture', 'module-dependency', 'boundary-constraint'],
84
+ },
85
+ 'code-pattern': {
86
+ label: '设计模式',
87
+ guide: '识别项目中使用的设计模式和架构模式。',
88
+ focusAreas: [
89
+ '创建型模式 (Singleton, Factory, Builder)',
90
+ '结构型模式 (Proxy, Adapter, Decorator, Composite)',
91
+ '行为型模式 (Observer, Strategy, Template Method, Delegate)',
92
+ '架构模式 (MVC/MVVM, Service Locator, Coordinator)',
93
+ ],
94
+ outputType: 'candidate',
95
+ allowedKnowledgeTypes: ['code-pattern', 'code-relation', 'inheritance'],
96
+ },
97
+ 'event-and-data-flow': {
98
+ label: '事件与数据流',
99
+ guide: '分析事件传播和数据状态管理方式。',
100
+ focusAreas: [
101
+ '事件传播 (Delegate/Notification/Block/Target-Action)',
102
+ '数据状态管理 (KVO/属性观察/响应式)',
103
+ '数据持久化方案',
104
+ '数据流转路径和状态同步',
105
+ ],
106
+ outputType: 'candidate',
107
+ allowedKnowledgeTypes: ['call-chain', 'data-flow', 'event-and-data-flow'],
108
+ },
109
+ 'best-practice': {
110
+ label: '最佳实践',
111
+ guide: '分析错误处理、并发安全、内存管理等工程实践。',
112
+ focusAreas: [
113
+ '错误处理策略和模式',
114
+ '并发安全 (GCD/NSOperation/锁)',
115
+ '内存管理 (ARC 下的弱引用/循环引用处理)',
116
+ '日志规范和调试基础设施',
117
+ ],
118
+ outputType: 'candidate',
119
+ allowedKnowledgeTypes: ['best-practice'],
120
+ },
121
+ 'agent-guidelines': {
122
+ label: 'Agent 开发注意事项',
123
+ guide: '总结 Agent 在此项目开发时必须遵守的规则和约束。',
124
+ focusAreas: [
125
+ '命名强制规则和前缀约定',
126
+ '线程安全约束',
127
+ '已废弃 API 标记',
128
+ '架构约束注释 (TODO/FIXME)',
129
+ ],
130
+ outputType: 'skill',
131
+ allowedKnowledgeTypes: ['boundary-constraint', 'code-standard'],
132
+ },
133
+ };
134
+
135
+ // ──────────────────────────────────────────────────────────────────
136
+ // fillDimensionsV3 — v3.0 管线入口
137
+ // ──────────────────────────────────────────────────────────────────
138
+
139
+ /**
140
+ * fillDimensionsV3 — v3.0 AI-First 维度填充管线
141
+ *
142
+ * @param {object} fillContext — 由 bootstrapKnowledge 构建的上下文
143
+ */
144
+ export async function fillDimensionsV3(fillContext) {
145
+ const {
146
+ ctx, dimensions, taskManager, sessionId, projectRoot,
147
+ depGraphData, guardAudit, langStats, primaryLang, astProjectSummary,
148
+ skillContext, skillsEnhanced,
149
+ } = fillContext;
150
+
151
+ logger.info('[Bootstrap-v3] ═══ fillDimensionsV3 entered — AI-First pipeline');
152
+
153
+ let allFiles = fillContext.allFiles;
154
+ fillContext.allFiles = null;
155
+
156
+ // ═══════════════════════════════════════════════════════════
157
+ // Step 0: AI 可用性检查
158
+ // ═══════════════════════════════════════════════════════════
159
+ let chatAgent = null;
160
+ try {
161
+ chatAgent = ctx.container.get('chatAgent');
162
+ if (chatAgent && !chatAgent.hasRealAI) chatAgent = null;
163
+ if (chatAgent) chatAgent.resetGlobalSubmittedTitles();
164
+ } catch { /* not available */ }
165
+
166
+ if (!chatAgent) {
167
+ logger.info('[Bootstrap-v3] AI not available — aborting v3 pipeline');
168
+ taskManager?.emitProgress('bootstrap:ai-unavailable', {
169
+ message: 'AI 不可用,v3 管线需要 AI。请检查 AI Provider 配置。',
170
+ });
171
+ for (const dim of dimensions) {
172
+ taskManager?.markTaskCompleted(dim.id, { type: 'skipped', reason: 'ai-unavailable' });
173
+ }
174
+ return;
175
+ }
176
+
177
+ // ═══════════════════════════════════════════════════════════
178
+ // Step 0.5: 构建 ProjectGraph
179
+ // ═══════════════════════════════════════════════════════════
180
+ let projectGraph = null;
181
+ try {
182
+ projectGraph = await ctx.container.buildProjectGraph(projectRoot, {
183
+ maxFiles: 500,
184
+ timeoutMs: 15_000,
185
+ });
186
+ if (projectGraph) {
187
+ const overview = projectGraph.getOverview();
188
+ logger.info(`[Bootstrap-v3] ProjectGraph: ${overview.totalClasses} classes, ${overview.totalProtocols} protocols (${overview.buildTimeMs}ms)`);
189
+ }
190
+ } catch (e) {
191
+ logger.warn(`[Bootstrap-v3] ProjectGraph build failed: ${e.message}`);
192
+ }
193
+
194
+ // ═══════════════════════════════════════════════════════════
195
+ // Step 1: 构建 Agents + 上下文
196
+ // ═══════════════════════════════════════════════════════════
197
+ const analystAgent = new AnalystAgent(chatAgent, projectGraph, { maxRetries: 1 });
198
+ const producerAgent = new ProducerAgent(chatAgent);
199
+
200
+ // 注入文件缓存
201
+ chatAgent.setFileCache(allFiles);
202
+
203
+ // 项目信息
204
+ const projectInfo = {
205
+ name: path.basename(projectRoot),
206
+ lang: primaryLang || 'objectivec',
207
+ fileCount: allFiles?.length || 0,
208
+ };
209
+
210
+ // 跨维度上下文
211
+ const dimContext = new DimensionContext({
212
+ projectName: projectInfo.name,
213
+ primaryLang: projectInfo.lang,
214
+ fileCount: projectInfo.fileCount,
215
+ targetCount: Object.keys(fillContext.targetFileMap || {}).length,
216
+ modules: Object.keys(fillContext.targetFileMap || {}),
217
+ depGraph: depGraphData || null,
218
+ astMetrics: astProjectSummary?.projectMetrics || null,
219
+ guardSummary: guardAudit?.summary || null,
220
+ });
221
+
222
+ // ═══════════════════════════════════════════════════════════
223
+ // Step 2: 按维度分层执行 (Analyst → Gate → Producer)
224
+ // ═══════════════════════════════════════════════════════════
225
+ const concurrency = parseInt(process.env.ASD_PARALLEL_CONCURRENCY || '2', 10);
226
+ const enableParallel = process.env.ASD_PARALLEL_BOOTSTRAP !== 'false';
227
+ const scheduler = new TierScheduler();
228
+
229
+ // 过滤出有定义的维度
230
+ const activeDimIds = dimensions
231
+ .map(d => d.id)
232
+ .filter(id => DIMENSION_CONFIGS_V3[id]);
233
+
234
+ logger.info(`[Bootstrap-v3] Active dimensions: [${activeDimIds.join(', ')}], concurrency=${enableParallel ? concurrency : 1}`);
235
+
236
+ const candidateResults = { created: 0, failed: 0, errors: [] };
237
+ const dimensionCandidates = {};
238
+
239
+ /**
240
+ * 执行单个维度: Analyst → Gate → Producer
241
+ */
242
+ async function executeDimension(dimId) {
243
+ const dim = dimensions.find(d => d.id === dimId);
244
+ const v3Config = DIMENSION_CONFIGS_V3[dimId];
245
+ if (!dim || !v3Config) {
246
+ return { candidateCount: 0, error: 'dimension not found' };
247
+ }
248
+
249
+ // 合并 v3 配置和原始维度配置 (保留 skillWorthy, skillMeta 等)
250
+ const dimConfig = {
251
+ ...v3Config,
252
+ id: dimId,
253
+ skillWorthy: dim.skillWorthy,
254
+ dualOutput: dim.dualOutput,
255
+ skillMeta: dim.skillMeta,
256
+ knowledgeTypes: dim.knowledgeTypes || v3Config.allowedKnowledgeTypes,
257
+ };
258
+
259
+ // Session 有效性检查
260
+ if (taskManager && !taskManager.isSessionValid(sessionId)) {
261
+ logger.warn(`[Bootstrap-v3] Session superseded — skipping "${dimId}"`);
262
+ return { candidateCount: 0, error: 'session-superseded' };
263
+ }
264
+
265
+ taskManager?.markTaskFilling(dimId);
266
+ logger.info(`[Bootstrap-v3] ── Dimension "${dimId}" (${dimConfig.label}) ──`);
267
+
268
+ const dimStartTime = Date.now();
269
+
270
+ try {
271
+ // ── Phase 1: Analyst ──
272
+ const analysisReport = await Promise.race([
273
+ analystAgent.analyze(dimConfig, projectInfo, { sessionId }),
274
+ new Promise((_, reject) =>
275
+ setTimeout(() => reject(new Error(`Analyst timeout for "${dimId}"`)), 180_000)),
276
+ ]);
277
+
278
+ logger.info(`[Bootstrap-v3] Analyst "${dimId}": ${analysisReport.analysisText.length} chars, ${analysisReport.referencedFiles.length} files (${Date.now() - dimStartTime}ms)`);
279
+
280
+ // ── Phase 2: Producer (如果需要候选输出) ──
281
+ let producerResult = { candidateCount: 0, toolCalls: [], reply: '' };
282
+ // v3 优先使用 DIMENSION_CONFIGS_V3 的 outputType,回退到 baseDimension 的 skillWorthy/dualOutput
283
+ const v3OutputType = DIMENSION_CONFIGS_V3[dimId]?.outputType;
284
+ const needsCandidates = v3OutputType
285
+ ? v3OutputType !== 'skill' // 'dual' 或 'candidate' 都产出候选
286
+ : (!dimConfig.skillWorthy || dimConfig.dualOutput);
287
+
288
+ if (needsCandidates && analysisReport.analysisText.length >= 100) {
289
+ producerResult = await Promise.race([
290
+ producerAgent.produce(analysisReport, dimConfig, projectInfo, { sessionId }),
291
+ new Promise((_, reject) =>
292
+ setTimeout(() => reject(new Error(`Producer timeout for "${dimId}"`)), 120_000)),
293
+ ]);
294
+
295
+ candidateResults.created += producerResult.candidateCount;
296
+ logger.info(`[Bootstrap-v3] Producer "${dimId}": ${producerResult.candidateCount} candidates (${Date.now() - dimStartTime}ms total)`);
297
+ }
298
+
299
+ // ── Phase 3: 记录 DimensionDigest ──
300
+ const digest = parseDimensionDigest(producerResult.reply) || {
301
+ summary: `v3 分析: ${analysisReport.analysisText.substring(0, 200)}...`,
302
+ candidateCount: producerResult.candidateCount,
303
+ keyFindings: [],
304
+ crossRefs: {},
305
+ gaps: [],
306
+ };
307
+ dimContext.addDimensionDigest(dimId, digest);
308
+
309
+ // 记录到 DimensionContext
310
+ for (const tc of (producerResult.toolCalls || [])) {
311
+ const tool = tc.tool || tc.name;
312
+ if (tool === 'submit_candidate' || tool === 'submit_with_check') {
313
+ dimContext.addSubmittedCandidate(dimId, {
314
+ title: tc.params?.title || '',
315
+ subTopic: tc.params?.category || '',
316
+ summary: tc.params?.summary || '',
317
+ });
318
+ }
319
+ }
320
+
321
+ // 保存分析结果供 Skill 生成
322
+ dimensionCandidates[dimId] = {
323
+ analysisReport,
324
+ producerResult,
325
+ };
326
+
327
+ taskManager?.markTaskCompleted(dimId, {
328
+ type: needsCandidates ? 'candidate' : 'skill',
329
+ extracted: producerResult.candidateCount,
330
+ created: producerResult.candidateCount,
331
+ status: 'v3-complete',
332
+ durationMs: Date.now() - dimStartTime,
333
+ toolCallCount: (analysisReport.metadata?.toolCallCount || 0) + (producerResult.toolCalls?.length || 0),
334
+ });
335
+
336
+ return {
337
+ candidateCount: producerResult.candidateCount,
338
+ analysisChars: analysisReport.analysisText.length,
339
+ referencedFiles: analysisReport.referencedFiles.length,
340
+ durationMs: Date.now() - dimStartTime,
341
+ };
342
+
343
+ } catch (err) {
344
+ logger.error(`[Bootstrap-v3] Dimension "${dimId}" failed: ${err.message}`);
345
+ candidateResults.errors.push({ dimId, error: err.message });
346
+ taskManager?.markTaskCompleted(dimId, { type: 'error', reason: err.message });
347
+ return { candidateCount: 0, error: err.message };
348
+ }
349
+ }
350
+
351
+ // ═══════════════════════════════════════════════════════════
352
+ // Step 3: 执行 (并行 or 串行)
353
+ // ═══════════════════════════════════════════════════════════
354
+ const t0 = Date.now();
355
+
356
+ if (enableParallel) {
357
+ const results = await scheduler.execute(executeDimension, {
358
+ concurrency,
359
+ shouldAbort: () => taskManager && !taskManager.isSessionValid(sessionId),
360
+ onTierComplete: (tierIndex, tierResults) => {
361
+ const tierStats = [...tierResults.values()];
362
+ const totalCandidates = tierStats.reduce((s, r) => s + (r.candidateCount || 0), 0);
363
+ logger.info(`[Bootstrap-v3] Tier ${tierIndex + 1} complete: ${tierResults.size} dimensions, ${totalCandidates} candidates`);
364
+ },
365
+ });
366
+
367
+ logger.info(`[Bootstrap-v3] All tiers complete: ${results.size} dimensions in ${Date.now() - t0}ms`);
368
+ } else {
369
+ // 串行: 按 TierScheduler 内部顺序逐个执行
370
+ for (const tier of scheduler.getTiers()) {
371
+ for (const dimId of tier) {
372
+ if (!activeDimIds.includes(dimId)) continue;
373
+ if (taskManager && !taskManager.isSessionValid(sessionId)) break;
374
+ await executeDimension(dimId);
375
+ }
376
+ }
377
+ logger.info(`[Bootstrap-v3] Serial execution complete in ${Date.now() - t0}ms`);
378
+ }
379
+
380
+ // ═══════════════════════════════════════════════════════════
381
+ // Step 4: Project Skill 生成 (skillWorthy 维度)
382
+ //
383
+ // v3: 直接使用 Analyst 的分析文本作为 Skill 内容
384
+ // 不再通过 buildProjectSkillContent 转换候选数组
385
+ // ═══════════════════════════════════════════════════════════
386
+ const skillResults = { created: 0, failed: 0, skills: [], errors: [] };
387
+
388
+ try {
389
+ const { createSkill } = await import('../../skill.js');
390
+
391
+ for (const dim of dimensions) {
392
+ if (!dim.skillWorthy) continue;
393
+ const dimData = dimensionCandidates[dim.id];
394
+ if (!dimData?.analysisReport?.analysisText) continue;
395
+ if (taskManager && !taskManager.isSessionValid(sessionId)) break;
396
+
397
+ try {
398
+ const skillName = dim.skillMeta?.name || `project-${dim.id}`;
399
+ const skillDescription = dim.skillMeta?.description || `Auto-generated skill for ${dim.label}`;
400
+
401
+ // v3: Analyst 分析文本就是高质量的 Skill 内容
402
+ const analysisText = dimData.analysisReport.analysisText;
403
+ const referencedFiles = dimData.analysisReport.referencedFiles || [];
404
+
405
+ // 构建 Markdown Skill 内容
406
+ const skillContent = [
407
+ `# ${dim.label || dim.id}`,
408
+ '',
409
+ `> Auto-generated by Bootstrap v3 (AI-First). Sources: ${referencedFiles.length} files analyzed.`,
410
+ '',
411
+ analysisText,
412
+ '',
413
+ referencedFiles.length > 0
414
+ ? `## Referenced Files\n\n${referencedFiles.map(f => `- \`${f}\``).join('\n')}`
415
+ : '',
416
+ ].filter(Boolean).join('\n');
417
+
418
+ const result = createSkill(ctx, {
419
+ name: skillName,
420
+ description: skillDescription,
421
+ content: skillContent,
422
+ overwrite: true,
423
+ createdBy: 'bootstrap-v3',
424
+ });
425
+
426
+ const parsed = JSON.parse(result);
427
+ if (parsed.success) {
428
+ skillResults.created++;
429
+ skillResults.skills.push(skillName);
430
+ logger.info(`[Bootstrap-v3] Skill "${skillName}" created for "${dim.id}"`);
431
+ } else {
432
+ throw new Error(parsed.error?.message || 'createSkill returned failure');
433
+ }
434
+
435
+ taskManager?.markTaskCompleted(dim.id, {
436
+ type: 'skill',
437
+ skillName,
438
+ sourceCount: referencedFiles.length,
439
+ });
440
+ } catch (err) {
441
+ logger.warn(`[Bootstrap-v3] Skill generation failed for "${dim.id}": ${err.message}`);
442
+ skillResults.failed++;
443
+ skillResults.errors.push({ dimId: dim.id, error: err.message });
444
+ taskManager?.markTaskFailed?.(dim.id, err);
445
+ }
446
+ }
447
+ } catch (e) {
448
+ logger.warn(`[Bootstrap-v3] Skill generation module import failed: ${e.message}`);
449
+ }
450
+
451
+ // ═══════════════════════════════════════════════════════════
452
+ // Summary
453
+ // ═══════════════════════════════════════════════════════════
454
+ const totalTimeMs = Date.now() - t0;
455
+ logger.info([
456
+ `[Bootstrap-v3] ═══ Pipeline complete ═══`,
457
+ ` Candidates: ${candidateResults.created} created, ${candidateResults.errors.length} errors`,
458
+ ` Skills: ${skillResults.created} created, ${skillResults.failed} failed`,
459
+ ` Time: ${totalTimeMs}ms (${(totalTimeMs / 1000).toFixed(1)}s)`,
460
+ ` Mode: ${enableParallel ? `parallel (concurrency=${concurrency})` : 'serial'}`,
461
+ ].join('\n'));
462
+
463
+ // 释放文件缓存
464
+ allFiles = null;
465
+ chatAgent.setFileCache(null);
466
+ }
467
+
468
+ export default fillDimensionsV3;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * TierScheduler.js — 维度分层并行调度器
3
+ *
4
+ * 按维度间信息依赖关系分 4 层执行:
5
+ * - Tier 1: 基础数据层 (project-profile, objc-deep-scan, category-scan) — 可并行
6
+ * - Tier 2: 规范+架构 (code-standard, architecture) — 依赖 Tier 1
7
+ * - Tier 3: 模式+流转 (code-pattern, event-and-data-flow) — 依赖 Tier 2
8
+ * - Tier 4: 总结层 (best-practice, agent-guidelines) — 依赖全部
9
+ *
10
+ * 每层内部可并行 (受 concurrency 限制),层间串行。
11
+ *
12
+ * @module TierScheduler
13
+ */
14
+
15
+ import Logger from '../../../../../infrastructure/logging/Logger.js';
16
+
17
+ const logger = Logger.getInstance();
18
+
19
+ // ──────────────────────────────────────────────────────────────────
20
+ // 分层定义
21
+ // ──────────────────────────────────────────────────────────────────
22
+
23
+ const DEFAULT_TIERS = [
24
+ ['project-profile', 'objc-deep-scan', 'category-scan'], // Tier 1: 基础数据
25
+ ['code-standard', 'architecture'], // Tier 2: 规范+架构
26
+ ['code-pattern', 'event-and-data-flow'], // Tier 3: 模式+流转
27
+ ['best-practice', 'agent-guidelines'], // Tier 4: 总结
28
+ ];
29
+
30
+ // ──────────────────────────────────────────────────────────────────
31
+ // 简单信号量 (控制并发)
32
+ // ──────────────────────────────────────────────────────────────────
33
+
34
+ class Semaphore {
35
+ #permits;
36
+ #queue = [];
37
+
38
+ constructor(permits) {
39
+ this.#permits = permits;
40
+ }
41
+
42
+ async acquire() {
43
+ if (this.#permits > 0) {
44
+ this.#permits--;
45
+ return;
46
+ }
47
+ return new Promise(resolve => {
48
+ this.#queue.push(resolve);
49
+ });
50
+ }
51
+
52
+ release() {
53
+ if (this.#queue.length > 0) {
54
+ const resolve = this.#queue.shift();
55
+ resolve();
56
+ } else {
57
+ this.#permits++;
58
+ }
59
+ }
60
+ }
61
+
62
+ // ──────────────────────────────────────────────────────────────────
63
+ // TierScheduler
64
+ // ──────────────────────────────────────────────────────────────────
65
+
66
+ export class TierScheduler {
67
+ /** @type {string[][]} */
68
+ #tiers;
69
+
70
+ /**
71
+ * @param {string[][]} [tiers] — 自定义分层 (默认使用 DEFAULT_TIERS)
72
+ */
73
+ constructor(tiers = DEFAULT_TIERS) {
74
+ this.#tiers = tiers;
75
+ }
76
+
77
+ /**
78
+ * 分层执行维度
79
+ *
80
+ * @param {Function} executeDimension — async (dimId) => DimensionResult
81
+ * @param {object} [options]
82
+ * @param {number} [options.concurrency=2] — Tier 内最大并行数
83
+ * @param {Function} [options.onTierComplete] — (tierIndex, tierResults) => void
84
+ * @param {Function} [options.shouldAbort] — () => boolean — 外部中止信号
85
+ * @returns {Promise<Map<string, any>>} — dimId → result
86
+ */
87
+ async execute(executeDimension, options = {}) {
88
+ const { concurrency = 2, onTierComplete, shouldAbort } = options;
89
+ const results = new Map();
90
+
91
+ for (let tierIndex = 0; tierIndex < this.#tiers.length; tierIndex++) {
92
+ const tier = this.#tiers[tierIndex];
93
+
94
+ if (shouldAbort?.()) {
95
+ logger.warn(`[TierScheduler] Aborted before Tier ${tierIndex + 1}`);
96
+ break;
97
+ }
98
+
99
+ logger.info(`[TierScheduler] ── Tier ${tierIndex + 1}/${this.#tiers.length}: [${tier.join(', ')}] (concurrency=${concurrency})`);
100
+
101
+ const tierResults = await this.#executeTier(tier, executeDimension, concurrency, shouldAbort);
102
+
103
+ for (const [dimId, result] of tierResults) {
104
+ results.set(dimId, result);
105
+ }
106
+
107
+ onTierComplete?.(tierIndex, tierResults);
108
+ }
109
+
110
+ return results;
111
+ }
112
+
113
+ /**
114
+ * 执行单个 Tier 内的所有维度 (并发控制)
115
+ */
116
+ async #executeTier(dimensionIds, executeDimension, concurrency, shouldAbort) {
117
+ const semaphore = new Semaphore(concurrency);
118
+ const results = new Map();
119
+
120
+ await Promise.all(
121
+ dimensionIds.map(async (dimId) => {
122
+ if (shouldAbort?.()) return;
123
+
124
+ await semaphore.acquire();
125
+ try {
126
+ if (shouldAbort?.()) return;
127
+ const result = await executeDimension(dimId);
128
+ results.set(dimId, result);
129
+ } catch (err) {
130
+ logger.error(`[TierScheduler] Dimension "${dimId}" failed: ${err.message}`);
131
+ results.set(dimId, { error: err.message, candidateCount: 0 });
132
+ } finally {
133
+ semaphore.release();
134
+ }
135
+ })
136
+ );
137
+
138
+ return results;
139
+ }
140
+
141
+ /**
142
+ * 获取维度所在的 Tier 索引
143
+ * @param {string} dimId
144
+ * @returns {number} — 0-based tier index, -1 if not found
145
+ */
146
+ getTierIndex(dimId) {
147
+ for (let i = 0; i < this.#tiers.length; i++) {
148
+ if (this.#tiers[i].includes(dimId)) return i;
149
+ }
150
+ return -1;
151
+ }
152
+
153
+ /**
154
+ * 获取分层定义
155
+ * @returns {string[][]}
156
+ */
157
+ getTiers() {
158
+ return this.#tiers;
159
+ }
160
+ }
161
+
162
+ export default TierScheduler;