autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
313 lines (312 loc) • 14.7 kB
JavaScript
/**
* ExternalSubmissionTracker — 外部 Agent 提交追踪与质量评估
*
* 质量门控的外部 Agent 对应模块。
* 内部 Agent 使用 EvidenceCollector 从 toolCall 中收集证据 (bootstrap-gate.js),
* 外部 Agent 使用 ExternalSubmissionTracker 从 submit_knowledge 调用中积累证据。
*
* 职责:
* - 追踪每个维度的 submit_knowledge 提交 (recipe 元数据 + 引用文件)
* - 从提交内容构建 evidenceMap (filePath → 引用摘要)
* - 从 dimension_complete 的 analysisText 提取负空间信号
* - 计算维度级质量评分 (对应 bootstrap-gate.js 的 buildQualityScores)
* - 为下游维度提供结构化跨维度证据
*
* 设计对应关系:
* 内部 Agent 外部 Agent
* ───────────────── ─────────────────
* EvidenceCollector.processToolCall → recordSubmission
* evidenceMap (代码片段) → evidenceMap (提交引用)
* negativeSignals (搜索未命中) → negativeSignals (analysisText 提取)
* buildQualityScores (4维评分) → buildQualityReport (4维评分)
* explorationLog (工具序列) → submissionLog (提交序列)
*
* @module bootstrap/ExternalSubmissionTracker
*/
// ── 常量 ────────────────────────────────────────────────────
/** 单个维度最大追踪提交数 */
const MAX_SUBMISSIONS_PER_DIM = 20;
/** 负空间信号最大数量 */
const MAX_NEGATIVE_SIGNALS = 30;
// ── 主类 ────────────────────────────────────────────────────
export class ExternalSubmissionTracker {
/** dimId → 提交记录列表 */
#dimensionSubmissions = new Map();
/** filePath → 引用此文件的 dimId 集合 */
#fileEvidenceMap = new Map();
/** 负空间信号 */
#negativeSignals = [];
/** dimId → 被拒绝的提交标题列表 */
#rejections = new Map();
/** 已使用的唯一 trigger 集合 (跨维度) */
#usedTriggers = new Set();
// ─── 提交记录 ─────────────────────────────────────────
/**
* 记录一次成功的 submit_knowledge 提交
*
* @param dimId 当前活跃维度 (由调用方根据 session 进度推断)
* @param submissionArgs submit_knowledge 的原始参数
* @param recipeId 提交成功后返回的 recipe ID
*/
recordSubmission(dimId, submissionArgs, recipeId) {
if (!this.#dimensionSubmissions.has(dimId)) {
this.#dimensionSubmissions.set(dimId, []);
}
const submissions = this.#dimensionSubmissions.get(dimId);
if (submissions.length >= MAX_SUBMISSIONS_PER_DIM) {
// 超过追踪上限,记录警告信息而非静默丢弃
this.#addNegativeSignal(`Dimension "${dimId}" exceeded ${MAX_SUBMISSIONS_PER_DIM} submissions tracking limit — quality scoring may be lower than actual`, 'tracker-overflow', dimId);
return;
}
const record = {
recipeId,
title: submissionArgs.title || '',
knowledgeType: submissionArgs.knowledgeType || '',
kind: submissionArgs.kind || '',
category: submissionArgs.category || '',
sources: submissionArgs.reasoning?.sources || [],
coreCodePreview: (submissionArgs.coreCode || '').substring(0, 200),
contentLength: submissionArgs.content?.markdown?.length || 0,
confidence: submissionArgs.reasoning?.confidence || 0,
submittedAt: Date.now(),
};
submissions.push(record);
// 记录 trigger
if (submissionArgs.trigger) {
this.#usedTriggers.add(submissionArgs.trigger);
}
// 更新 fileEvidenceMap
for (const source of record.sources) {
const filePath = source.split(':')[0]; // "file.m:123" → "file.m"
if (!this.#fileEvidenceMap.has(filePath)) {
this.#fileEvidenceMap.set(filePath, new Set());
}
this.#fileEvidenceMap.get(filePath).add(dimId);
}
}
/**
* 记录被拒绝的提交 (RecipeReadiness 或 dedup 拒绝)
*
* @param title 被拒绝候选的标题
* @param reason 拒绝原因
*/
recordRejection(dimId, title, reason) {
if (!this.#rejections.has(dimId)) {
this.#rejections.set(dimId, []);
}
this.#rejections.get(dimId).push(`${title}: ${reason}`);
// 拒绝也是一种负空间信号
this.#addNegativeSignal(`Rejected submission "${title}": ${reason}`, 'rejection', dimId);
}
// ─── 负空间信号 ───────────────────────────────────────
/**
* 从 dimension_complete 的 analysisText 中提取负空间信号
*
* 识别模式:
* - "未找到..." / "不存在..." / "没有发现..."
* - "Not found" / "No evidence of" / "does not use"
* - "项目未使用..." / "没有使用..."
*/
extractNegativeSignals(analysisText, dimId) {
if (!analysisText) {
return;
}
const negativePatterns = [
// 中文负空间
/(?:未找到|不存在|没有发现|没有使用|未使用|未见|项目未采用|项目不使用|缺少)\s*[^。\n]{5,60}/g,
// 英文负空间
/(?:not found|no evidence of|does not use|no instances? of|absence of|missing|not implemented|not detected)\s+[^.\n]{5,80}/gi,
// 明确的反面结论
/(?:与预期不同|contrary to|unlike|despite|although)[^。.\n]{10,80}/gi,
];
for (const pattern of negativePatterns) {
let match;
while ((match = pattern.exec(analysisText)) !== null) {
this.#addNegativeSignal(match[0].trim(), 'analysisText', dimId);
}
}
}
/** 添加负空间信号 (去重) */
#addNegativeSignal(pattern, source, dimId) {
if (this.#negativeSignals.length >= MAX_NEGATIVE_SIGNALS) {
return;
}
// 去重: 相同 pattern 不重复添加
const normalized = pattern.toLowerCase().substring(0, 80);
const exists = this.#negativeSignals.some((s) => s.pattern.toLowerCase().substring(0, 80) === normalized);
if (!exists) {
this.#negativeSignals.push({ pattern, source, dimId });
}
}
// ─── 质量评估 ─────────────────────────────────────────
/**
* 计算维度级质量报告
*
* 4 维度评分 (各 0-100, 加权总分):
* coverageScore (30%) — 提交数量 + 引用文件覆盖
* evidenceScore (30%) — 提交内容丰富度 (长度 + coreCode + confidence)
* diversityScore (20%) — 知识类型 + category 多样性
* coherenceScore (20%) — analysisText 结构化程度
*
* 与内部 Agent 的 buildQualityScores 对齐:
* 内部 depthScore → 外部 coverageScore
* 内部 evidenceScore → 外部 evidenceScore
* 内部 breadthScore → 外部 diversityScore
* 内部 coherenceScore → 外部 coherenceScore
*
* @param [analysisText] dimension_complete 提供的分析文本
* @param [referencedFiles] 引用文件列表
*/
buildQualityReport(dimId, analysisText = '', referencedFiles = []) {
const submissions = this.#dimensionSubmissions.get(dimId) || [];
const rejections = this.#rejections.get(dimId) || [];
const scores = {};
const suggestions = [];
// §1: coverageScore — 提交数量 + 引用文件覆盖
const submissionCount = submissions.length;
const uniqueSources = new Set(submissions.flatMap((s) => s.sources));
const fileCount = new Set([...uniqueSources, ...referencedFiles]).size;
scores.coverageScore = Math.min(100, submissionCount * 20 + fileCount * 8);
if (submissionCount < 3) {
suggestions.push(`只提交了 ${submissionCount} 条候选,建议至少 3 条以充分覆盖维度`);
}
if (fileCount < 3) {
suggestions.push(`引用文件仅 ${fileCount} 个,建议引用更多源码文件作为证据`);
}
// §2: evidenceScore — 提交内容丰富度
const avgContentLen = submissions.length > 0
? submissions.reduce((sum, s) => sum + s.contentLength, 0) / submissions.length
: 0;
const hasCoreCode = submissions.filter((s) => s.coreCodePreview.length > 0).length;
const avgConfidence = submissions.length > 0
? submissions.reduce((sum, s) => sum + s.confidence, 0) / submissions.length
: 0;
scores.evidenceScore = Math.min(100, (avgContentLen > 400 ? 40 : avgContentLen / 10) +
(hasCoreCode / Math.max(submissions.length, 1)) * 30 +
avgConfidence * 30);
if (avgContentLen < 200) {
suggestions.push('候选内容平均长度偏短,建议包含更多代码引用和项目上下文');
}
if (rejections.length > 0) {
suggestions.push(`有 ${rejections.length} 条提交被拒绝,请检查字段完整性`);
}
// §3: diversityScore — 知识类型 + category 多样性
const uniqueTypes = new Set(submissions.map((s) => s.knowledgeType));
const uniqueCategories = new Set(submissions.map((s) => s.category));
const uniqueKinds = new Set(submissions.map((s) => s.kind));
scores.diversityScore = Math.min(100, uniqueTypes.size * 25 + uniqueCategories.size * 15 + uniqueKinds.size * 20);
// §4: coherenceScore — analysisText 结构化程度
const textLen = analysisText.length;
const hasHeaders = /#{1,3}\s/.test(analysisText);
const hasLists = /\d+\.\s|[-•]\s/.test(analysisText);
const hasCodeBlocks = /```[\s\S]*?```/.test(analysisText);
scores.coherenceScore = Math.min(100, (textLen > 500 ? 30 : textLen / 17) +
(hasHeaders ? 25 : 0) +
(hasLists ? 20 : 0) +
(hasCodeBlocks ? 25 : 0));
if (textLen < 200) {
suggestions.push('分析文本过短,建议包含更详细的代码分析过程');
}
// 加权总分
const totalScore = Math.round(scores.coverageScore * 0.3 +
scores.evidenceScore * 0.3 +
scores.diversityScore * 0.2 +
scores.coherenceScore * 0.2);
// 门控阈值
const pass = totalScore >= 50;
if (!pass) {
suggestions.unshift(`质量评分 ${totalScore}/100 未达标 (≥50),建议补充更多高质量候选`);
}
return { scores, totalScore, suggestions, pass };
}
// ─── 跨维度证据 ───────────────────────────────────────
/**
* 获取跨维度累积证据摘要 — 供下一维度参考
*
* @param currentDimId 当前维度 (将排除在结果之外)
* @returns { completedDimSummaries, sharedFiles, negativeSignals, usedTriggers }
*/
getAccumulatedEvidence(currentDimId) {
const completedDimSummaries = [];
for (const [dimId, submissions] of this.#dimensionSubmissions) {
if (dimId === currentDimId) {
continue;
}
completedDimSummaries.push({
dimId,
submissionCount: submissions.length,
titles: submissions.map((s) => s.title),
knowledgeTypes: [...new Set(submissions.map((s) => s.knowledgeType))],
referencedFiles: [
...new Set(submissions.flatMap((s) => s.sources)),
].slice(0, 15),
});
}
// 多维度引用的文件 (交叉点)
const sharedFiles = [];
for (const [filePath, dimIds] of this.#fileEvidenceMap) {
if (dimIds.size > 1) {
sharedFiles.push({ filePath, dimensions: [...dimIds] });
}
}
return {
completedDimSummaries,
sharedFiles,
negativeSignals: this.#negativeSignals.filter((s) => s.dimId !== currentDimId),
usedTriggers: [...this.#usedTriggers],
};
}
// ─── 查询 API ─────────────────────────────────────────
/** 获取指定维度的提交列表 */
getSubmissions(dimId) {
return this.#dimensionSubmissions.get(dimId) || [];
}
/** 获取所有负空间信号 */
getNegativeSignals() {
return [...this.#negativeSignals];
}
/** 获取全局文件证据地图 */
getFileEvidenceMap() {
return new Map(this.#fileEvidenceMap);
}
/** 获取追踪统计 */
getStats() {
let totalSubmissions = 0;
let totalRejections = 0;
for (const subs of this.#dimensionSubmissions.values()) {
totalSubmissions += subs.length;
}
for (const rejs of this.#rejections.values()) {
totalRejections += rejs.length;
}
return {
dimensions: this.#dimensionSubmissions.size,
totalSubmissions,
totalRejections,
uniqueFiles: this.#fileEvidenceMap.size,
negativeSignals: this.#negativeSignals.length,
usedTriggers: this.#usedTriggers.size,
};
}
/**
* 获取所有已提交候选的标题集合(小写,用于跨维度硬去重)
*
* @param [excludeDimId] 可选,排除指定维度的标题
* @returns Set<string> 小写标题集合
*/
getAllSubmittedTitles(excludeDimId) {
const titles = new Set();
for (const [dimId, submissions] of this.#dimensionSubmissions) {
if (excludeDimId && dimId === excludeDimId) {
continue;
}
for (const sub of submissions) {
if (sub.title) {
titles.add(sub.title.toLowerCase().trim());
}
}
}
return titles;
}
}
export default ExternalSubmissionTracker;