feat(self): 자기 평가 질의에 기능 인벤토리 강제 주입 + 충돌 신뢰도 비교 권고 (v2.2.226)

[자기 지식 구식화 — 마지막 구멍 봉쇄]
인벤토리를 자동 생성해도(v2.2.225) 모델이 검색 없이 기억으로 답하면 무용
— 실사례: 답변 말미 "출처: 모델 지식 (검색 미사용)" 후 이미 있는 기능
(CoVe·멀티스텝 플래닝·노후점검 자동화)을 신규 제안. 프롬프트 규칙은 검색을
강제할 수 없으므로 scheduleContext 와 동일 패턴으로 해결:
- selfAssessContext: "기능 개선/고도화/self-evolving/무슨 기능" 류 질의 감지
  시 인벤토리 전문을 RAG 경쟁 없이 결정론적 주입 + "이미 있는 기능 신규 제안
  금지, '현재 X 있음 — 빠진 증분 Y' 형태" 지시. 인벤토리 미생성 시 정직 안내.

[충돌 해결사 — 권고까지만, 자동 결정은 안 함]
- conflictScan 에 신뢰도 비교 추가: 양쪽 frontmatter(source_trust_level S~D,
  confidence_score) + 최신성으로 "신규/기존 우선 권고" 생성. 메타데이터 없거나
  비등하면 권고 보류 (근거 없는 권고 금지). 삭제·덮어쓰기는 여전히 사람 결정.

테스트 17건 추가 (질의 감지·인벤토리 주입·신뢰 파싱·권고 분기).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 11:59:05 +09:00
parent b03a49bfc3
commit 960f43f643
6 changed files with 192 additions and 5 deletions
+51 -2
View File
@@ -61,6 +61,48 @@ export interface ConflictFinding {
newDoc: string;
existingDoc: string;
summary: string;
/** 신뢰도 비교 기반 우선 권고 (자동 삭제는 하지 않음 — 결정은 사람). */
recommend: string;
}
// ── 신뢰도 비교 — frontmatter(source_trust_level·confidence_score) + 최신성 ──
export interface DocTrust {
trustLevel: string; // S/A/B/C/D 또는 ''
confidence: number; // 0~1, 미상 -1
mtimeMs: number;
}
const TRUST_RANK: Record<string, number> = { S: 4, A: 3, B: 2, C: 1, D: 0 };
/** 문서 머리 frontmatter 에서 신뢰 메타데이터 추출 (없으면 빈 값). */
export function parseDocTrust(content: string, mtimeMs: number): DocTrust {
const head = (content || '').slice(0, 1200);
const trust = head.match(/^source_trust_level:\s*["']?([SABCD])["']?\s*$/mi)?.[1]?.toUpperCase() || '';
const confRaw = head.match(/^confidence_score:\s*["']?([\d.]+)["']?\s*$/mi)?.[1];
const confidence = confRaw !== undefined && Number.isFinite(Number(confRaw)) ? Math.max(0, Math.min(1, Number(confRaw))) : -1;
return { trustLevel: trust, confidence, mtimeMs };
}
const fmtTrust = (t: DocTrust) =>
`${t.trustLevel || '등급없음'}·${t.confidence >= 0 ? t.confidence.toFixed(1) : '점수없음'}`;
/**
* 충돌 양쪽의 신뢰도를 비교해 우선 권고 문자열 생성.
* 점수 = 신뢰등급(0~4)×2 + confidence(0~1)×2 + 최신성(+1). 메타데이터가 양쪽 다
* 없으면 권고 보류 — 근거 없는 권고는 하지 않는다.
*/
export function buildTrustRecommendation(newer: DocTrust, older: DocTrust): string {
const hasMetaN = !!newer.trustLevel || newer.confidence >= 0;
const hasMetaO = !!older.trustLevel || older.confidence >= 0;
if (!hasMetaN && !hasMetaO) return '권고 보류 — 양쪽 모두 신뢰 메타데이터 없음 (내용으로 직접 판단 필요)';
const score = (t: DocTrust, isNewer: boolean) =>
(TRUST_RANK[t.trustLevel] ?? 0) * 2 + (t.confidence >= 0 ? t.confidence : 0) * 2 + (isNewer ? 1 : 0);
const sNew = score(newer, true), sOld = score(older, false);
if (Math.abs(sNew - sOld) < 1) return `권고 보류 — 신뢰도 비등 (신규 ${fmtTrust(newer)} vs 기존 ${fmtTrust(older)})`;
return sNew > sOld
? `권고: **신규 우선** (신규 ${fmtTrust(newer)}·최신 vs 기존 ${fmtTrust(older)})`
: `권고: **기존 우선** (기존 ${fmtTrust(older)} vs 신규 ${fmtTrust(newer)}·최신)`;
}
/** 1회 스캔 실행. 반환: 요약 문자열. */
@@ -114,7 +156,14 @@ export async function runConflictScanOnce(): Promise<string> {
if (!m) continue;
const parsed = JSON.parse(m[0]);
if (parsed?.conflict === true && String(parsed?.summary || '').trim()) {
findings.push({ newDoc: rel, existingDoc: n.relativePath, summary: String(parsed.summary).slice(0, 200) });
let existingMtime = 0;
try { existingMtime = fs.statSync(n.filePath).mtimeMs; } catch { /* 0 유지 */ }
findings.push({
newDoc: rel,
existingDoc: n.relativePath,
summary: String(parsed.summary).slice(0, 200),
recommend: buildTrustRecommendation(parseDocTrust(content, t.m), parseDocTrust(existing, existingMtime)),
});
}
}
} catch (e: any) {
@@ -128,7 +177,7 @@ export async function runConflictScanOnce(): Promise<string> {
const reportFile = path.join(brainPath, REPORT_REL);
const block = [
`\n## ${new Date().toLocaleString()} — 충돌 ${findings.length}`,
...findings.map(f => `- ⚔️ 신규 **${f.newDoc}** ↔ 기존 **${f.existingDoc}**: ${f.summary}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`),
...findings.map(f => `- ⚔️ 신규 **${f.newDoc}** ↔ 기존 **${f.existingDoc}**: ${f.summary}\n → ${f.recommend}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`),
].join('\n');
try {
fs.mkdirSync(path.dirname(reportFile), { recursive: true });