autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
277 lines (276 loc) • 10.5 kB
JavaScript
/**
* ReverseGuard — Recipe→Code 反向验证
*
* 正向: 代码 → Guard → "代码是否符合知识?" ✅ 已有
* 反向: Recipe → Guard → "知识是否还符合代码?" ← 本文件
*
* 对每条 active rule Recipe:
* 1. 提取 coreCode 中的 API 引用(类名、方法名)
* 2. 在 code_entities 表中查找这些符号
* 3. 符号不存在 → PatternDrift
* 4. 提取 guard regex pattern → 对项目代码运行匹配
* 5. 匹配率骤降 → 代码模式正在迁移
*/
import Logger from '../../infrastructure/logging/Logger.js';
/* ────────────────────── 常量 ────────────────────── */
/** 从 coreCode 中提取符号引用的正则(多语言通用) */
const SYMBOL_PATTERNS = [
// ClassName.method / ClassName.shared / ClassName() — Swift/Java/Kotlin/TS/Dart/C#
/\b([A-Z][A-Za-z0-9_]+)\s*[.(]/g,
// [ClassName method] — ObjC 消息发送
/\[\s*([A-Z][A-Za-z0-9_]+)\s+\w/g,
// import/from 引用 — JS/TS/Python/Dart/Go
/(?:import|from)\s+['"]([^'"]+)['"]/g,
// #import / #include — ObjC/C/C++ 头文件引用
/^\s*#(?:import|include)\s+[<"]([^>"]+)[>"]/gm,
// package.function / module::Type — Go/Rust 限定名
/\b([a-z][a-z0-9_]+(?:::[A-Z][A-Za-z0-9_]+|\.[A-Z][A-Za-z0-9_]+))/g,
// @decorator — Python/TS decorator 引用
/@([A-Z][A-Za-z0-9_]+)/g,
];
/** 定义多少条 drift signal 算 investigate / decay */
const DRIFT_THRESHOLDS = {
/** ≥1 high → investigate */
INVESTIGATE_HIGH: 1,
/** ≥2 high → decay */
DECAY_HIGH: 2,
/** ≥3 medium → investigate */
INVESTIGATE_MEDIUM: 3,
};
/* ────────────────────── Class ────────────────────── */
export class ReverseGuard {
#knowledgeRepo;
#entityRepo;
#sourceRefRepo;
#signalBus;
#logger = Logger.getInstance();
constructor(knowledgeRepo, entityRepo, sourceRefRepo, options = {}) {
this.#knowledgeRepo = knowledgeRepo;
this.#entityRepo = entityRepo;
this.#sourceRefRepo = sourceRefRepo;
this.#signalBus = options.signalBus ?? null;
}
/**
* 对一条 Recipe 执行反向验证
*/
checkRecipe(recipe, projectFiles) {
const signals = [];
// 1. 检查 coreCode 中引用的符号是否还存在于代码库
if (recipe.core_code) {
signals.push(...this.#checkSymbolExistence(recipe.core_code));
}
// 2. 检查 guard pattern 在项目代码中的匹配率
if (recipe.guard_pattern) {
signals.push(...this.#checkPatternMatchRate(recipe.id, recipe.guard_pattern, projectFiles));
}
// 3. 检查 sourceRef 路径是否失效(与 SourceRefReconciler 数据交叉验证)
signals.push(...this.#checkSourceRefStaleness(recipe.id));
// 4. 综合判定
const recommendation = this.#computeRecommendation(signals);
// 5. 发射信号
if (this.#signalBus && signals.length > 0) {
const severity = recommendation === 'decay' ? 1 : recommendation === 'investigate' ? 0.5 : 0;
this.#signalBus.send('quality', 'ReverseGuard', severity, {
target: recipe.id,
metadata: {
signalCount: signals.length,
recommendation,
driftTypes: [...new Set(signals.map((s) => s.type))],
},
});
}
return {
recipeId: recipe.id,
title: recipe.title,
signals,
recommendation,
};
}
/**
* 批量对所有 active rule Recipes 执行反向验证
*/
auditAllRules(projectFiles) {
const recipes = this.#loadActiveRuleRecipes();
const results = [];
for (const recipe of recipes) {
try {
results.push(this.checkRecipe(recipe, projectFiles));
}
catch (err) {
this.#logger.debug(`ReverseGuard: failed to check recipe ${recipe.id}: ${err.message}`);
}
}
return results;
}
/**
* 获取需要调查/衰退的 Recipe 结果
*/
getDriftResults(results) {
return results.filter((r) => r.recommendation !== 'healthy');
}
/* ── 内部方法 ── */
#loadActiveRuleRecipes() {
try {
const rows = this.#knowledgeRepo.findActiveRulesWithContentSync();
return rows.map((r) => ({
id: r.id,
title: r.title,
core_code: r.coreCode ?? null,
guard_pattern: r.guardPattern ?? null,
stats: r.stats ?? null,
}));
}
catch {
return [];
}
}
/**
* 检查 coreCode 中引用的符号是否存在于 code_entities 表
*/
#checkSymbolExistence(coreCode) {
const symbols = this.#extractSymbols(coreCode);
if (symbols.size === 0) {
return [];
}
const signals = [];
for (const symbol of symbols) {
try {
const exists = this.#entityRepo.existsByName(symbol);
if (!exists) {
signals.push({
type: 'symbol_missing',
detail: `Symbol "${symbol}" referenced in recipe coreCode not found in codebase`,
severity: 'high',
evidence: { expectedSymbol: symbol },
});
}
}
catch {
// code_entities 表不存在时静默跳过
break;
}
}
return signals;
}
/**
* 检查 guard pattern 在项目代码中的匹配情况
*/
#checkPatternMatchRate(recipeId, guardPattern, projectFiles) {
let re;
try {
re = new RegExp(guardPattern, 'gm');
}
catch {
return []; // 无效正则,不产出 drift(由 UncertaintyCollector 处理)
}
// 统计当前匹配数
let currentMatches = 0;
for (const file of projectFiles) {
const matches = file.content.match(re);
if (matches) {
currentMatches += matches.length;
}
}
// 获取历史匹配率(从 stats.guardHits 推断)
const historicalHits = this.#getHistoricalHits(recipeId);
const signals = [];
if (currentMatches === 0 && projectFiles.length > 0) {
// 完全匹配不到 — 场景已不存在
signals.push({
type: 'zero_match',
detail: `Guard pattern matches 0 times across ${projectFiles.length} files — scenario may no longer exist`,
severity: 'high',
evidence: {
matchRate: { current: 0, historical: historicalHits },
},
});
}
else if (historicalHits > 0 && currentMatches > 0) {
// 匹配率大幅下降
const dropRatio = currentMatches / historicalHits;
if (dropRatio < 0.3) {
signals.push({
type: 'match_rate_drop',
detail: `Guard pattern match count dropped significantly: ${currentMatches} current vs ${historicalHits} historical (${Math.round(dropRatio * 100)}%)`,
severity: 'medium',
evidence: {
matchRate: { current: currentMatches, historical: historicalHits },
},
});
}
}
return signals;
}
/**
* 检查 recipe_source_refs 中是否有 stale 条目(与 SourceRefReconciler 数据交叉验证)
*/
#checkSourceRefStaleness(recipeId) {
try {
const staleRefs = this.#sourceRefRepo
.findByRecipeId(recipeId)
.filter((r) => r.status === 'stale');
if (staleRefs.length === 0) {
return [];
}
return [
{
type: 'source_ref_stale',
detail: `${staleRefs.length} source file(s) no longer exist: ${staleRefs
.slice(0, 3)
.map((r) => r.sourcePath)
.join(', ')}${staleRefs.length > 3 ? ` (+${staleRefs.length - 3} more)` : ''}`,
severity: staleRefs.length >= 3 ? 'high' : 'medium',
evidence: {},
},
];
}
catch {
return [];
}
}
#extractSymbols(coreCode) {
const symbols = new Set();
for (const pattern of SYMBOL_PATTERNS) {
// 重置 lastIndex(全局正则需要重置)
const re = new RegExp(pattern.source, pattern.flags);
let match;
while ((match = re.exec(coreCode)) !== null) {
const symbol = match[1];
if (!symbol || symbol.length < 3) {
continue;
}
// 过滤纯小写短词(if/for/var 等关键词),但允许含大写、含分隔符(::/.)、含路径分隔符(/)的符号
if (/^[a-z]+$/.test(symbol) && symbol.length < 6) {
continue;
}
symbols.add(symbol);
}
}
return symbols;
}
#getHistoricalHits(recipeId) {
try {
return this.#knowledgeRepo.getGuardHitsSync(recipeId);
}
catch {
return 0;
}
}
#computeRecommendation(signals) {
if (signals.length === 0) {
return 'healthy';
}
const highCount = signals.filter((s) => s.severity === 'high').length;
const mediumCount = signals.filter((s) => s.severity === 'medium').length;
if (highCount >= DRIFT_THRESHOLDS.DECAY_HIGH) {
return 'decay';
}
if (highCount >= DRIFT_THRESHOLDS.INVESTIGATE_HIGH) {
return 'investigate';
}
if (mediumCount >= DRIFT_THRESHOLDS.INVESTIGATE_MEDIUM) {
return 'investigate';
}
return 'healthy';
}
}