autosnippet 3.1.4 → 3.1.6

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 (68) hide show
  1. package/lib/cli/SetupService.js +7 -1
  2. package/lib/cli/UpgradeService.js +4 -0
  3. package/lib/core/enhancement/EnhancementPack.js +11 -0
  4. package/lib/core/enhancement/android-enhancement.js +2 -0
  5. package/lib/core/enhancement/django-enhancement.js +3 -0
  6. package/lib/core/enhancement/fastapi-enhancement.js +3 -0
  7. package/lib/core/enhancement/go-grpc-enhancement.js +2 -0
  8. package/lib/core/enhancement/go-web-enhancement.js +2 -0
  9. package/lib/core/enhancement/langchain-enhancement.js +3 -0
  10. package/lib/core/enhancement/ml-enhancement.js +3 -0
  11. package/lib/core/enhancement/nextjs-enhancement.js +3 -0
  12. package/lib/core/enhancement/node-server-enhancement.js +3 -0
  13. package/lib/core/enhancement/react-enhancement.js +4 -0
  14. package/lib/core/enhancement/rust-tokio-enhancement.js +2 -0
  15. package/lib/core/enhancement/rust-web-enhancement.js +2 -0
  16. package/lib/core/enhancement/spring-enhancement.js +2 -0
  17. package/lib/core/enhancement/vue-enhancement.js +3 -0
  18. package/lib/external/mcp/McpServer.js +38 -4
  19. package/lib/external/mcp/autoApproveInjector.js +154 -0
  20. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +251 -0
  21. package/lib/external/mcp/handlers/bootstrap/ExternalSubmissionTracker.js +397 -0
  22. package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +578 -0
  23. package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +11 -6
  24. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +10 -6
  25. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +1 -1
  26. package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +5 -0
  27. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -2
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +5 -2
  29. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +1 -1
  30. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +41 -79
  31. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +22 -5
  32. package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +639 -0
  33. package/lib/external/mcp/handlers/bootstrap/shared/dimension-sop.js +670 -0
  34. package/lib/external/mcp/handlers/bootstrap/shared/dimension-text.js +253 -0
  35. package/lib/external/mcp/handlers/bootstrap/shared/skill-generator.js +263 -0
  36. package/lib/external/mcp/handlers/bootstrap/skills.js +2 -2
  37. package/lib/external/mcp/handlers/bootstrap-external.js +177 -0
  38. package/lib/external/mcp/handlers/{bootstrap.js → bootstrap-internal.js} +28 -31
  39. package/lib/external/mcp/handlers/consolidated.js +55 -43
  40. package/lib/external/mcp/handlers/dimension-complete-external.js +370 -0
  41. package/lib/external/mcp/handlers/knowledge.js +35 -4
  42. package/lib/external/mcp/handlers/system.js +13 -2
  43. package/lib/external/mcp/handlers/wiki-external.js +516 -0
  44. package/lib/external/mcp/tools.js +98 -42
  45. package/lib/http/routes/candidates.js +1 -1
  46. package/lib/platform/ios/spm/SpmService.js +1 -1
  47. package/lib/platform/ios/xcode/XcodeIntegration.js +2 -2
  48. package/lib/service/candidate/CandidateAggregator.js +52 -0
  49. package/lib/service/chat/AnalystAgent.js +7 -3
  50. package/lib/service/chat/ChatAgent.js +1 -1
  51. package/lib/service/chat/EvidenceCollector.js +500 -0
  52. package/lib/service/chat/HandoffProtocol.js +222 -1
  53. package/lib/service/chat/ProducerAgent.js +142 -12
  54. package/lib/service/chat/tools/ai-analysis.js +3 -3
  55. package/lib/service/chat/tools/ast-graph.js +2 -2
  56. package/lib/service/chat/tools/infrastructure.js +1 -1
  57. package/lib/service/module/ModuleService.js +1 -1
  58. package/lib/service/recipe/RecipeCandidateValidator.js +5 -0
  59. package/lib/shared/RecipeReadinessChecker.js +15 -0
  60. package/package.json +1 -1
  61. package/scripts/migrate-md-to-knowledge.mjs +5 -2
  62. package/skills/autosnippet-analysis/SKILL.md +1 -1
  63. package/skills/autosnippet-candidates/SKILL.md +2 -2
  64. package/skills/autosnippet-coldstart/SKILL.md +3 -0
  65. package/skills/autosnippet-concepts/SKILL.md +8 -2
  66. package/skills/autosnippet-create/SKILL.md +2 -2
  67. package/skills/autosnippet-recipes/SKILL.md +1 -1
  68. package/lib/external/mcp/handlers/wiki.js +0 -271
@@ -0,0 +1,251 @@
1
+ /**
2
+ * BootstrapSession — 外部 Agent 驱动的 Bootstrap 会话状态管理
3
+ *
4
+ * 跨多次 MCP 调用保持状态(进程生命周期内有效)。
5
+ * 通过 ServiceContainer 单例注册,每个项目同时只有一个 active session。
6
+ *
7
+ * 职责:
8
+ * - 维度完成状态跟踪
9
+ * - Phase 缓存(供 wiki_plan 复用)
10
+ * - EpisodicMemory 管理
11
+ * - Cross-dimension hints 收集与分发
12
+ * - 进度查询
13
+ * - Session 过期与恢复
14
+ *
15
+ * @module bootstrap/BootstrapSession
16
+ */
17
+
18
+ import crypto from 'node:crypto';
19
+ import { EpisodicMemory } from './pipeline/EpisodicMemory.js';
20
+ import { ExternalSubmissionTracker } from './ExternalSubmissionTracker.js';
21
+
22
+ // ── 常量 ────────────────────────────────────────────────────
23
+
24
+ const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 小时
25
+
26
+ // ── BootstrapSession ────────────────────────────────────────
27
+
28
+ export class BootstrapSession {
29
+ /**
30
+ * @param {object} opts
31
+ * @param {string} opts.projectRoot — 项目根目录
32
+ * @param {Array} opts.dimensions — 激活的维度定义列表
33
+ * @param {object} [opts.projectContext] — 传给 EpisodicMemory 的项目元数据
34
+ */
35
+ constructor({ projectRoot, dimensions, projectContext = {} }) {
36
+ this.id = `bs-${crypto.randomUUID()}`;
37
+ this.projectRoot = projectRoot;
38
+ this.dimensions = dimensions;
39
+ this.completedDimensions = new Map(); // dimId → { report, completedAt, recipeIds }
40
+ this.episodicMemory = new EpisodicMemory(projectContext);
41
+
42
+ /** 外部 Agent 提交追踪 (v2: 对标内部 Agent 的 EvidenceCollector) */
43
+ this.submissionTracker = new ExternalSubmissionTracker();
44
+
45
+ /** Phase 1-4 分析结果缓存,供 wiki_plan 复用 */
46
+ this.phaseCache = null;
47
+
48
+ /** 跨维度 hints 收集 */
49
+ this.crossDimensionHints = {}; // targetDimId → [{ fromDim, hint }]
50
+
51
+ this.startedAt = Date.now();
52
+ this.expiresAt = Date.now() + SESSION_TTL_MS;
53
+ }
54
+
55
+ // ── 状态查询 ──────────────────────────────────────────────
56
+
57
+ get isExpired() {
58
+ return Date.now() > this.expiresAt;
59
+ }
60
+
61
+ get isComplete() {
62
+ return this.completedDimensions.size >= this.dimensions.length;
63
+ }
64
+
65
+ getProgress() {
66
+ return {
67
+ completed: this.completedDimensions.size,
68
+ total: this.dimensions.length,
69
+ completedDimIds: [...this.completedDimensions.keys()],
70
+ remainingDimIds: this.dimensions
71
+ .map((d) => d.id)
72
+ .filter((id) => !this.completedDimensions.has(id)),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * 检查某个维度是否已完成
78
+ * @param {string} dimId
79
+ * @returns {boolean}
80
+ */
81
+ isDimensionComplete(dimId) {
82
+ return this.completedDimensions.has(dimId);
83
+ }
84
+
85
+ // ── 维度完成 ──────────────────────────────────────────────
86
+
87
+ /**
88
+ * 标记维度完成
89
+ * @param {string} dimId
90
+ * @param {object} report — { analysisText, findings, referencedFiles, recipeIds, candidateCount }
91
+ * @returns {{ updated: boolean, qualityReport: object|null }} — updated=true 表示覆盖了已有记录
92
+ */
93
+ markDimensionComplete(dimId, report) {
94
+ const updated = this.completedDimensions.has(dimId);
95
+
96
+ this.completedDimensions.set(dimId, {
97
+ ...report,
98
+ completedAt: Date.now(),
99
+ });
100
+
101
+ // 写入 EpisodicMemory
102
+ this.episodicMemory.storeDimensionReport(dimId, {
103
+ analysisText: report.analysisText,
104
+ findings: (report.keyFindings || []).map((f) => ({ content: f, importance: 'high' })),
105
+ referencedFiles: report.referencedFiles || [],
106
+ candidatesSummary: [],
107
+ });
108
+
109
+ // v2: 从 analysisText 提取负空间信号并计算质量报告
110
+ this.submissionTracker.extractNegativeSignals(report.analysisText, dimId);
111
+ const qualityReport = this.submissionTracker.buildQualityReport(
112
+ dimId,
113
+ report.analysisText,
114
+ report.referencedFiles || [],
115
+ );
116
+
117
+ return { updated, qualityReport };
118
+ }
119
+
120
+ // ── Cross-Dimension Hints ─────────────────────────────────
121
+
122
+ /**
123
+ * 存储跨维度 hints
124
+ * @param {string} fromDimId — 来源维度
125
+ * @param {Record<string, string>} hints — { targetDimId: hintText }
126
+ */
127
+ storeHints(fromDimId, hints) {
128
+ if (!hints || typeof hints !== 'object') return;
129
+
130
+ for (const [targetDim, hintText] of Object.entries(hints)) {
131
+ if (!this.crossDimensionHints[targetDim]) {
132
+ this.crossDimensionHints[targetDim] = [];
133
+ }
134
+ // 去重:同源维度只保留最新 hint
135
+ this.crossDimensionHints[targetDim] = this.crossDimensionHints[targetDim].filter(
136
+ (h) => h.fromDim !== fromDimId
137
+ );
138
+ this.crossDimensionHints[targetDim].push({
139
+ fromDim: fromDimId,
140
+ hint: hintText,
141
+ });
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 收集与剩余维度相关的 accumulated hints
147
+ * @returns {Record<string, Array<{ fromDim: string, hint: string }>>}
148
+ */
149
+ getAccumulatedHints() {
150
+ const progress = this.getProgress();
151
+ const accumulated = {};
152
+
153
+ for (const remainingDim of progress.remainingDimIds) {
154
+ const hints = this.crossDimensionHints[remainingDim];
155
+ if (hints?.length > 0) {
156
+ accumulated[remainingDim] = hints;
157
+ }
158
+ }
159
+
160
+ return accumulated;
161
+ }
162
+
163
+ // ── Phase 缓存 ────────────────────────────────────────────
164
+
165
+ /**
166
+ * 缓存 Phase 1-4 分析结果
167
+ * @param {object} cache — { files, astData, entityGraph, depGraph, guardFindings, skills, ... }
168
+ */
169
+ setPhaseCache(cache) {
170
+ this.phaseCache = cache;
171
+ }
172
+
173
+ /**
174
+ * 获取 Phase 缓存(wiki_plan 复用)
175
+ * @returns {object|null}
176
+ */
177
+ getPhaseCache() {
178
+ return this.phaseCache;
179
+ }
180
+
181
+ // ── 序列化 ────────────────────────────────────────────────
182
+
183
+ toJSON() {
184
+ return {
185
+ id: this.id,
186
+ projectRoot: this.projectRoot,
187
+ startedAt: this.startedAt,
188
+ expiresAt: this.expiresAt,
189
+ progress: this.getProgress(),
190
+ dimensionCount: this.dimensions.length,
191
+ };
192
+ }
193
+ }
194
+
195
+ // ── Session 管理器(进程级单例)──────────────────────────────
196
+
197
+ /**
198
+ * BootstrapSessionManager — 管理 active session
199
+ *
200
+ * 设计为进程级单例,通过 ServiceContainer 注册。
201
+ * 同时只有一个 active session(单项目场景)。
202
+ */
203
+ export class BootstrapSessionManager {
204
+ constructor() {
205
+ /** @type {BootstrapSession|null} */
206
+ this._activeSession = null;
207
+ }
208
+
209
+ /**
210
+ * 创建新的 bootstrap session
211
+ * @param {object} opts — 传给 BootstrapSession 构造函数的参数
212
+ * @returns {BootstrapSession}
213
+ */
214
+ createSession(opts) {
215
+ // 如果有旧的未过期 session,先标记过期
216
+ if (this._activeSession && !this._activeSession.isExpired) {
217
+ this._activeSession.expiresAt = Date.now(); // 强制过期
218
+ }
219
+ this._activeSession = new BootstrapSession(opts);
220
+ return this._activeSession;
221
+ }
222
+
223
+ /**
224
+ * 获取 active session
225
+ * @param {string} [sessionId] — 可选,用于验证 session ID
226
+ * @returns {BootstrapSession|null}
227
+ */
228
+ getSession(sessionId) {
229
+ if (!this._activeSession) return null;
230
+ if (this._activeSession.isExpired) return null;
231
+ if (sessionId && this._activeSession.id !== sessionId) return null;
232
+ return this._activeSession;
233
+ }
234
+
235
+ /**
236
+ * 获取 active session,无论是否过期(用于恢复场景)
237
+ * @returns {BootstrapSession|null}
238
+ */
239
+ getAnySession() {
240
+ return this._activeSession;
241
+ }
242
+
243
+ /**
244
+ * 清除 active session
245
+ */
246
+ clearSession() {
247
+ this._activeSession = null;
248
+ }
249
+ }
250
+
251
+ export default BootstrapSession;
@@ -0,0 +1,397 @@
1
+ /**
2
+ * ExternalSubmissionTracker — 外部 Agent 提交追踪与质量评估
3
+ *
4
+ * HandoffProtocol v2 的外部 Agent 对应模块。
5
+ * 内部 Agent 使用 EvidenceCollector 从 toolCall 中收集证据,
6
+ * 外部 Agent 使用 ExternalSubmissionTracker 从 submit_knowledge 调用中积累证据。
7
+ *
8
+ * 职责:
9
+ * - 追踪每个维度的 submit_knowledge 提交 (recipe 元数据 + 引用文件)
10
+ * - 从提交内容构建 evidenceMap (filePath → 引用摘要)
11
+ * - 从 dimension_complete 的 analysisText 提取负空间信号
12
+ * - 计算维度级质量评分 (类似 HandoffProtocol.buildQualityScores)
13
+ * - 为下游维度提供结构化跨维度证据
14
+ *
15
+ * 设计对应关系:
16
+ * 内部 Agent 外部 Agent
17
+ * ───────────────── ─────────────────
18
+ * EvidenceCollector.processToolCall → recordSubmission
19
+ * evidenceMap (代码片段) → evidenceMap (提交引用)
20
+ * negativeSignals (搜索未命中) → negativeSignals (analysisText 提取)
21
+ * buildQualityScores (4维评分) → buildQualityReport (4维评分)
22
+ * explorationLog (工具序列) → submissionLog (提交序列)
23
+ *
24
+ * @module bootstrap/ExternalSubmissionTracker
25
+ */
26
+
27
+ // ── 常量 ────────────────────────────────────────────────────
28
+
29
+ /** 单个维度最大追踪提交数 */
30
+ const MAX_SUBMISSIONS_PER_DIM = 20;
31
+
32
+ /** 负空间信号最大数量 */
33
+ const MAX_NEGATIVE_SIGNALS = 30;
34
+
35
+ // ── 类型定义 ────────────────────────────────────────────────
36
+
37
+ /**
38
+ * @typedef {object} SubmissionRecord
39
+ * @property {string} recipeId — 提交返回的 recipe ID
40
+ * @property {string} title — 候选标题
41
+ * @property {string} knowledgeType — 知识类型
42
+ * @property {string} kind — rule/pattern/fact
43
+ * @property {string} category — 分类
44
+ * @property {string[]} sources — reasoning.sources (引用文件)
45
+ * @property {string} coreCodePreview — coreCode 前 200 字符
46
+ * @property {number} contentLength — content.markdown 长度
47
+ * @property {number} confidence — reasoning.confidence
48
+ * @property {number} submittedAt — 时间戳
49
+ */
50
+
51
+ /**
52
+ * @typedef {object} NegativeSignal
53
+ * @property {string} pattern — 未找到/不存在的模式描述
54
+ * @property {string} source — 'analysisText' | 'rejection'
55
+ * @property {string} [dimId] — 来源维度
56
+ */
57
+
58
+ /**
59
+ * @typedef {object} DimensionQualityReport
60
+ * @property {object} scores — { coverageScore, evidenceScore, diversityScore, coherenceScore }
61
+ * @property {number} totalScore — 加权总分 (0-100)
62
+ * @property {string[]} suggestions — 改进建议
63
+ * @property {boolean} pass — 是否通过质量门控
64
+ */
65
+
66
+ // ── 主类 ────────────────────────────────────────────────────
67
+
68
+ export class ExternalSubmissionTracker {
69
+ /** @type {Map<string, SubmissionRecord[]>} dimId → 提交记录列表 */
70
+ #dimensionSubmissions = new Map();
71
+
72
+ /** @type {Map<string, Set<string>>} filePath → 引用此文件的 dimId 集合 */
73
+ #fileEvidenceMap = new Map();
74
+
75
+ /** @type {NegativeSignal[]} 负空间信号 */
76
+ #negativeSignals = [];
77
+
78
+ /** @type {Map<string, string[]>} dimId → 被拒绝的提交标题列表 */
79
+ #rejections = new Map();
80
+
81
+ /** @type {Set<string>} 已使用的唯一 trigger 集合 (跨维度) */
82
+ #usedTriggers = new Set();
83
+
84
+ // ─── 提交记录 ─────────────────────────────────────────
85
+
86
+ /**
87
+ * 记录一次成功的 submit_knowledge 提交
88
+ *
89
+ * @param {string} dimId — 当前活跃维度 (由调用方根据 session 进度推断)
90
+ * @param {object} submissionArgs — submit_knowledge 的原始参数
91
+ * @param {string} recipeId — 提交成功后返回的 recipe ID
92
+ */
93
+ recordSubmission(dimId, submissionArgs, recipeId) {
94
+ if (!this.#dimensionSubmissions.has(dimId)) {
95
+ this.#dimensionSubmissions.set(dimId, []);
96
+ }
97
+
98
+ const submissions = this.#dimensionSubmissions.get(dimId);
99
+ if (submissions.length >= MAX_SUBMISSIONS_PER_DIM) return;
100
+
101
+ const record = {
102
+ recipeId,
103
+ title: submissionArgs.title || '',
104
+ knowledgeType: submissionArgs.knowledgeType || '',
105
+ kind: submissionArgs.kind || '',
106
+ category: submissionArgs.category || '',
107
+ sources: submissionArgs.reasoning?.sources || [],
108
+ coreCodePreview: (submissionArgs.coreCode || '').substring(0, 200),
109
+ contentLength: submissionArgs.content?.markdown?.length || 0,
110
+ confidence: submissionArgs.reasoning?.confidence || 0,
111
+ submittedAt: Date.now(),
112
+ };
113
+
114
+ submissions.push(record);
115
+
116
+ // 记录 trigger
117
+ if (submissionArgs.trigger) {
118
+ this.#usedTriggers.add(submissionArgs.trigger);
119
+ }
120
+
121
+ // 更新 fileEvidenceMap
122
+ for (const source of record.sources) {
123
+ const filePath = source.split(':')[0]; // "file.m:123" → "file.m"
124
+ if (!this.#fileEvidenceMap.has(filePath)) {
125
+ this.#fileEvidenceMap.set(filePath, new Set());
126
+ }
127
+ this.#fileEvidenceMap.get(filePath).add(dimId);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 记录被拒绝的提交 (RecipeReadiness 或 dedup 拒绝)
133
+ *
134
+ * @param {string} dimId
135
+ * @param {string} title — 被拒绝候选的标题
136
+ * @param {string} reason — 拒绝原因
137
+ */
138
+ recordRejection(dimId, title, reason) {
139
+ if (!this.#rejections.has(dimId)) {
140
+ this.#rejections.set(dimId, []);
141
+ }
142
+ this.#rejections.get(dimId).push(`${title}: ${reason}`);
143
+
144
+ // 拒绝也是一种负空间信号
145
+ this.#addNegativeSignal(`Rejected submission "${title}": ${reason}`, 'rejection', dimId);
146
+ }
147
+
148
+ // ─── 负空间信号 ───────────────────────────────────────
149
+
150
+ /**
151
+ * 从 dimension_complete 的 analysisText 中提取负空间信号
152
+ *
153
+ * 识别模式:
154
+ * - "未找到..." / "不存在..." / "没有发现..."
155
+ * - "Not found" / "No evidence of" / "does not use"
156
+ * - "项目未使用..." / "没有使用..."
157
+ *
158
+ * @param {string} analysisText
159
+ * @param {string} dimId
160
+ */
161
+ extractNegativeSignals(analysisText, dimId) {
162
+ if (!analysisText) return;
163
+
164
+ const negativePatterns = [
165
+ // 中文负空间
166
+ /(?:未找到|不存在|没有发现|没有使用|未使用|未见|项目未采用|项目不使用|缺少)\s*[^。\n]{5,60}/g,
167
+ // 英文负空间
168
+ /(?:not found|no evidence of|does not use|no instances? of|absence of|missing|not implemented|not detected)\s+[^.\n]{5,80}/gi,
169
+ // 明确的反面结论
170
+ /(?:与预期不同|contrary to|unlike|despite|although)[^。.\n]{10,80}/gi,
171
+ ];
172
+
173
+ for (const pattern of negativePatterns) {
174
+ let match;
175
+ while ((match = pattern.exec(analysisText)) !== null) {
176
+ this.#addNegativeSignal(match[0].trim(), 'analysisText', dimId);
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 添加负空间信号 (去重)
183
+ * @param {string} pattern
184
+ * @param {string} source
185
+ * @param {string} [dimId]
186
+ */
187
+ #addNegativeSignal(pattern, source, dimId) {
188
+ if (this.#negativeSignals.length >= MAX_NEGATIVE_SIGNALS) return;
189
+
190
+ // 去重: 相同 pattern 不重复添加
191
+ const normalized = pattern.toLowerCase().substring(0, 80);
192
+ const exists = this.#negativeSignals.some(
193
+ (s) => s.pattern.toLowerCase().substring(0, 80) === normalized,
194
+ );
195
+ if (!exists) {
196
+ this.#negativeSignals.push({ pattern, source, dimId });
197
+ }
198
+ }
199
+
200
+ // ─── 质量评估 ─────────────────────────────────────────
201
+
202
+ /**
203
+ * 计算维度级质量报告
204
+ *
205
+ * 4 维度评分 (各 0-100, 加权总分):
206
+ * coverageScore (30%) — 提交数量 + 引用文件覆盖
207
+ * evidenceScore (30%) — 提交内容丰富度 (长度 + coreCode + confidence)
208
+ * diversityScore (20%) — 知识类型 + category 多样性
209
+ * coherenceScore (20%) — analysisText 结构化程度
210
+ *
211
+ * 与内部 Agent 的 buildQualityScores 对齐:
212
+ * 内部 depthScore → 外部 coverageScore
213
+ * 内部 evidenceScore → 外部 evidenceScore
214
+ * 内部 breadthScore → 外部 diversityScore
215
+ * 内部 coherenceScore → 外部 coherenceScore
216
+ *
217
+ * @param {string} dimId
218
+ * @param {string} [analysisText] — dimension_complete 提供的分析文本
219
+ * @param {string[]} [referencedFiles] — 引用文件列表
220
+ * @returns {DimensionQualityReport}
221
+ */
222
+ buildQualityReport(dimId, analysisText = '', referencedFiles = []) {
223
+ const submissions = this.#dimensionSubmissions.get(dimId) || [];
224
+ const rejections = this.#rejections.get(dimId) || [];
225
+ const scores = {};
226
+ const suggestions = [];
227
+
228
+ // §1: coverageScore — 提交数量 + 引用文件覆盖
229
+ const submissionCount = submissions.length;
230
+ const uniqueSources = new Set(submissions.flatMap((s) => s.sources));
231
+ const fileCount = new Set([...uniqueSources, ...referencedFiles]).size;
232
+ scores.coverageScore = Math.min(
233
+ 100,
234
+ submissionCount * 20 + fileCount * 8,
235
+ );
236
+ if (submissionCount < 3) {
237
+ suggestions.push(`只提交了 ${submissionCount} 条候选,建议至少 3 条以充分覆盖维度`);
238
+ }
239
+ if (fileCount < 3) {
240
+ suggestions.push(`引用文件仅 ${fileCount} 个,建议引用更多源码文件作为证据`);
241
+ }
242
+
243
+ // §2: evidenceScore — 提交内容丰富度
244
+ const avgContentLen = submissions.length > 0
245
+ ? submissions.reduce((sum, s) => sum + s.contentLength, 0) / submissions.length
246
+ : 0;
247
+ const hasCoreCode = submissions.filter((s) => s.coreCodePreview.length > 0).length;
248
+ const avgConfidence = submissions.length > 0
249
+ ? submissions.reduce((sum, s) => sum + s.confidence, 0) / submissions.length
250
+ : 0;
251
+ scores.evidenceScore = Math.min(
252
+ 100,
253
+ (avgContentLen > 400 ? 40 : avgContentLen / 10) +
254
+ (hasCoreCode / Math.max(submissions.length, 1)) * 30 +
255
+ avgConfidence * 30,
256
+ );
257
+ if (avgContentLen < 200) {
258
+ suggestions.push('候选内容平均长度偏短,建议包含更多代码引用和项目上下文');
259
+ }
260
+ if (rejections.length > 0) {
261
+ suggestions.push(`有 ${rejections.length} 条提交被拒绝,请检查字段完整性`);
262
+ }
263
+
264
+ // §3: diversityScore — 知识类型 + category 多样性
265
+ const uniqueTypes = new Set(submissions.map((s) => s.knowledgeType));
266
+ const uniqueCategories = new Set(submissions.map((s) => s.category));
267
+ const uniqueKinds = new Set(submissions.map((s) => s.kind));
268
+ scores.diversityScore = Math.min(
269
+ 100,
270
+ uniqueTypes.size * 25 + uniqueCategories.size * 15 + uniqueKinds.size * 20,
271
+ );
272
+
273
+ // §4: coherenceScore — analysisText 结构化程度
274
+ const textLen = analysisText.length;
275
+ const hasHeaders = /#{1,3}\s/.test(analysisText);
276
+ const hasLists = /\d+\.\s|[-•]\s/.test(analysisText);
277
+ const hasCodeBlocks = /```[\s\S]*?```/.test(analysisText);
278
+ scores.coherenceScore = Math.min(
279
+ 100,
280
+ (textLen > 500 ? 30 : textLen / 17) +
281
+ (hasHeaders ? 25 : 0) +
282
+ (hasLists ? 20 : 0) +
283
+ (hasCodeBlocks ? 25 : 0),
284
+ );
285
+ if (textLen < 200) {
286
+ suggestions.push('分析文本过短,建议包含更详细的代码分析过程');
287
+ }
288
+
289
+ // 加权总分
290
+ const totalScore = Math.round(
291
+ scores.coverageScore * 0.3 +
292
+ scores.evidenceScore * 0.3 +
293
+ scores.diversityScore * 0.2 +
294
+ scores.coherenceScore * 0.2,
295
+ );
296
+
297
+ // 门控阈值
298
+ const pass = totalScore >= 50;
299
+ if (!pass) {
300
+ suggestions.unshift(`质量评分 ${totalScore}/100 未达标 (≥50),建议补充更多高质量候选`);
301
+ }
302
+
303
+ return { scores, totalScore, suggestions, pass };
304
+ }
305
+
306
+ // ─── 跨维度证据 ───────────────────────────────────────
307
+
308
+ /**
309
+ * 获取跨维度累积证据摘要 — 供下一维度参考
310
+ *
311
+ * @param {string} currentDimId — 当前维度 (将排除在结果之外)
312
+ * @returns {object} — { completedDimSummaries, sharedFiles, negativeSignals, usedTriggers }
313
+ */
314
+ getAccumulatedEvidence(currentDimId) {
315
+ const completedDimSummaries = [];
316
+
317
+ for (const [dimId, submissions] of this.#dimensionSubmissions) {
318
+ if (dimId === currentDimId) continue;
319
+
320
+ completedDimSummaries.push({
321
+ dimId,
322
+ submissionCount: submissions.length,
323
+ titles: submissions.map((s) => s.title),
324
+ knowledgeTypes: [...new Set(submissions.map((s) => s.knowledgeType))],
325
+ referencedFiles: [...new Set(submissions.flatMap((s) => s.sources))].slice(0, 15),
326
+ });
327
+ }
328
+
329
+ // 多维度引用的文件 (交叉点)
330
+ const sharedFiles = [];
331
+ for (const [filePath, dimIds] of this.#fileEvidenceMap) {
332
+ if (dimIds.size > 1) {
333
+ sharedFiles.push({ filePath, dimensions: [...dimIds] });
334
+ }
335
+ }
336
+
337
+ return {
338
+ completedDimSummaries,
339
+ sharedFiles,
340
+ negativeSignals: this.#negativeSignals.filter((s) => s.dimId !== currentDimId),
341
+ usedTriggers: [...this.#usedTriggers],
342
+ };
343
+ }
344
+
345
+ // ─── 查询 API ─────────────────────────────────────────
346
+
347
+ /**
348
+ * 获取指定维度的提交列表
349
+ * @param {string} dimId
350
+ * @returns {SubmissionRecord[]}
351
+ */
352
+ getSubmissions(dimId) {
353
+ return this.#dimensionSubmissions.get(dimId) || [];
354
+ }
355
+
356
+ /**
357
+ * 获取所有负空间信号
358
+ * @returns {NegativeSignal[]}
359
+ */
360
+ getNegativeSignals() {
361
+ return [...this.#negativeSignals];
362
+ }
363
+
364
+ /**
365
+ * 获取全局文件证据地图
366
+ * @returns {Map<string, Set<string>>}
367
+ */
368
+ getFileEvidenceMap() {
369
+ return new Map(this.#fileEvidenceMap);
370
+ }
371
+
372
+ /**
373
+ * 获取追踪统计
374
+ * @returns {object}
375
+ */
376
+ getStats() {
377
+ let totalSubmissions = 0;
378
+ let totalRejections = 0;
379
+ for (const subs of this.#dimensionSubmissions.values()) {
380
+ totalSubmissions += subs.length;
381
+ }
382
+ for (const rejs of this.#rejections.values()) {
383
+ totalRejections += rejs.length;
384
+ }
385
+
386
+ return {
387
+ dimensions: this.#dimensionSubmissions.size,
388
+ totalSubmissions,
389
+ totalRejections,
390
+ uniqueFiles: this.#fileEvidenceMap.size,
391
+ negativeSignals: this.#negativeSignals.length,
392
+ usedTriggers: this.#usedTriggers.size,
393
+ };
394
+ }
395
+ }
396
+
397
+ export default ExternalSubmissionTracker;