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
@@ -154,6 +154,47 @@ export class CandidateService {
154
154
  }
155
155
  }
156
156
 
157
+ /**
158
+ * 删除候选项(DB + .md 文件)
159
+ */
160
+ async deleteCandidate(candidateId, context = {}) {
161
+ try {
162
+ const candidate = await this.candidateRepository.findById(candidateId);
163
+
164
+ // 先删 .md 文件(即使 DB 中不存在也尝试按 id 清理文件)
165
+ if (this.fileWriter && candidate) {
166
+ this.fileWriter.removeCandidate(candidate);
167
+ }
168
+
169
+ // 删除 DB 记录
170
+ const deleted = await this.candidateRepository.delete(candidateId);
171
+
172
+ // 审计日志
173
+ if (deleted) {
174
+ await this.auditLogger.log({
175
+ action: 'delete_candidate',
176
+ resource: `candidate:${candidateId}`,
177
+ actor: context.userId || 'system',
178
+ result: 'success',
179
+ });
180
+ }
181
+
182
+ this.logger.info('Candidate deleted', {
183
+ candidateId,
184
+ dbDeleted: deleted,
185
+ fileRemoved: !!candidate,
186
+ });
187
+
188
+ return deleted;
189
+ } catch (error) {
190
+ this.logger.error('Error deleting candidate', {
191
+ candidateId,
192
+ error: error.message,
193
+ });
194
+ throw error;
195
+ }
196
+ }
197
+
157
198
  /**
158
199
  * 驳回候选项
159
200
  */
@@ -543,6 +584,8 @@ export class CandidateService {
543
584
  throw new ValidationError('AI provider with chat capability is required');
544
585
  }
545
586
 
587
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
588
+
546
589
  // 1. 收集候选
547
590
  let candidates;
548
591
  if (options.candidateIds?.length) {
@@ -564,6 +607,12 @@ export class CandidateService {
564
607
  return { refined: 0, total: 0, errors: [], results: [] };
565
608
  }
566
609
 
610
+ // 通知:润色开始
611
+ onProgress?.('refine:started', {
612
+ total: candidates.length,
613
+ candidateIds: candidates.map(c => c.id),
614
+ });
615
+
567
616
  // 2. 收集同批次标题列表(供 AI 推断关系)
568
617
  const allTitles = candidates.map(c => (c.metadata || {}).title || '').filter(Boolean);
569
618
 
@@ -571,22 +620,46 @@ export class CandidateService {
571
620
  const results = [];
572
621
  const errors = [];
573
622
  let refined = 0;
623
+ let processed = 0;
574
624
 
575
625
  for (const candidate of candidates) {
626
+ processed++;
627
+ const title = (candidate.metadata || {}).title || '';
628
+
629
+ // 通知:开始处理当前候选
630
+ onProgress?.('refine:item-started', {
631
+ candidateId: candidate.id,
632
+ title,
633
+ current: processed,
634
+ total: candidates.length,
635
+ progress: Math.round(((processed - 1) / candidates.length) * 100),
636
+ });
637
+
576
638
  try {
577
639
  const meta = { ...(candidate.metadata || {}) };
578
- const title = meta.title || '';
579
640
  const prompt = this._buildRefinePrompt(candidate, allTitles, options.userPrompt);
580
641
  const response = await aiProvider.chat(prompt, { temperature: 0.3 });
581
642
  const parsed = aiProvider.extractJSON(response, '{', '}');
582
643
 
583
644
  if (!parsed) {
584
645
  errors.push({ id: candidate.id, title, error: 'AI returned no valid JSON' });
646
+ onProgress?.('refine:item-failed', {
647
+ candidateId: candidate.id, title,
648
+ error: 'AI returned no valid JSON',
649
+ current: processed, total: candidates.length,
650
+ progress: Math.round((processed / candidates.length) * 100),
651
+ });
585
652
  continue;
586
653
  }
587
654
 
588
655
  if (options.dryRun) {
589
656
  results.push({ id: candidate.id, title, preview: parsed });
657
+ onProgress?.('refine:item-completed', {
658
+ candidateId: candidate.id, title, refined: false,
659
+ current: processed, total: candidates.length,
660
+ progress: Math.round((processed / candidates.length) * 100),
661
+ refinedSoFar: refined,
662
+ });
590
663
  continue;
591
664
  }
592
665
 
@@ -624,9 +697,16 @@ export class CandidateService {
624
697
  }
625
698
  }
626
699
 
627
- // 更新 code(如果 AI 改写了文档)
700
+ // 更新 code(如果 AI 改写了文档 — 增强校验防止截断/片段代码/类型变更)
628
701
  let newCode = candidate.code;
629
- if (parsed.code && parsed.code.length > 50 && parsed.code !== candidate.code) {
702
+ const origCode = candidate.code || '';
703
+ const origLen = origCode.length;
704
+ const isOrigMarkdown = /^---\s*\n/.test(origCode) || /^#\s+/.test(origCode) || (origCode.match(/^#{1,3}\s+/gm) || []).length >= 2;
705
+ const isNewMarkdown = parsed.code && (/^---\s*\n/.test(parsed.code) || /^#\s+/.test(parsed.code) || (parsed.code.match(/^#{1,3}\s+/gm) || []).length >= 2);
706
+ const codeTypeChanged = !isOrigMarkdown && isNewMarkdown;
707
+ if (parsed.code && parsed.code.length > 50 && parsed.code !== candidate.code
708
+ && parsed.code.length >= origLen * 0.4 // 不能太短(防止 AI 返回截断片段)
709
+ && !codeTypeChanged) { // 不允许源代码 → Markdown 类型变更
630
710
  newCode = parsed.code;
631
711
  changed = true;
632
712
  }
@@ -645,11 +725,39 @@ export class CandidateService {
645
725
  }
646
726
 
647
727
  results.push({ id: candidate.id, title, refined: changed, fields: Object.keys(parsed) });
728
+
729
+ // 通知:当前候选完成
730
+ onProgress?.('refine:item-completed', {
731
+ candidateId: candidate.id,
732
+ title,
733
+ refined: changed,
734
+ current: processed,
735
+ total: candidates.length,
736
+ progress: Math.round((processed / candidates.length) * 100),
737
+ refinedSoFar: refined,
738
+ });
648
739
  } catch (err) {
649
740
  errors.push({ id: candidate.id, title: (candidate.metadata || {}).title, error: err.message });
741
+
742
+ // 通知:当前候选失败
743
+ onProgress?.('refine:item-failed', {
744
+ candidateId: candidate.id,
745
+ title,
746
+ error: err.message,
747
+ current: processed,
748
+ total: candidates.length,
749
+ progress: Math.round((processed / candidates.length) * 100),
750
+ });
650
751
  }
651
752
  }
652
753
 
754
+ // 通知:全部完成
755
+ onProgress?.('refine:completed', {
756
+ total: candidates.length,
757
+ refined,
758
+ failed: errors.length,
759
+ });
760
+
653
761
  // 4. 审计
654
762
  await this.auditLogger.log({
655
763
  action: 'refine_bootstrap_candidates',
@@ -669,10 +777,47 @@ export class CandidateService {
669
777
  _buildRefinePrompt(candidate, allTitles, userPrompt = '') {
670
778
  const meta = candidate.metadata || {};
671
779
  const otherTitles = allTitles.filter(t => t !== meta.title).map(t => ` - ${t}`).join('\n');
780
+ const code = (candidate.code || '');
781
+
782
+ // ── 检测 code 字段是源代码还是 Markdown 文档 ──
783
+ const isMarkdownDoc = /^---\s*\n/.test(code) // frontmatter
784
+ || /^#\s+/.test(code) // 以 # 标题开头
785
+ || (code.match(/^#{1,3}\s+/gm) || []).length >= 2; // 含 ≥2 个 Markdown 标题
786
+ const codeContentType = isMarkdownDoc ? 'markdown-document' : 'source-code';
787
+
788
+ // ── 用户指令段落 ──
789
+ let userSection = '';
790
+ if (userPrompt) {
791
+ userSection = `
792
+ # User Instructions (HIGHEST PRIORITY — STRICTLY follow these)
793
+ ${userPrompt}
794
+
795
+ ## Scope Restriction
796
+ - ONLY modify fields that are DIRECTLY related to the user instruction above.
797
+ - Do NOT touch or return any field that the user did not ask to change.
798
+ - For example, if user says "增加使用案例", ONLY return the field where usage examples belong (e.g. "code" for markdown docs, or "agentNotes" for source-code candidates). Do NOT return "summary", "confidence", "tags", "insight", or "relations" unless explicitly requested.
799
+ - If you are unsure which field a user instruction maps to, prefer "agentNotes" or "code" (for markdown docs).
800
+ `;
801
+ }
672
802
 
673
- const userSection = userPrompt
674
- ? `\n# User Instructions (IMPORTANT — follow these with highest priority)\n${userPrompt}\n`
675
- : '';
803
+ // ── code 字段的任务描述根据内容类型而不同 ──
804
+ let codeTask;
805
+ if (codeContentType === 'source-code') {
806
+ codeTask = `7. **code**: The content field contains RAW SOURCE CODE (not a Markdown document).
807
+ - Do NOT convert source code into a Markdown document — that breaks the candidate.
808
+ - If the user asks to add usage examples, documentation notes or explanations,
809
+ put them in "agentNotes" (array of strings) instead of modifying "code".
810
+ - Only modify "code" if the user explicitly asks to change the source code itself
811
+ (e.g. fix a bug, add comments, refactor). Return the COMPLETE source code.
812
+ - If no code change is needed, OMIT this field entirely.`;
813
+ } else {
814
+ codeTask = `7. **code**: The content field is a Markdown document.
815
+ - If improvements are needed, return the COMPLETE improved Markdown document.
816
+ - CRITICAL: You MUST return the ENTIRE document content, including ALL sections.
817
+ - Do NOT return only source code fragments or partial snippets.
818
+ - The code blocks inside the Markdown should contain PURE source code.
819
+ - If no code improvement is needed, OMIT this field entirely.`;
820
+ }
676
821
 
677
822
  return `# Role
678
823
  You are a senior software architect refining a Bootstrap knowledge candidate.\n${userSection}
@@ -683,10 +828,11 @@ Category: ${candidate.category || 'bootstrap'}
683
828
  Language: ${candidate.language || 'unknown'}
684
829
  Summary: ${meta.summary || '(none)'}
685
830
  Tags: ${(meta.tags || []).join(', ')}
831
+ Content Type: ${codeContentType}
686
832
 
687
- ## Content (Markdown document)
833
+ ## Content
688
834
  \`\`\`
689
- ${(candidate.code || '').substring(0, 3000)}
835
+ ${code.substring(0, 3000)}
690
836
  \`\`\`
691
837
 
692
838
  # Sibling Candidates (same bootstrap batch)
@@ -699,10 +845,10 @@ ${otherTitles || '(none)'}
699
845
  4. **relations**: Array of cross-references to sibling candidates: [{ "type": "DEPENDS_ON|EXTENDS|RELATED|CONFLICTS|ENFORCES|PREREQUISITE", "target": "<exact title from siblings>", "description": "<why>" }]
700
846
  5. **confidence**: Float 0-1 rating of this candidate's value (0.3=low, 0.6=medium, 0.9=high)
701
847
  6. **tags**: Additional relevant tags (array of strings, optional)
702
- 7. **code**: If the Markdown document can be improved, return the full improved version. Otherwise omit this field.
848
+ ${codeTask}
703
849
 
704
850
  # Output
705
- Return a single JSON object with the fields above. Only include fields you want to change.
851
+ Return a single JSON object. Only include fields you want to change.${userPrompt ? '\nREMINDER: Only return fields DIRECTLY related to the user instruction. Omit all others!' : ''}
706
852
  Do NOT wrap in markdown code blocks. Return raw JSON only.`;
707
853
  }
708
854
 
@@ -0,0 +1,216 @@
1
+ /**
2
+ * AnalystAgent.js — v3.0 分析者 Agent
3
+ *
4
+ * 职责:
5
+ * - 使用 AST 工具 + 文件搜索工具自由探索代码库
6
+ * - 输出自然语言分析结果 (无格式约束)
7
+ * - 不提交候选、不关心格式
8
+ *
9
+ * 设计哲学:
10
+ * "给 AI 一个任务描述和一套好工具,让它像资深工程师一样自由探索代码库。"
11
+ *
12
+ * @module AnalystAgent
13
+ */
14
+
15
+ import { buildAnalysisReport, analysisQualityGate, buildRetryPrompt } from './HandoffProtocol.js';
16
+ import Logger from '../../infrastructure/logging/Logger.js';
17
+
18
+ // ──────────────────────────────────────────────────────────────────
19
+ // System Prompt — Analyst 专用 (~100 tokens)
20
+ // ──────────────────────────────────────────────────────────────────
21
+
22
+ const ANALYST_SYSTEM_PROMPT = `你是一名高级软件架构师,正在分析一个代码项目。
23
+ 使用提供的工具深入探索代码结构,理解设计决策背后的原因。
24
+ 输出你的分析发现,包括具体的文件路径和代码位置。
25
+ 不需要任何特定格式,用自然语言描述你的理解即可。
26
+ 尽可能多地使用工具来获取准确信息,不要猜测。`;
27
+
28
+ // ──────────────────────────────────────────────────────────────────
29
+ // Analyst 可用工具白名单 — 只做探索,不做提交
30
+ // ──────────────────────────────────────────────────────────────────
31
+
32
+ const ANALYST_TOOLS = [
33
+ // AST 结构化分析
34
+ 'get_project_overview',
35
+ 'get_class_hierarchy',
36
+ 'get_class_info',
37
+ 'get_protocol_info',
38
+ 'get_method_overrides',
39
+ 'get_category_map',
40
+ // 文件访问
41
+ 'search_project_code',
42
+ 'read_project_file',
43
+ 'list_project_structure',
44
+ 'get_file_summary',
45
+ 'semantic_search_code',
46
+ // 前序上下文 (可选)
47
+ 'get_previous_analysis',
48
+ ];
49
+
50
+ // ──────────────────────────────────────────────────────────────────
51
+ // Analyst 预算 — 自由探索,不需要 PhaseRouter
52
+ // ──────────────────────────────────────────────────────────────────
53
+
54
+ const ANALYST_BUDGET = {
55
+ maxIterations: 20,
56
+ searchBudget: 15, // 探索为主,给更多搜索预算
57
+ searchBudgetGrace: 10,
58
+ maxSubmits: 0, // Analyst 不提交候选
59
+ softSubmitLimit: 0,
60
+ idleRoundsToExit: 3,
61
+ };
62
+
63
+ // ──────────────────────────────────────────────────────────────────
64
+ // 维度 Prompt 模板
65
+ // ──────────────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * 构建 Analyst Prompt
69
+ * @param {object} dimConfig — 维度配置 { id, label, guide, focusAreas }
70
+ * @param {object} projectInfo — { name, lang, fileCount }
71
+ * @returns {string}
72
+ */
73
+ function buildAnalystPrompt(dimConfig, projectInfo) {
74
+ const parts = [];
75
+
76
+ // §1 任务描述
77
+ parts.push(`分析项目 ${projectInfo.name} (${projectInfo.lang}, ${projectInfo.fileCount} 个文件) 的 ${dimConfig.label}。`);
78
+
79
+ // §2 维度指引
80
+ if (dimConfig.guide) {
81
+ parts.push(dimConfig.guide);
82
+ }
83
+
84
+ // §3 探索焦点
85
+ if (dimConfig.focusAreas?.length > 0) {
86
+ parts.push(`重点关注:\n${dimConfig.focusAreas.map(f => `- ${f}`).join('\n')}`);
87
+ }
88
+
89
+ // §4 输出要求
90
+ const outputType = dimConfig.outputType || 'analysis';
91
+ const needsCandidates = outputType === 'dual' || outputType === 'candidate';
92
+ const depthHint = needsCandidates
93
+ ? '你的分析将被转化为知识候选,请确保每个发现都有足够的代码证据和文件引用。目标: 发现 3-5 个独立的知识点。'
94
+ : '';
95
+
96
+ parts.push(`请将分析组织成结构化段落,包含:
97
+ 1. 在哪些文件/类中发现 (写出具体文件路径)
98
+ 2. 具体的实现方式和代码特征
99
+ 3. 为什么选择这种方式(设计意图)
100
+ 4. 统计数据 (如数量、占比)
101
+
102
+ 每个关键发现用编号列表呈现,引用 3 个以上具体文件。
103
+ ${depthHint}
104
+ 重要: 务必使用 read_project_file 阅读代码确认,不要假设文件存在。引用的每个文件路径都必须是你亲眼看到的。`);
105
+
106
+ // §5 前序上下文提示
107
+ parts.push('可以调用 get_previous_analysis 获取前序维度的分析结果,避免重复分析。');
108
+
109
+ return parts.join('\n\n');
110
+ }
111
+
112
+ // ──────────────────────────────────────────────────────────────────
113
+ // AnalystAgent 类
114
+ // ──────────────────────────────────────────────────────────────────
115
+
116
+ export class AnalystAgent {
117
+ /** @type {import('./ChatAgent.js').ChatAgent} */
118
+ #chatAgent;
119
+
120
+ /** @type {import('../../core/ast/ProjectGraph.js').default} */
121
+ #projectGraph;
122
+
123
+ /** @type {import('../../infrastructure/logging/Logger.js').default} */
124
+ #logger;
125
+
126
+ /** @type {number} Gate 最大重试次数 */
127
+ #maxRetries;
128
+
129
+ /**
130
+ * @param {object} chatAgent — ChatAgent 实例
131
+ * @param {object} [projectGraph] — ProjectGraph 实例
132
+ * @param {object} [options]
133
+ * @param {number} [options.maxRetries=1] — Gate 失败最大重试次数
134
+ */
135
+ constructor(chatAgent, projectGraph = null, options = {}) {
136
+ this.#chatAgent = chatAgent;
137
+ this.#projectGraph = projectGraph;
138
+ this.#logger = Logger.getInstance();
139
+ this.#maxRetries = options.maxRetries ?? 1;
140
+ }
141
+
142
+ /**
143
+ * 分析指定维度
144
+ *
145
+ * @param {object} dimConfig — 维度配置 { id, label, guide, focusAreas }
146
+ * @param {object} projectInfo — { name, lang, fileCount }
147
+ * @param {object} [options]
148
+ * @param {string} [options.sessionId] — Bootstrap session ID
149
+ * @returns {Promise<import('./HandoffProtocol.js').AnalysisReport>}
150
+ */
151
+ async analyze(dimConfig, projectInfo, options = {}) {
152
+ const dimId = dimConfig.id;
153
+ const prompt = buildAnalystPrompt(dimConfig, projectInfo);
154
+
155
+ this.#logger.info(`[AnalystAgent] ▶ analyzing dimension "${dimId}" — prompt ${prompt.length} chars`);
156
+
157
+ let retries = 0;
158
+ let lastReport = null;
159
+
160
+ while (retries <= this.#maxRetries) {
161
+ const execPrompt = retries === 0
162
+ ? prompt
163
+ : prompt + '\n\n' + buildRetryPrompt(lastReport?._gateReason || 'Analysis too short');
164
+
165
+ try {
166
+ const result = await this.#chatAgent.execute(execPrompt, {
167
+ source: 'system',
168
+ conversationId: options.sessionId ? `analyst-${options.sessionId}-${dimId}` : undefined,
169
+ budget: ANALYST_BUDGET,
170
+ systemPromptOverride: ANALYST_SYSTEM_PROMPT,
171
+ allowedTools: ANALYST_TOOLS,
172
+ disablePhaseRouter: true,
173
+ temperature: 0.4,
174
+ dimensionMeta: {
175
+ id: dimId,
176
+ outputType: 'analysis',
177
+ allowedKnowledgeTypes: dimConfig.allowedKnowledgeTypes || [],
178
+ },
179
+ });
180
+
181
+ // 构建 AnalysisReport
182
+ const report = buildAnalysisReport(result, dimId, this.#projectGraph);
183
+
184
+ // 质量门控 — 传入 outputType 以调整门槛
185
+ const gate = analysisQualityGate(report, { outputType: dimConfig.outputType || 'analysis' });
186
+ if (gate.pass) {
187
+ this.#logger.info(`[AnalystAgent] ✅ dimension "${dimId}" — ${report.analysisText.length} chars, ${report.referencedFiles.length} files referenced, ${report.metadata.toolCallCount} tool calls`);
188
+ return report;
189
+ }
190
+
191
+ this.#logger.warn(`[AnalystAgent] ⚠ Gate failed for "${dimId}": ${gate.reason} (action=${gate.action})`);
192
+
193
+ if (gate.action === 'degrade') {
194
+ // 直接降级 — 不重试
195
+ report._gateResult = gate;
196
+ return report;
197
+ }
198
+
199
+ // retry
200
+ lastReport = report;
201
+ lastReport._gateReason = gate.reason;
202
+ retries++;
203
+ } catch (err) {
204
+ this.#logger.error(`[AnalystAgent] ❌ dimension "${dimId}" error: ${err.message}`);
205
+ // 返回空 report
206
+ return buildAnalysisReport({ reply: '', toolCalls: [] }, dimId, this.#projectGraph);
207
+ }
208
+ }
209
+
210
+ // 重试耗尽 — 返回最后一次结果
211
+ this.#logger.warn(`[AnalystAgent] Retries exhausted for "${dimId}" — returning last report`);
212
+ return lastReport || buildAnalysisReport({ reply: '', toolCalls: [] }, dimId, this.#projectGraph);
213
+ }
214
+ }
215
+
216
+ export default AnalystAgent;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * CandidateGuardrail.js — Producer 产出的候选验证链
3
+ *
4
+ * 三层验证:
5
+ * 1. 结构验证 — 必填字段、内容长度、knowledgeType 约束
6
+ * 2. 去重验证 — 标题不重复
7
+ * 3. 质量启发式 — 包含代码引用、项目特定内容
8
+ *
9
+ * @module CandidateGuardrail
10
+ */
11
+
12
+ export class CandidateGuardrail {
13
+ /** @type {Set<string>} 已提交标题 (小写) */
14
+ #globalTitles;
15
+
16
+ /** @type {object} 维度配置 */
17
+ #dimensionConfig;
18
+
19
+ /**
20
+ * @param {Set<string>} globalTitles — 全局已提交标题集合 (小写)
21
+ * @param {object} dimensionConfig — { allowedKnowledgeTypes, id, outputType }
22
+ */
23
+ constructor(globalTitles, dimensionConfig) {
24
+ this.#globalTitles = globalTitles;
25
+ this.#dimensionConfig = dimensionConfig;
26
+ }
27
+
28
+ /**
29
+ * 验证候选结构
30
+ * @param {object} candidate — submit_candidate 工具参数
31
+ * @returns {{ valid: boolean, error?: string }}
32
+ */
33
+ validateStructure(candidate) {
34
+ // 必填字段检查
35
+ const required = ['title', 'code'];
36
+ for (const field of required) {
37
+ if (!candidate[field] || String(candidate[field]).trim().length === 0) {
38
+ return { valid: false, error: `缺少必填字段: ${field}` };
39
+ }
40
+ }
41
+
42
+ // 内容长度检查 — 「项目特写」需要足够的代码和描述
43
+ const content = candidate.code || '';
44
+ if (content.length < 200) {
45
+ return { valid: false, error: `内容过短 (${content.length} 字符, 最少 200)。请包含代码片段和项目上下文描述,而非一句话概括。` };
46
+ }
47
+
48
+ // knowledgeType 约束
49
+ const allowed = this.#dimensionConfig.allowedKnowledgeTypes;
50
+ if (allowed?.length > 0 && candidate.knowledgeType) {
51
+ if (!allowed.includes(candidate.knowledgeType)) {
52
+ return { valid: false, error: `knowledgeType "${candidate.knowledgeType}" 不在允许列表: [${allowed}]` };
53
+ }
54
+ }
55
+
56
+ return { valid: true };
57
+ }
58
+
59
+ /**
60
+ * 验证去重
61
+ * @param {object} candidate
62
+ * @returns {{ valid: boolean, error?: string }}
63
+ */
64
+ validateUniqueness(candidate) {
65
+ const normalizedTitle = (candidate.title || '').toLowerCase().trim();
66
+ if (this.#globalTitles.has(normalizedTitle)) {
67
+ return { valid: false, error: `标题重复: "${candidate.title}"` };
68
+ }
69
+ return { valid: true };
70
+ }
71
+
72
+ /**
73
+ * 质量启发式检查
74
+ * @param {object} candidate
75
+ * @returns {{ valid: boolean, error?: string, warning?: string }}
76
+ */
77
+ validateQuality(candidate) {
78
+ const content = candidate.code || '';
79
+
80
+ // 检查是否包含代码引用或文件路径
81
+ const hasCodeBlock = /```[\s\S]*?```/.test(content) || /\.(m|h|swift|js|ts)(:\d+)?/.test(content);
82
+ const hasSourceRef = /\(来源[::]/.test(content) || /\bFileName\b/.test(content) === false && /[A-Z]\w+\.(m|h|swift|java|kt|js|ts)/.test(content);
83
+
84
+ if (!hasCodeBlock && !hasSourceRef) {
85
+ return { valid: false, error: '内容缺少代码片段或文件引用 — 请用 read_project_file 获取代码后再提交,「项目特写」必须包含真实代码' };
86
+ }
87
+
88
+ // 检查是否是 Skill 摘要式内容(一行式描述、无代码、无结构)
89
+ const lines = content.split('\n').filter(l => l.trim().length > 0);
90
+ if (lines.length <= 2 && !hasCodeBlock) {
91
+ return { valid: false, error: `内容过于简单 (仅 ${lines.length} 行) — 请包含代码片段、设计意图和项目上下文,不要只写一句话概括` };
92
+ }
93
+
94
+ // 检查是否是通用知识而非项目特定
95
+ const genericPatterns = [
96
+ /^(Singleton|Factory|Observer|MVC|MVVM) (pattern|模式)$/i,
97
+ ];
98
+ const title = candidate.title || '';
99
+ if (genericPatterns.some(p => p.test(title.trim()))) {
100
+ return { valid: false, error: `标题过于通用: "${title}" — 请加上项目特定的上下文` };
101
+ }
102
+
103
+ return { valid: true };
104
+ }
105
+
106
+ /**
107
+ * 完整验证链
108
+ * @param {object} candidate
109
+ * @returns {{ valid: boolean, error?: string, warning?: string }}
110
+ */
111
+ validate(candidate) {
112
+ const structureResult = this.validateStructure(candidate);
113
+ if (!structureResult.valid) return structureResult;
114
+
115
+ const uniqueResult = this.validateUniqueness(candidate);
116
+ if (!uniqueResult.valid) return uniqueResult;
117
+
118
+ const qualityResult = this.validateQuality(candidate);
119
+ // 质量问题返回 warning 但不阻止提交
120
+ if (!qualityResult.valid) return qualityResult;
121
+
122
+ return { valid: true, warning: qualityResult.warning };
123
+ }
124
+
125
+ /**
126
+ * 记录已提交标题(提交成功后调用)
127
+ * @param {string} title
128
+ */
129
+ recordTitle(title) {
130
+ this.#globalTitles.add((title || '').toLowerCase().trim());
131
+ }
132
+ }
133
+
134
+ export default CandidateGuardrail;