autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
247 lines (246 loc) • 8.1 kB
JavaScript
/**
* ContradictionDetector — Recipe 级矛盾检测
*
* 从 MemoryConsolidator 提升:Memory 层只做 session 内去重,
* Recipe 层做跨 lifecycle 的持久化矛盾检测。
*
* 检测维度:
* 1. 否定模式检测(中/英双语 negation patterns)
* 2. 主题词重叠 ≥ 30% Jaccard
* 3. doClause vs dontClause 交叉引用
* 4. guard regex 互斥检测
*
* 结果:硬矛盾 (confidence ≥ 0.8) / 软矛盾 (0.4-0.8)
*/
var _a;
import { CONSUMABLE_LIFECYCLES } from '../../domain/knowledge/Lifecycle.js';
import Logger from '../../infrastructure/logging/Logger.js';
/* ────────────────────── Constants ────────────────────── */
const NEGATION_PATTERNS_ZH = /不(再)?使用|禁止|废弃|移除|取消|停止|不要|不采用|弃用|淘汰/;
const NEGATION_PATTERNS_EN = /\b(don'?t|do\s+not|never|no\s+longer|removed?|deprecated?|stop|avoid|disable|abandon|drop)\b/i;
const MIN_TOPIC_OVERLAP_WORDS = 2;
const MIN_TOPIC_OVERLAP_RATIO = 0.3;
const STOP_WORDS = new Set([
'我们',
'使用',
'项目',
'需要',
'可以',
'应该',
'建议',
'目前',
'已经',
'这个',
'那个',
'一个',
'进行',
'通过',
'对于',
'the',
'is',
'are',
'was',
'were',
'be',
'been',
'have',
'has',
'had',
'do',
'does',
'did',
'will',
'would',
'could',
'should',
'may',
'might',
'can',
'this',
'that',
'these',
'those',
'for',
'and',
'but',
'with',
'not',
'from',
'use',
'all',
'any',
]);
/* ────────────────────── Class ────────────────────── */
export class ContradictionDetector {
#knowledgeRepo;
#signalBus;
#logger = Logger.getInstance();
constructor(knowledgeRepo, options = {}) {
this.#knowledgeRepo = knowledgeRepo;
this.#signalBus = options.signalBus ?? null;
}
/**
* 检测所有 active/staging/evolving Recipe 之间的矛盾
*/
async detectAll() {
const recipes = await this.#loadRecipes();
const results = [];
for (let i = 0; i < recipes.length; i++) {
for (let j = i + 1; j < recipes.length; j++) {
const result = this.detectPair(recipes[i], recipes[j]);
if (result) {
results.push(result);
}
}
}
// 发射矛盾信号
if (this.#signalBus && results.length > 0) {
for (const r of results) {
this.#signalBus.send('lifecycle', 'ContradictionDetector', r.confidence, {
target: r.recipeA,
metadata: {
contradictsWith: r.recipeB,
type: r.type,
evidence: r.evidence,
},
});
}
}
this.#logger.debug(`ContradictionDetector: found ${results.length} contradictions`);
return results;
}
/**
* 检测两条 Recipe 是否矛盾
*/
detectPair(a, b) {
const evidence = [];
let score = 0;
// 维度 1: 否定模式 + 主题重叠
const textA = [a.title, a.description, a.doClause, a.dontClause, a.content_markdown]
.filter(Boolean)
.join(' ');
const textB = [b.title, b.description, b.doClause, b.dontClause, b.content_markdown]
.filter(Boolean)
.join(' ');
if (this.#hasNegationConflict(textA, textB)) {
evidence.push('negation_pattern_conflict');
score += 0.4;
}
// 维度 2: doClause vs dontClause 交叉引用
if (a.doClause && b.dontClause && this.#hasTopicOverlap(a.doClause, b.dontClause)) {
evidence.push('doClause_vs_dontClause_cross');
score += 0.3;
}
if (b.doClause && a.dontClause && this.#hasTopicOverlap(b.doClause, a.dontClause)) {
evidence.push('dontClause_vs_doClause_cross');
score += 0.3;
}
// 维度 3: guard regex 互斥检测
if (a.guardPattern && b.guardPattern) {
if (this.#areRegexMutuallyExclusive(a.guardPattern, b.guardPattern)) {
evidence.push('guard_regex_mutual_exclusive');
score += 0.2;
}
}
if (evidence.length === 0) {
return null;
}
const confidence = Math.min(1, score);
const type = confidence >= 0.8 ? 'hard' : 'soft';
return {
recipeA: a.id,
recipeB: b.id,
confidence,
type,
evidence,
};
}
/* ── Internal ── */
async #loadRecipes() {
try {
const entries = await this.#knowledgeRepo.findAllByLifecycles(CONSUMABLE_LIFECYCLES);
return entries.map((e) => ({
id: e.id,
title: e.title,
lifecycle: e.lifecycle,
doClause: e.doClause || null,
dontClause: e.dontClause || null,
guardPattern: e.content?.pattern || null,
description: e.description || null,
content_markdown: e.content?.markdown || null,
}));
}
catch {
return [];
}
}
#hasNegationConflict(textA, textB) {
if (!textA || !textB) {
return false;
}
const aNeg = NEGATION_PATTERNS_ZH.test(textA) || NEGATION_PATTERNS_EN.test(textA);
const bNeg = NEGATION_PATTERNS_ZH.test(textB) || NEGATION_PATTERNS_EN.test(textB);
// 同极性不算矛盾
if (aNeg === bNeg) {
return false;
}
return this.#hasTopicOverlap(textA, textB);
}
#hasTopicOverlap(textA, textB) {
const wordsA = _a.extractTopicWords(textA);
const wordsB = _a.extractTopicWords(textB);
let overlap = 0;
for (const w of wordsA) {
if (wordsB.has(w)) {
overlap++;
}
}
const minSize = Math.min(wordsA.size, wordsB.size);
if (minSize === 0) {
return false;
}
return overlap >= MIN_TOPIC_OVERLAP_WORDS || overlap / minSize >= MIN_TOPIC_OVERLAP_RATIO;
}
#areRegexMutuallyExclusive(patternA, patternB) {
// 简单启发式:如果两个 pattern 的核心词完全相同但一个含否定前缀
// 例如 "use.*SnapKit" vs "(?!.*SnapKit)" 或 "avoid.*SnapKit"
try {
const coreA = patternA
.replace(/[\\^$.*+?()[\]{}|]/g, ' ')
.trim()
.toLowerCase();
const coreB = patternB
.replace(/[\\^$.*+?()[\]{}|]/g, ' ')
.trim()
.toLowerCase();
const wordsA = new Set(coreA.split(/\s+/).filter((w) => w.length >= 3));
const wordsB = new Set(coreB.split(/\s+/).filter((w) => w.length >= 3));
let overlap = 0;
for (const w of wordsA) {
if (wordsB.has(w)) {
overlap++;
}
}
// 高重叠 + 一个含否定前瞻
if (overlap >= 2 && (patternA.includes('(?!') || patternB.includes('(?!'))) {
return true;
}
}
catch {
// regex parsing error
}
return false;
}
/** 提取主题词(公开为静态方法,供 RedundancyAnalyzer 复用) */
static extractTopicWords(text) {
if (!text) {
return new Set();
}
const tokens = text
.toLowerCase()
.split(/[\s,;:!?。,;:!?\-_/\\|()[\]{}'"<>·、]+/)
.filter((t) => t.length >= 2);
return new Set(tokens.filter((t) => !STOP_WORDS.has(t)));
}
}
_a = ContradictionDetector;