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.
- package/lib/cli/SetupService.js +7 -1
- package/lib/cli/UpgradeService.js +4 -0
- package/lib/core/enhancement/EnhancementPack.js +11 -0
- package/lib/core/enhancement/android-enhancement.js +2 -0
- package/lib/core/enhancement/django-enhancement.js +3 -0
- package/lib/core/enhancement/fastapi-enhancement.js +3 -0
- package/lib/core/enhancement/go-grpc-enhancement.js +2 -0
- package/lib/core/enhancement/go-web-enhancement.js +2 -0
- package/lib/core/enhancement/langchain-enhancement.js +3 -0
- package/lib/core/enhancement/ml-enhancement.js +3 -0
- package/lib/core/enhancement/nextjs-enhancement.js +3 -0
- package/lib/core/enhancement/node-server-enhancement.js +3 -0
- package/lib/core/enhancement/react-enhancement.js +4 -0
- package/lib/core/enhancement/rust-tokio-enhancement.js +2 -0
- package/lib/core/enhancement/rust-web-enhancement.js +2 -0
- package/lib/core/enhancement/spring-enhancement.js +2 -0
- package/lib/core/enhancement/vue-enhancement.js +3 -0
- package/lib/external/mcp/McpServer.js +38 -4
- package/lib/external/mcp/autoApproveInjector.js +154 -0
- package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +251 -0
- package/lib/external/mcp/handlers/bootstrap/ExternalSubmissionTracker.js +397 -0
- package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +578 -0
- package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +11 -6
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +10 -6
- package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +5 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -2
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +5 -2
- package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +1 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +41 -79
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +22 -5
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +639 -0
- package/lib/external/mcp/handlers/bootstrap/shared/dimension-sop.js +670 -0
- package/lib/external/mcp/handlers/bootstrap/shared/dimension-text.js +253 -0
- package/lib/external/mcp/handlers/bootstrap/shared/skill-generator.js +263 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +2 -2
- package/lib/external/mcp/handlers/bootstrap-external.js +177 -0
- package/lib/external/mcp/handlers/{bootstrap.js → bootstrap-internal.js} +28 -31
- package/lib/external/mcp/handlers/consolidated.js +55 -43
- package/lib/external/mcp/handlers/dimension-complete-external.js +370 -0
- package/lib/external/mcp/handlers/knowledge.js +35 -4
- package/lib/external/mcp/handlers/system.js +13 -2
- package/lib/external/mcp/handlers/wiki-external.js +516 -0
- package/lib/external/mcp/tools.js +98 -42
- package/lib/http/routes/candidates.js +1 -1
- package/lib/platform/ios/spm/SpmService.js +1 -1
- package/lib/platform/ios/xcode/XcodeIntegration.js +2 -2
- package/lib/service/candidate/CandidateAggregator.js +52 -0
- package/lib/service/chat/AnalystAgent.js +7 -3
- package/lib/service/chat/ChatAgent.js +1 -1
- package/lib/service/chat/EvidenceCollector.js +500 -0
- package/lib/service/chat/HandoffProtocol.js +222 -1
- package/lib/service/chat/ProducerAgent.js +142 -12
- package/lib/service/chat/tools/ai-analysis.js +3 -3
- package/lib/service/chat/tools/ast-graph.js +2 -2
- package/lib/service/chat/tools/infrastructure.js +1 -1
- package/lib/service/module/ModuleService.js +1 -1
- package/lib/service/recipe/RecipeCandidateValidator.js +5 -0
- package/lib/shared/RecipeReadinessChecker.js +15 -0
- package/package.json +1 -1
- package/scripts/migrate-md-to-knowledge.mjs +5 -2
- package/skills/autosnippet-analysis/SKILL.md +1 -1
- package/skills/autosnippet-candidates/SKILL.md +2 -2
- package/skills/autosnippet-coldstart/SKILL.md +3 -0
- package/skills/autosnippet-concepts/SKILL.md +8 -2
- package/skills/autosnippet-create/SKILL.md +2 -2
- package/skills/autosnippet-recipes/SKILL.md +1 -1
- 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;
|