autosnippet 2.7.0 → 2.8.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 (44) hide show
  1. package/README.md +138 -66
  2. package/bin/api-server.js +5 -0
  3. package/bin/cli.js +26 -0
  4. package/bin/mcp-server.js +22 -0
  5. package/dashboard/dist/assets/{icons-Cq4-iQhP.js → icons-B_Xg4B-s.js} +61 -61
  6. package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
  7. package/dashboard/dist/assets/index-Duc8Qk-c.js +197 -0
  8. package/dashboard/dist/index.html +3 -3
  9. package/lib/bootstrap.js +17 -0
  10. package/lib/cli/SetupService.js +53 -0
  11. package/lib/external/ai/providers/ClaudeProvider.js +12 -1
  12. package/lib/external/ai/providers/GoogleGeminiProvider.js +13 -1
  13. package/lib/external/ai/providers/OpenAiProvider.js +13 -3
  14. package/lib/external/mcp/McpServer.js +11 -4
  15. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +194 -5
  16. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +8 -10
  17. package/lib/external/mcp/handlers/bootstrap.js +8 -0
  18. package/lib/external/mcp/handlers/skill.js +202 -0
  19. package/lib/external/mcp/tools.js +54 -1
  20. package/lib/http/routes/ai.js +155 -1
  21. package/lib/infrastructure/config/Paths.js +3 -0
  22. package/lib/infrastructure/database/DatabaseConnection.js +6 -1
  23. package/lib/infrastructure/vector/JsonVectorAdapter.js +2 -0
  24. package/lib/service/automation/handlers/AlinkHandler.js +43 -4
  25. package/lib/service/candidate/CandidateFileWriter.js +4 -0
  26. package/lib/service/chat/AnalystAgent.js +37 -8
  27. package/lib/service/chat/CandidateGuardrail.js +3 -3
  28. package/lib/service/chat/ChatAgent.js +20 -1
  29. package/lib/service/chat/ConversationStore.js +3 -0
  30. package/lib/service/chat/HandoffProtocol.js +1 -0
  31. package/lib/service/chat/Memory.js +3 -0
  32. package/lib/service/chat/ProducerAgent.js +53 -0
  33. package/lib/service/chat/tools.js +13 -6
  34. package/lib/service/guard/ExclusionManager.js +2 -0
  35. package/lib/service/guard/RuleLearner.js +2 -0
  36. package/lib/service/quality/FeedbackCollector.js +2 -0
  37. package/lib/service/recipe/RecipeFileWriter.js +4 -0
  38. package/lib/service/recipe/RecipeStatsTracker.js +2 -0
  39. package/lib/service/skills/SignalCollector.js +2 -0
  40. package/lib/shared/PathGuard.js +314 -0
  41. package/package.json +1 -1
  42. package/resources/native-ui/combined-window.swift +494 -0
  43. package/dashboard/dist/assets/index-DBxH7pVn.css +0 -1
  44. package/dashboard/dist/assets/index-Dw2F6qAS.js +0 -197
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>AutoSnippet Dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-Dw2F6qAS.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Duc8Qk-c.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-BotF760a.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
12
- <link rel="modulepreload" crossorigin href="/assets/icons-Cq4-iQhP.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/icons-B_Xg4B-s.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-CVLHn9O5.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-markdown-BA6FB2NP.js">
15
- <link rel="stylesheet" crossorigin href="/assets/index-DBxH7pVn.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-CkIih2CC.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
package/lib/bootstrap.js CHANGED
@@ -11,6 +11,7 @@ import Gateway from './core/gateway/Gateway.js';
11
11
  import AuditLogger from './infrastructure/audit/AuditLogger.js';
12
12
  import AuditStore from './infrastructure/audit/AuditStore.js';
13
13
  import { SkillHooks } from './service/skills/SkillHooks.js';
14
+ import pathGuard from './shared/PathGuard.js';
14
15
 
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
@@ -24,6 +25,22 @@ export class Bootstrap {
24
25
  this.components = {};
25
26
  }
26
27
 
28
+ /**
29
+ * 配置 PathGuard 路径安全守卫
30
+ * 必须在任何文件写操作前调用
31
+ * @param {string} projectRoot - 用户项目的绝对路径
32
+ * @param {string} [knowledgeBaseDir] - 知识库目录名(如 'AutoSnippet')
33
+ */
34
+ static configurePathGuard(projectRoot, knowledgeBaseDir) {
35
+ if (!pathGuard.configured && projectRoot) {
36
+ const packageRoot = path.resolve(__dirname, '..');
37
+ pathGuard.configure({ projectRoot, packageRoot, knowledgeBaseDir });
38
+ } else if (knowledgeBaseDir) {
39
+ // 已配置但知识库目录名可能后续才知道
40
+ pathGuard.setKnowledgeBaseDir(knowledgeBaseDir);
41
+ }
42
+ }
43
+
27
44
  /**
28
45
  * 初始化应用程序
29
46
  */
@@ -188,6 +188,9 @@ export class SetupService {
188
188
  // 确保 .autosnippet/ 在主仓库 .gitignore 中
189
189
  this._ensureGitignore();
190
190
 
191
+ // .env — AI 配置模板
192
+ this._ensureEnvFile();
193
+
191
194
  return { created: 'runtime' };
192
195
  }
193
196
 
@@ -640,6 +643,35 @@ export class SetupService {
640
643
 
641
644
  /* ═══ Helpers ════════════════════════════════════════ */
642
645
 
646
+ /**
647
+ * @private 在项目根目录创建 .env 文件(从 .env.example 复制)
648
+ * 如果 .env 已存在则跳过并提示用户手动配置。
649
+ */
650
+ _ensureEnvFile() {
651
+ const envPath = join(this.projectRoot, '.env');
652
+ if (existsSync(envPath)) {
653
+ console.log(' ℹ️ .env 已存在,跳过写入。如需配置 AI,请手动编辑或通过 Dashboard 设置');
654
+ return;
655
+ }
656
+
657
+ const examplePath = join(REPO_ROOT, '.env.example');
658
+ if (existsSync(examplePath)) {
659
+ copyFileSync(examplePath, envPath);
660
+ } else {
661
+ // fallback: .env.example 缺失时写入最小模板
662
+ writeFileSync(envPath, [
663
+ '# AutoSnippet AI 配置(由 asd setup 自动生成)',
664
+ '# 完整配置说明见 .env.example',
665
+ '',
666
+ 'ASD_AI_PROVIDER=google',
667
+ 'ASD_AI_MODEL=gemini-2.0-flash',
668
+ '# ASD_GOOGLE_API_KEY=',
669
+ '',
670
+ ].join('\n'));
671
+ }
672
+ console.log(' ✅ .env(已从 .env.example 复制,请填写 API Key 后使用)');
673
+ }
674
+
643
675
  /** @private 确保项目 .gitignore 正确配置 AutoSnippet 相关规则 */
644
676
  _ensureGitignore() {
645
677
  const giPath = join(this.projectRoot, '.gitignore');
@@ -672,6 +704,27 @@ export class SetupService {
672
704
  console.log(' ✅ .gitignore += !.autosnippet/config.json');
673
705
  }
674
706
 
707
+ // ── 必须忽略:.env(包含 API Key 等敏感信息) ──
708
+ if (!content.includes('.env') || (!content.match(/^\.env$/m) && !content.match(/^\.env\s/m))) {
709
+ content += `\n# AutoSnippet 环境变量(含 API Key,不入库)\n.env\n`;
710
+ changed = true;
711
+ console.log(' ✅ .gitignore += .env');
712
+ }
713
+
714
+ // ── 必须忽略:logs/(winston 运行日志,可达数十 MB) ──
715
+ if (!content.match(/^logs\/?$/m)) {
716
+ content += `\n# AutoSnippet 运行日志\nlogs/\n`;
717
+ changed = true;
718
+ console.log(' ✅ .gitignore += logs/');
719
+ }
720
+
721
+ // ── 必须忽略:.autosnippet-drafts/(AI 草稿临时目录) ──
722
+ if (!content.includes('.autosnippet-drafts')) {
723
+ content += `\n# AutoSnippet AI 草稿(临时)\n.autosnippet-drafts/\n`;
724
+ changed = true;
725
+ console.log(' ✅ .gitignore += .autosnippet-drafts/');
726
+ }
727
+
675
728
  // Skills 已迁移到 AutoSnippet/skills/(知识库目录内),自动跟随 Git
676
729
 
677
730
  // ── 清理旧版本的 .autosnippet/skills/ negation(已迁移,不再需要)──
@@ -203,8 +203,17 @@ export class ClaudeProvider extends AiProvider {
203
203
  * stop_reason: 'end_turn' | 'tool_use' | 'max_tokens'
204
204
  */
205
205
  #parseToolResponse(data) {
206
+ // 提取 token 用量 (Claude usage)
207
+ const usage = data?.usage
208
+ ? {
209
+ inputTokens: data.usage.input_tokens || 0,
210
+ outputTokens: data.usage.output_tokens || 0,
211
+ totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
212
+ }
213
+ : null;
214
+
206
215
  if (!data?.content?.length) {
207
- return { text: '', functionCalls: null };
216
+ return { text: '', functionCalls: null, usage };
208
217
  }
209
218
 
210
219
  const functionCalls = [];
@@ -227,12 +236,14 @@ export class ClaudeProvider extends AiProvider {
227
236
  return {
228
237
  text: textParts.length > 0 ? textParts.join('\n') : null,
229
238
  functionCalls,
239
+ usage,
230
240
  };
231
241
  }
232
242
 
233
243
  return {
234
244
  text: textParts.join('\n'),
235
245
  functionCalls: null,
246
+ usage,
236
247
  };
237
248
  }
238
249
 
@@ -242,8 +242,18 @@ export class GoogleGeminiProvider extends AiProvider {
242
242
  */
243
243
  #parseToolResponse(data) {
244
244
  const content = data?.candidates?.[0]?.content;
245
+
246
+ // 提取 token 用量 (Gemini usageMetadata)
247
+ const usage = data?.usageMetadata
248
+ ? {
249
+ inputTokens: data.usageMetadata.promptTokenCount || 0,
250
+ outputTokens: data.usageMetadata.candidatesTokenCount || 0,
251
+ totalTokens: data.usageMetadata.totalTokenCount || 0,
252
+ }
253
+ : null;
254
+
245
255
  if (!content || !content.parts || content.parts.length === 0) {
246
- return { text: '', functionCalls: null };
256
+ return { text: '', functionCalls: null, usage };
247
257
  }
248
258
 
249
259
  const functionCalls = [];
@@ -267,12 +277,14 @@ export class GoogleGeminiProvider extends AiProvider {
267
277
  return {
268
278
  text: textParts.length > 0 ? textParts.join('\n') : null,
269
279
  functionCalls,
280
+ usage,
270
281
  };
271
282
  }
272
283
 
273
284
  return {
274
285
  text: textParts.join('\n'),
275
286
  functionCalls: null,
287
+ usage,
276
288
  };
277
289
  }
278
290
 
@@ -146,7 +146,17 @@ export class OpenAiProvider extends AiProvider {
146
146
  */
147
147
  #parseToolResponse(data) {
148
148
  const choice = data?.choices?.[0];
149
- if (!choice) return { text: '', functionCalls: null };
149
+
150
+ // 提取 token 用量 (OpenAI usage)
151
+ const usage = data?.usage
152
+ ? {
153
+ inputTokens: data.usage.prompt_tokens || 0,
154
+ outputTokens: data.usage.completion_tokens || 0,
155
+ totalTokens: data.usage.total_tokens || 0,
156
+ }
157
+ : null;
158
+
159
+ if (!choice) return { text: '', functionCalls: null, usage };
150
160
 
151
161
  const message = choice.message;
152
162
  const text = message?.content || null;
@@ -165,11 +175,11 @@ export class OpenAiProvider extends AiProvider {
165
175
 
166
176
  if (functionCalls.length > 0) {
167
177
  this.logger.debug(`[OpenAI] native function calls: ${functionCalls.map(fc => fc.name).join(', ')}`);
168
- return { text, functionCalls };
178
+ return { text, functionCalls, usage };
169
179
  }
170
180
  }
171
181
 
172
- return { text, functionCalls: null };
182
+ return { text, functionCalls: null, usage };
173
183
  }
174
184
 
175
185
  async summarize(code) {
@@ -51,6 +51,11 @@ export class McpServer {
51
51
  async initialize() {
52
52
  if (!this.container) {
53
53
  const { default: Bootstrap } = await import('../../bootstrap.js');
54
+
55
+ // 路径安全守卫 — 在任何写操作前配置
56
+ const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
57
+ Bootstrap.configurePathGuard(projectRoot);
58
+
54
59
  this.bootstrap = new Bootstrap();
55
60
  const components = await this.bootstrap.initialize();
56
61
 
@@ -62,7 +67,7 @@ export class McpServer {
62
67
  auditLogger: components.auditLogger,
63
68
  gateway: components.gateway,
64
69
  constitution: components.constitution,
65
- projectRoot: process.env.ASD_PROJECT_DIR || process.cwd(),
70
+ projectRoot,
66
71
  });
67
72
 
68
73
  // 注册 Gateway action handlers
@@ -143,10 +148,12 @@ export class McpServer {
143
148
  // Bootstrap 冷启动
144
149
  case 'autosnippet_bootstrap_knowledge': return bootstrapHandlers.bootstrapKnowledge(ctx, args);
145
150
  case 'autosnippet_bootstrap_refine': return bootstrapHandlers.bootstrapRefine(ctx, args);
146
- // Skills 加载 & 创建 & 推荐
151
+ // Skills 加载 & 创建 & 管理 & 推荐
147
152
  case 'autosnippet_list_skills': return skillHandlers.listSkills();
148
153
  case 'autosnippet_load_skill': return skillHandlers.loadSkill(ctx, args);
149
154
  case 'autosnippet_create_skill': return skillHandlers.createSkill(ctx, args);
155
+ case 'autosnippet_delete_skill': return skillHandlers.deleteSkill(ctx, args);
156
+ case 'autosnippet_update_skill': return skillHandlers.updateSkill(ctx, args);
150
157
  case 'autosnippet_suggest_skills': return skillHandlers.suggestSkills(ctx);
151
158
  default: throw new Error(`Unknown tool: ${name}`);
152
159
  }
@@ -195,9 +202,9 @@ export class McpServer {
195
202
  await this.initialize();
196
203
  const transport = new StdioServerTransport();
197
204
  await this.server.connect(transport);
198
- this.logger.info('MCP Server started (stdio) — 31 tools');
205
+ this.logger.info('MCP Server started (stdio) — 38 tools');
199
206
  // 在 stderr 写一行简洁的就绪通知(不使用 winston,仅用于 Cursor 日志面板 & 调试)
200
- process.stderr.write('AutoSnippet MCP ready — 31 tools\n');
207
+ process.stderr.write('AutoSnippet MCP ready — 38 tools\n');
201
208
  }
202
209
 
203
210
  async shutdown() {
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import path from 'node:path';
15
+ import fs from 'node:fs/promises';
15
16
  import { AnalystAgent } from '../../../../../service/chat/AnalystAgent.js';
16
17
  import { ProducerAgent } from '../../../../../service/chat/ProducerAgent.js';
17
18
  import { TierScheduler } from './tier-scheduler.js';
@@ -20,6 +21,69 @@ import Logger from '../../../../../infrastructure/logging/Logger.js';
20
21
 
21
22
  const logger = Logger.getInstance();
22
23
 
24
+ // ──────────────────────────────────────────────────────────────────
25
+ // P3: 断点续传 — Checkpoint 存储/恢复
26
+ // ──────────────────────────────────────────────────────────────────
27
+
28
+ const CHECKPOINT_TTL_MS = 3600_000; // 1小时内有效
29
+
30
+ /**
31
+ * 保存维度级 checkpoint
32
+ * @param {string} projectRoot
33
+ * @param {string} sessionId
34
+ * @param {string} dimId
35
+ * @param {object} result — 维度执行结果
36
+ * @param {object} [digest] — DimensionDigest
37
+ */
38
+ async function saveDimensionCheckpoint(projectRoot, sessionId, dimId, result, digest = null) {
39
+ try {
40
+ const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
41
+ await fs.mkdir(checkpointDir, { recursive: true });
42
+ await fs.writeFile(
43
+ path.join(checkpointDir, `${dimId}.json`),
44
+ JSON.stringify({ dimId, sessionId, ...result, digest, completedAt: Date.now() }),
45
+ );
46
+ } catch (err) {
47
+ logger.warn(`[Bootstrap-v3] checkpoint save failed for "${dimId}": ${err.message}`);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 加载有效的 checkpoints
53
+ * @param {string} projectRoot
54
+ * @returns {Promise<Map<string, object>>} dimId → checkpoint data
55
+ */
56
+ async function loadCheckpoints(projectRoot) {
57
+ const checkpoints = new Map();
58
+ try {
59
+ const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
60
+ const files = await fs.readdir(checkpointDir).catch(() => []);
61
+ const now = Date.now();
62
+ for (const file of files) {
63
+ if (!file.endsWith('.json')) continue;
64
+ try {
65
+ const content = await fs.readFile(path.join(checkpointDir, file), 'utf-8');
66
+ const data = JSON.parse(content);
67
+ if (data.completedAt && (now - data.completedAt) < CHECKPOINT_TTL_MS) {
68
+ checkpoints.set(data.dimId, data);
69
+ }
70
+ } catch { /* skip corrupt checkpoint */ }
71
+ }
72
+ } catch { /* checkpoint dir doesn't exist */ }
73
+ return checkpoints;
74
+ }
75
+
76
+ /**
77
+ * 清理 checkpoint 目录
78
+ * @param {string} projectRoot
79
+ */
80
+ async function clearCheckpoints(projectRoot) {
81
+ try {
82
+ const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
83
+ await fs.rm(checkpointDir, { recursive: true, force: true });
84
+ } catch { /* ignore */ }
85
+ }
86
+
23
87
  // ──────────────────────────────────────────────────────────────────
24
88
  // v3.0 维度配置 (增加 focusAreas 用于 Analyst prompt)
25
89
  // ──────────────────────────────────────────────────────────────────
@@ -222,7 +286,7 @@ export async function fillDimensionsV3(fillContext) {
222
286
  // ═══════════════════════════════════════════════════════════
223
287
  // Step 2: 按维度分层执行 (Analyst → Gate → Producer)
224
288
  // ═══════════════════════════════════════════════════════════
225
- const concurrency = parseInt(process.env.ASD_PARALLEL_CONCURRENCY || '2', 10);
289
+ const concurrency = parseInt(process.env.ASD_PARALLEL_CONCURRENCY || '3', 10);
226
290
  const enableParallel = process.env.ASD_PARALLEL_BOOTSTRAP !== 'false';
227
291
  const scheduler = new TierScheduler();
228
292
 
@@ -233,13 +297,52 @@ export async function fillDimensionsV3(fillContext) {
233
297
 
234
298
  logger.info(`[Bootstrap-v3] Active dimensions: [${activeDimIds.join(', ')}], concurrency=${enableParallel ? concurrency : 1}`);
235
299
 
300
+ // ── P3: 断点续传 — 加载有效 checkpoints ──
301
+ const completedCheckpoints = await loadCheckpoints(projectRoot);
302
+ const skippedDims = [];
303
+ for (const [dimId, checkpoint] of completedCheckpoints) {
304
+ if (activeDimIds.includes(dimId)) {
305
+ // 恢复 DimensionContext 中的 digest
306
+ if (checkpoint.digest) {
307
+ dimContext.addDimensionDigest(dimId, checkpoint.digest);
308
+ }
309
+ taskManager?.markTaskCompleted(dimId, {
310
+ type: 'checkpoint-restored',
311
+ ...checkpoint,
312
+ });
313
+ skippedDims.push(dimId);
314
+ logger.info(`[Bootstrap-v3] ⏩ 跳过已完成维度 (checkpoint): "${dimId}"`);
315
+ }
316
+ }
317
+
236
318
  const candidateResults = { created: 0, failed: 0, errors: [] };
237
319
  const dimensionCandidates = {};
320
+ const dimensionStats = {}; // P4.2: 维度级统计
238
321
 
239
322
  /**
240
323
  * 执行单个维度: Analyst → Gate → Producer
241
324
  */
242
325
  async function executeDimension(dimId) {
326
+ // P3: 跳过已有 checkpoint 的维度
327
+ if (skippedDims.includes(dimId)) {
328
+ const cp = completedCheckpoints.get(dimId);
329
+ const cpResult = {
330
+ candidateCount: cp?.candidateCount || 0,
331
+ rejectedCount: cp?.rejectedCount || 0,
332
+ analysisChars: cp?.analysisChars || 0,
333
+ referencedFiles: cp?.referencedFiles || 0,
334
+ durationMs: cp?.durationMs || 0,
335
+ toolCallCount: cp?.toolCallCount || 0,
336
+ tokenUsage: cp?.tokenUsage || { input: 0, output: 0 },
337
+ skipped: true,
338
+ restoredFromCheckpoint: true,
339
+ };
340
+ // P4.2: 将恢复的维度也记入统计
341
+ dimensionStats[dimId] = cpResult;
342
+ candidateResults.created += cpResult.candidateCount;
343
+ return cpResult;
344
+ }
345
+
243
346
  const dim = dimensions.find(d => d.id === dimId);
244
347
  const v3Config = DIMENSION_CONFIGS_V3[dimId];
245
348
  if (!dim || !v3Config) {
@@ -270,7 +373,7 @@ export async function fillDimensionsV3(fillContext) {
270
373
  try {
271
374
  // ── Phase 1: Analyst ──
272
375
  const analysisReport = await Promise.race([
273
- analystAgent.analyze(dimConfig, projectInfo, { sessionId }),
376
+ analystAgent.analyze(dimConfig, projectInfo, { sessionId, dimensionContext: dimContext }),
274
377
  new Promise((_, reject) =>
275
378
  setTimeout(() => reject(new Error(`Analyst timeout for "${dimId}"`)), 180_000)),
276
379
  ]);
@@ -333,13 +436,32 @@ export async function fillDimensionsV3(fillContext) {
333
436
  toolCallCount: (analysisReport.metadata?.toolCallCount || 0) + (producerResult.toolCalls?.length || 0),
334
437
  });
335
438
 
336
- return {
439
+ // P4.1: 聚合 token 用量
440
+ const analystTokens = analysisReport.metadata?.tokenUsage || { input: 0, output: 0 };
441
+ const producerTokens = producerResult.tokenUsage || { input: 0, output: 0 };
442
+ const dimTokenUsage = {
443
+ input: (analystTokens.input || 0) + (producerTokens.input || 0),
444
+ output: (analystTokens.output || 0) + (producerTokens.output || 0),
445
+ };
446
+
447
+ const dimResult = {
337
448
  candidateCount: producerResult.candidateCount,
449
+ rejectedCount: producerResult.rejectedCount || 0,
338
450
  analysisChars: analysisReport.analysisText.length,
339
451
  referencedFiles: analysisReport.referencedFiles.length,
340
452
  durationMs: Date.now() - dimStartTime,
453
+ toolCallCount: (analysisReport.metadata?.toolCallCount || 0) + (producerResult.toolCalls?.length || 0),
454
+ tokenUsage: dimTokenUsage,
341
455
  };
342
456
 
457
+ // P4.2: 记录维度统计
458
+ dimensionStats[dimId] = dimResult;
459
+
460
+ // P3: 保存 checkpoint
461
+ await saveDimensionCheckpoint(projectRoot, sessionId, dimId, dimResult, digest);
462
+
463
+ return dimResult;
464
+
343
465
  } catch (err) {
344
466
  logger.error(`[Bootstrap-v3] Dimension "${dimId}" failed: ${err.message}`);
345
467
  candidateResults.errors.push({ dimId, error: err.message });
@@ -449,16 +571,83 @@ export async function fillDimensionsV3(fillContext) {
449
571
  }
450
572
 
451
573
  // ═══════════════════════════════════════════════════════════
452
- // Summary
574
+ // Summary + P4.2: Bootstrap Report
453
575
  // ═══════════════════════════════════════════════════════════
454
576
  const totalTimeMs = Date.now() - t0;
577
+
578
+ // P4.1: 汇总所有维度 token 用量
579
+ const totalTokenUsage = { input: 0, output: 0 };
580
+ const totalToolCalls = Object.values(dimensionStats).reduce((sum, s) => sum + (s.toolCallCount || 0), 0);
581
+ for (const stat of Object.values(dimensionStats)) {
582
+ if (stat.tokenUsage) {
583
+ totalTokenUsage.input += stat.tokenUsage.input || 0;
584
+ totalTokenUsage.output += stat.tokenUsage.output || 0;
585
+ }
586
+ }
587
+
455
588
  logger.info([
456
589
  `[Bootstrap-v3] ═══ Pipeline complete ═══`,
457
590
  ` Candidates: ${candidateResults.created} created, ${candidateResults.errors.length} errors`,
458
591
  ` Skills: ${skillResults.created} created, ${skillResults.failed} failed`,
459
592
  ` Time: ${totalTimeMs}ms (${(totalTimeMs / 1000).toFixed(1)}s)`,
460
593
  ` Mode: ${enableParallel ? `parallel (concurrency=${concurrency})` : 'serial'}`,
461
- ].join('\n'));
594
+ ` Tokens: input=${totalTokenUsage.input}, output=${totalTokenUsage.output}`,
595
+ ` Tool calls: ${totalToolCalls}`,
596
+ skippedDims.length > 0 ? ` Checkpoints restored: [${skippedDims.join(', ')}]` : '',
597
+ ].filter(Boolean).join('\n'));
598
+
599
+ // P4.2: 生成冷启动报告
600
+ try {
601
+ const report = {
602
+ version: '2.7.0',
603
+ timestamp: new Date().toISOString(),
604
+ project: {
605
+ name: projectInfo.name,
606
+ files: projectInfo.fileCount,
607
+ lang: projectInfo.lang,
608
+ },
609
+ duration: {
610
+ totalMs: totalTimeMs,
611
+ totalSec: Math.round(totalTimeMs / 1000),
612
+ },
613
+ dimensions: {},
614
+ totals: {
615
+ candidates: candidateResults.created,
616
+ skills: skillResults.created,
617
+ toolCalls: totalToolCalls,
618
+ tokenUsage: totalTokenUsage,
619
+ errors: candidateResults.errors.length,
620
+ },
621
+ checkpoints: {
622
+ restored: skippedDims,
623
+ },
624
+ };
625
+
626
+ for (const [dimId, stat] of Object.entries(dimensionStats)) {
627
+ report.dimensions[dimId] = {
628
+ candidatesSubmitted: stat.candidateCount || 0,
629
+ candidatesRejected: stat.rejectedCount || 0,
630
+ analysisChars: stat.analysisChars || 0,
631
+ referencedFiles: stat.referencedFiles || 0,
632
+ durationMs: stat.durationMs || 0,
633
+ toolCallCount: stat.toolCallCount || 0,
634
+ tokenUsage: stat.tokenUsage || { input: 0, output: 0 },
635
+ };
636
+ }
637
+
638
+ const reportDir = path.join(projectRoot, '.autosnippet');
639
+ await fs.mkdir(reportDir, { recursive: true });
640
+ await fs.writeFile(
641
+ path.join(reportDir, 'bootstrap-report.json'),
642
+ JSON.stringify(report, null, 2),
643
+ );
644
+ logger.info(`[Bootstrap-v3] 📊 Bootstrap report saved to .autosnippet/bootstrap-report.json`);
645
+ } catch (reportErr) {
646
+ logger.warn(`[Bootstrap-v3] Bootstrap report generation failed: ${reportErr.message}`);
647
+ }
648
+
649
+ // P3: 成功完成后清理 checkpoints
650
+ await clearCheckpoints(projectRoot);
462
651
 
463
652
  // 释放文件缓存
464
653
  allFiles = null;
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * TierScheduler.js — 维度分层并行调度器
3
3
  *
4
- * 按维度间信息依赖关系分 4 层执行:
4
+ * 按维度间信息依赖关系分 3 层执行:
5
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) — 依赖全部
6
+ * - Tier 2: 规范+架构+模式 (code-standard, architecture, code-pattern) — 依赖 Tier 1
7
+ * - Tier 3: 流转+实践+总结 (event-and-data-flow, best-practice, agent-guidelines) — 依赖 Tier 2
9
8
  *
10
9
  * 每层内部可并行 (受 concurrency 限制),层间串行。
11
10
  *
@@ -21,10 +20,9 @@ const logger = Logger.getInstance();
21
20
  // ──────────────────────────────────────────────────────────────────
22
21
 
23
22
  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: 总结
23
+ ['project-profile', 'objc-deep-scan', 'category-scan'], // Tier 1: 基础数据
24
+ ['code-standard', 'architecture', 'code-pattern'], // Tier 2: 规范+架构+模式
25
+ ['event-and-data-flow', 'best-practice', 'agent-guidelines'], // Tier 3: 流转+实践+总结
28
26
  ];
29
27
 
30
28
  // ──────────────────────────────────────────────────────────────────
@@ -79,13 +77,13 @@ export class TierScheduler {
79
77
  *
80
78
  * @param {Function} executeDimension — async (dimId) => DimensionResult
81
79
  * @param {object} [options]
82
- * @param {number} [options.concurrency=2] — Tier 内最大并行数
80
+ * @param {number} [options.concurrency=3] — Tier 内最大并行数
83
81
  * @param {Function} [options.onTierComplete] — (tierIndex, tierResults) => void
84
82
  * @param {Function} [options.shouldAbort] — () => boolean — 外部中止信号
85
83
  * @returns {Promise<Map<string, any>>} — dimId → result
86
84
  */
87
85
  async execute(executeDimension, options = {}) {
88
- const { concurrency = 2, onTierComplete, shouldAbort } = options;
86
+ const { concurrency = 3, onTierComplete, shouldAbort } = options;
89
87
  const results = new Map();
90
88
 
91
89
  for (let tierIndex = 0; tierIndex < this.#tiers.length; tierIndex++) {
@@ -41,6 +41,7 @@ import { envelope } from '../envelope.js';
41
41
  import { inferLang, detectPrimaryLanguage, buildLanguageExtension } from './LanguageExtensions.js';
42
42
  import { inferTargetRole, inferFilePriority } from './TargetClassifier.js';
43
43
  import { analyzeProject, generateContextForAgent, isAvailable as astIsAvailable } from '../../../core/AstAnalyzer.js';
44
+ import pathGuard from '../../../shared/PathGuard.js';
44
45
 
45
46
  // ── Sub-modules ──
46
47
  import { loadBootstrapSkills, extractSkillDimensionGuides, enhanceDimensions } from './bootstrap/skills.js';
@@ -67,6 +68,13 @@ export { loadBootstrapSkills };
67
68
  export async function bootstrapKnowledge(ctx, args) {
68
69
  const t0 = Date.now();
69
70
  const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd();
71
+
72
+ // 路径安全守卫 — 确保所有写操作限制在项目目录内
73
+ if (!pathGuard.configured) {
74
+ const { default: Bootstrap } = await import('../../../bootstrap.js');
75
+ Bootstrap.configurePathGuard(projectRoot);
76
+ }
77
+
70
78
  const maxFiles = args.maxFiles || 500;
71
79
  const skipGuard = args.skipGuard || false;
72
80
  const contentMaxLines = args.contentMaxLines || 120;