autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
494 lines (493 loc) • 19.1 kB
JavaScript
/**
* RecipeRelevanceAuditor — 基于代码证据验证 Recipe 当前相关性
*
* Rescan 时主动触发,检查每个保留 Recipe 的代码证据是否仍然存在:
* 1. 触发模式匹配 (trigger 引用的文件类型/路径模式是否有匹配)
* 2. 代码符号存活 (content.pattern 引用的类名/函数名是否在 AST 中)
* 3. 依赖关系完整 (涉及模块依赖是否在依赖图中)
* 4. 源代码文件存活 (reasoning.sources + content.codeChanges 的文件是否存在)
*
* 评分后驱动快速衰退:
* - healthy (80-100): 无操作
* - watch (60-79): 报告警告
* - decay (40-59): active → decaying (7d grace)
* - severe (20-39): active → decaying (3d grace)
* - dead (0-19): active → deprecated (immediate)
*
* 支持 Category 权重豁免:架构/规范类 Recipe 侧重触发模式而非具体符号。
*
* @module service/evolution/RecipeRelevanceAuditor
*/
// ── 常量 ────────────────────────────────────────────────────
/** 默认证据权重
*
* 注意:不包含 sourceFileExists。DB 中 sourceFile 存储的是 Recipe md 文件路径
* (如 AutoSnippet/candidates/xxx.md),不是源代码路径。
* 真正的源代码来源在 reasoning.sources 中,由 codeFilesExist 维度检查。
*/
const DEFAULT_WEIGHTS = {
triggerStillMatches: 0.2,
symbolsAlive: 0.3,
depsIntact: 0.15,
codeFilesExist: 0.35,
};
/**
* 按 category 覆盖权重 — 架构/规范类侧重触发模式和来源文件
*/
const CATEGORY_WEIGHT_OVERRIDES = {
architecture: {
symbolsAlive: 0.05,
depsIntact: 0.05,
triggerStillMatches: 0.45,
codeFilesExist: 0.45,
},
'coding-standards': {
symbolsAlive: 0.05,
depsIntact: 0.05,
triggerStillMatches: 0.45,
codeFilesExist: 0.45,
},
'agent-guidelines': {
symbolsAlive: 0.0,
depsIntact: 0.0,
triggerStillMatches: 0.5,
codeFilesExist: 0.5,
},
};
/** Grace Period 常量 */
const GRACE_PERIOD_DECAY = 7 * 24 * 60 * 60 * 1000; // 7d
const GRACE_PERIOD_SEVERE = 3 * 24 * 60 * 60 * 1000; // 3d
// ── RecipeRelevanceAuditor ──────────────────────────────────
export class RecipeRelevanceAuditor {
#knowledgeRepo;
#proposalRepo;
#logger;
constructor(opts) {
this.#knowledgeRepo = opts.knowledgeRepo;
this.#proposalRepo = opts.proposalRepo ?? null;
this.#logger = opts.logger || { info() { }, warn() { } };
}
/**
* 审计所有保留 Recipe 的代码证据
*/
async audit(recipes, analysisData) {
const summary = {
totalAudited: 0,
healthy: 0,
watch: 0,
decay: 0,
severe: 0,
dead: 0,
results: [],
proposalsCreated: 0,
immediateDeprecated: 0,
};
// 预处理:构建快速查找集合
const fileSet = new Set(analysisData.fileList.map((f) => f.toLowerCase()));
const entityNames = new Set(analysisData.codeEntities.map((e) => e.name.toLowerCase()));
const depModules = new Set();
for (const edge of analysisData.dependencyGraph) {
depModules.add(edge.from.toLowerCase());
depModules.add(edge.to.toLowerCase());
}
for (const recipe of recipes) {
const fullRecipe = await this.#loadFullRecipe(recipe.id);
if (!fullRecipe) {
continue;
}
const result = await this.#computeRelevanceScore(fullRecipe, {
fileSet,
entityNames,
depModules,
fileList: analysisData.fileList,
});
summary.totalAudited++;
summary[result.verdict]++;
summary.results.push(result);
// 执行衰退状态转换
if (result.verdict === 'dead' || result.verdict === 'severe' || result.verdict === 'decay') {
const executed = await this.#executeDecay(result);
if (result.verdict === 'dead') {
summary.immediateDeprecated += executed ? 1 : 0;
}
if (executed) {
summary.proposalsCreated++;
}
}
}
this.#logger.info('[RecipeRelevanceAuditor] Audit complete', {
total: summary.totalAudited,
healthy: summary.healthy,
watch: summary.watch,
decay: summary.decay,
severe: summary.severe,
dead: summary.dead,
});
return summary;
}
// ─── 内部方法 ─────────────────────────────────────────
/** 从 DB 加载完整 Recipe 数据 */
async #loadFullRecipe(id) {
try {
const entry = await this.#knowledgeRepo.findById(id);
if (!entry) {
return null;
}
return {
id: entry.id,
title: entry.title,
trigger: entry.trigger ?? '',
category: entry.category ?? '',
content: JSON.stringify(entry.content?.toJSON?.() ?? entry.content ?? {}),
doClause: entry.doClause ?? null,
coreCode: entry.coreCode ?? null,
};
}
catch {
return null;
}
}
/** 计算单个 Recipe 的 relevanceScore */
async #computeRelevanceScore(recipe, ctx) {
const category = recipe.category || '';
const weights = {
...DEFAULT_WEIGHTS,
...(CATEGORY_WEIGHT_OVERRIDES[category] || {}),
};
const decayReasons = [];
// 1. trigger 模式匹配
const triggerStillMatches = this.#checkTriggerMatch(recipe.trigger, ctx.fileList);
if (!triggerStillMatches) {
decayReasons.push(`触发条件 "${recipe.trigger}" 无匹配文件`);
}
// 2. 代码符号存活率
const referencedSymbols = this.#extractReferencedSymbols(recipe);
let symbolsAlive = 1.0;
if (referencedSymbols.length > 0) {
const aliveCount = referencedSymbols.filter((s) => ctx.entityNames.has(s.toLowerCase())).length;
symbolsAlive = aliveCount / referencedSymbols.length;
if (symbolsAlive < 0.5) {
decayReasons.push(`引用符号存活 ${aliveCount}/${referencedSymbols.length} (${(symbolsAlive * 100).toFixed(0)}%)`);
}
}
// 3. 依赖关系完整性
const referencedModules = this.#extractModuleReferences(recipe);
let depsIntact = true;
if (referencedModules.length > 0) {
const intactCount = referencedModules.filter((m) => ctx.depModules.has(m.toLowerCase())).length;
depsIntact = intactCount >= referencedModules.length * 0.5;
if (!depsIntact) {
decayReasons.push(`模块依赖 ${intactCount}/${referencedModules.length} 存活`);
}
}
// 4. 源代码文件存活率(来自 reasoning.sources + content.codeChanges)
const codeFiles = await this.#extractCodeFiles(recipe);
let codeFilesExist = 1.0;
if (codeFiles.length > 0) {
const existCount = codeFiles.filter((f) => ctx.fileSet.has(f.toLowerCase())).length;
codeFilesExist = existCount / codeFiles.length;
if (codeFilesExist < 0.5) {
decayReasons.push(`codeChanges 文件存活 ${existCount}/${codeFiles.length}`);
}
}
// 加权计算 relevanceScore
const relevanceScore = Math.round((triggerStillMatches ? 1 : 0) * weights.triggerStillMatches * 100 +
symbolsAlive * weights.symbolsAlive * 100 +
(depsIntact ? 1 : 0) * weights.depsIntact * 100 +
codeFilesExist * weights.codeFilesExist * 100);
// 分级判定
let verdict;
if (relevanceScore >= 80) {
verdict = 'healthy';
}
else if (relevanceScore >= 60) {
verdict = 'watch';
}
else if (relevanceScore >= 40) {
verdict = 'decay';
}
else if (relevanceScore >= 20) {
verdict = 'severe';
}
else {
verdict = 'dead';
}
return {
recipeId: recipe.id,
title: recipe.title,
relevanceScore,
verdict,
evidence: {
triggerStillMatches,
symbolsAlive,
depsIntact,
codeFilesExist,
},
decayReasons,
};
}
/** 检查 trigger 模式是否仍有匹配文件 */
#checkTriggerMatch(trigger, fileList) {
if (!trigger || trigger.trim() === '') {
return true; // 无 trigger 视为匹配
}
const triggerLower = trigger.toLowerCase();
// 检查 @trigger 格式 (如 @bilidili-api-response-model)
// 这些是自定义 trigger,不与文件路径匹配,视为始终有效
if (triggerLower.startsWith('@')) {
return true;
}
// 检查文件扩展名匹配 (如 "When creating .swift files")
const extMatch = triggerLower.match(/\.(swift|ts|tsx|js|jsx|py|java|kt|rb|go|rs|vue|svelte)\b/);
if (extMatch) {
const ext = extMatch[0];
return fileList.some((f) => f.toLowerCase().endsWith(ext));
}
// 检查路径模式匹配 (如 "When modifying Packages/")
const pathPatterns = trigger.match(/(?:[\w.-]+\/)+[\w.-]*/g) || [];
if (pathPatterns.length > 0) {
return pathPatterns.some((pattern) => {
const p = pattern.toLowerCase();
return fileList.some((f) => f.toLowerCase().includes(p));
});
}
// 无法判断时视为匹配
return true;
}
/** 从 Recipe content 中提取引用的符号 */
#extractReferencedSymbols(recipe) {
const symbols = [];
// 从 content JSON 中提取
try {
const content = JSON.parse(recipe.content || '{}');
const pattern = content.pattern || '';
const markdown = content.markdown || '';
const text = `${pattern} ${markdown} ${recipe.doClause || ''} ${recipe.coreCode || ''}`;
// 匹配 PascalCase 标识符 (类名/协议名)
const identifiers = text.match(/\b[A-Z][a-zA-Z0-9]{2,}\b/g) || [];
// 去掉常见英文单词
const COMMON_WORDS = new Set([
'When',
'Then',
'The',
'This',
'That',
'Use',
'Not',
'All',
'For',
'With',
'From',
'Each',
'Must',
'May',
'Can',
'Will',
'Has',
'Are',
'New',
'Set',
'Get',
'Add',
'Run',
'End',
'Try',
'Nil',
'True',
'False',
'Void',
'Self',
'Type',
'Error',
'Result',
'String',
'Int',
'Bool',
'Array',
'Dict',
'Optional',
'Protocol',
'Class',
'Struct',
'Enum',
'Import',
'Return',
'Override',
'Private',
'Public',
'Internal',
'Func',
'Var',
'Let',
'Guard',
'Async',
'Await',
'Throws',
'Release',
'Debug',
'Swift',
'Function',
'Method',
'Property',
'Value',
'Default',
'Shared',
'Static',
'Final',
'Weak',
'Lazy',
]);
for (const id of identifiers) {
if (!COMMON_WORDS.has(id) && id.length >= 3) {
symbols.push(id);
}
}
}
catch {
/* invalid JSON */
}
// 去重
return [...new Set(symbols)];
}
/** 从 Recipe 中提取模块/依赖引用 */
#extractModuleReferences(recipe) {
const modules = [];
try {
const content = JSON.parse(recipe.content || '{}');
const text = `${content.markdown || ''} ${content.pattern || ''} ${recipe.doClause || ''}`;
// 匹配 import 语句中的模块名
const importMatches = text.match(/import\s+(\w+)/g) || [];
for (const m of importMatches) {
const name = m.replace(/^import\s+/, '');
if (name.length >= 2) {
modules.push(name);
}
}
}
catch {
/* invalid JSON */
}
return [...new Set(modules)];
}
/** 从 Recipe 中提取 codeChanges 引用的文件路径 */
async #extractCodeFiles(recipe) {
const files = [];
try {
const content = JSON.parse(recipe.content || '{}');
const codeChanges = content.codeChanges;
if (Array.isArray(codeChanges)) {
for (const change of codeChanges) {
if (change.file) {
files.push(change.file);
}
}
}
}
catch {
/* invalid JSON */
}
// reasoning.sources 在 entry 的 reasoning 属性中
try {
const entry = await this.#knowledgeRepo.findById(recipe.id);
if (entry?.reasoning) {
const reasoning = (typeof entry.reasoning === 'object' ? entry.reasoning : {});
if (Array.isArray(reasoning.sources)) {
for (const src of reasoning.sources) {
if (typeof src === 'string') {
files.push(src);
}
else if (src?.file) {
files.push(src.file);
}
else if (src?.path) {
files.push(src.path);
}
}
}
}
}
catch {
/* entry not found */
}
return [...new Set(files)];
}
/** 执行衰退状态转换 */
async #executeDecay(result) {
try {
const now = Date.now();
switch (result.verdict) {
case 'dead': {
await this.#knowledgeRepo.updateLifecycle(result.recipeId, 'deprecated');
await this.#createProposal({
targetRecipeId: result.recipeId,
type: 'deprecate',
source: 'rescan-relevance-audit',
status: 'executed',
description: `[Rescan Relevance Audit] Score: ${result.relevanceScore}. ${result.decayReasons.join('; ')}`,
evidence: { relevanceScore: result.relevanceScore, evidence: result.evidence },
expiresAt: now,
});
this.#logger.info(`[RecipeRelevanceAuditor] DEAD: "${result.title}" → deprecated (score: ${result.relevanceScore})`);
return true;
}
case 'severe': {
await this.#knowledgeRepo.updateLifecycle(result.recipeId, 'decaying');
await this.#createProposal({
targetRecipeId: result.recipeId,
type: 'deprecate',
source: 'rescan-relevance-audit',
status: 'observing',
description: `[Rescan Relevance Audit] Score: ${result.relevanceScore}. ${result.decayReasons.join('; ')}`,
evidence: { relevanceScore: result.relevanceScore, evidence: result.evidence },
expiresAt: now + GRACE_PERIOD_SEVERE,
});
this.#logger.info(`[RecipeRelevanceAuditor] SEVERE: "${result.title}" → decaying (3d grace, score: ${result.relevanceScore})`);
return true;
}
case 'decay': {
await this.#knowledgeRepo.updateLifecycle(result.recipeId, 'decaying');
await this.#createProposal({
targetRecipeId: result.recipeId,
type: 'deprecate',
source: 'rescan-relevance-audit',
status: 'observing',
description: `[Rescan Relevance Audit] Score: ${result.relevanceScore}. ${result.decayReasons.join('; ')}`,
evidence: { relevanceScore: result.relevanceScore, evidence: result.evidence },
expiresAt: now + GRACE_PERIOD_DECAY,
});
this.#logger.info(`[RecipeRelevanceAuditor] DECAY: "${result.title}" → decaying (7d grace, score: ${result.relevanceScore})`);
return true;
}
default:
return false;
}
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.#logger.warn(`[RecipeRelevanceAuditor] executeDecay failed for ${result.recipeId}: ${msg}`);
return false;
}
}
/** 创建 evolution proposal */
async #createProposal(input) {
try {
if (this.#proposalRepo) {
this.#proposalRepo.create({
type: input.type,
targetRecipeId: input.targetRecipeId,
relatedRecipeIds: [],
confidence: 0.95,
source: input.source,
description: input.description,
evidence: [input.evidence],
status: input.status,
expiresAt: input.expiresAt,
});
}
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
this.#logger.warn(`[RecipeRelevanceAuditor] createProposal failed: ${msg}`);
}
}
}