feat(growth): 지식 재구성 — 충돌 통합 초안 + A-MEM 레슨 네트워크 (v2.2.228)
ASTRA 자기 제안(검증 통과분) 1순위 구현: 충돌을 '감지'에서 '재구성'으로. [충돌 → 통합 초안 (사람 승인 대기)] - conflictScan: 모순 감지 시 LLM이 통합 초안 생성 → .astra/growth/reconcile/ (런당 ≤3건). 신뢰 권고 우선 쪽 기준 + 타방의 유효 정보 보존 + 판단 불가 사실은 "(확인 필요: A는 X, B는 Y)" 병기 + 출처 표기 강제. - 자동 반영 절대 없음 — 초안 머리에 명기, 승인 시 사람이 직접 반영 (status: pending-review). 거부 = 파일 삭제. [A-MEM 레슨 네트워크 (NeurIPS 2025 이식, deep research 2순위)] - lessonNetwork.ts: 새 레슨 저장 시 기존 레슨과 토큰 자카드 유사도 상위 3개를 "## 관련 레슨" [[위키링크]]로 연결 + 기존 레슨에 백링크(역방향 갱신 — memory evolution). LLM 호출 0 — 캡처 경로 지연 없음. 멱등(재실행 안전). - 연결 지점: Correction Loop 자동 레슨 + 수동 레슨 생성(Astra: New Lesson). 고립된 카드 모음 → 상호 연결 네트워크: "같은 종류의 실수" 패턴이 파일 수준에서 보이고 RAG 위키링크로 함께 검색됨. 테스트 6건 추가 (유사도·링크 멱등·양방향·초안 형식). 전체 588 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,9 @@ import { DIGEST_DIR } from './sleepDigest';
|
||||
|
||||
const STATE_REL = path.join('.astra', 'growth', 'conflict-scan-state.json');
|
||||
const REPORT_REL = path.join('.astra', 'growth', 'conflict-report.md');
|
||||
const RECONCILE_DIR_REL = path.join('.astra', 'growth', 'reconcile');
|
||||
const MAX_COMPARES_PER_RUN = 5;
|
||||
const MAX_RECONCILE_PER_RUN = 3;
|
||||
const NEIGHBOR_K = 2;
|
||||
|
||||
/** 스캔 제외 — 소화 노트·레슨·자동 생성 인벤토리는 지식 본문이 아니다. */
|
||||
@@ -51,6 +53,41 @@ function saveLastScanMs(brainPath: string, ms: number): void {
|
||||
} catch { /* 상태 저장 실패 — 다음 런이 더 넓게 스캔할 뿐 */ }
|
||||
}
|
||||
|
||||
// ── 지식 재구성 — 충돌을 감지에서 끝내지 않고 통합 초안까지 (적용은 사람) ──────
|
||||
|
||||
const RECONCILE_SYSTEM = [
|
||||
'너는 지식 통합 편집자다. 같은 주제에 대해 모순되는 두 위키 문서를 받아, 모순을 해소한 *통합 초안*을 작성한다.',
|
||||
'규칙:',
|
||||
'1. [신뢰 권고]가 우선하는 쪽의 사실을 기준으로 삼되, 다른 쪽의 유효한(모순되지 않는) 정보는 보존하라.',
|
||||
'2. 어느 쪽이 맞는지 문서만으로 판단 불가능한 사실은 양쪽을 병기하고 "(확인 필요: A는 X, B는 Y)" 로 표시하라.',
|
||||
'3. 원문에 없는 내용을 지어내지 마라. 출처 문서 제목을 [제목] 으로 표기하라.',
|
||||
'4. 마크다운 본문만 출력 (frontmatter·해설 없이).',
|
||||
].join('\n');
|
||||
|
||||
/** 통합 초안 파일 본문 (순수 — 테스트 가능). 적용은 사람이 검토 후 직접. */
|
||||
export function reconcileDraftMarkdown(f: ConflictFinding, body: string, nowIso: string): string {
|
||||
return [
|
||||
'---',
|
||||
'type: reconcile-draft',
|
||||
`created_at: ${nowIso}`,
|
||||
`conflict_new: "${f.newDoc.replace(/"/g, "'")}"`,
|
||||
`conflict_existing: "${f.existingDoc.replace(/"/g, "'")}"`,
|
||||
'status: pending-review',
|
||||
'---',
|
||||
'',
|
||||
`# 통합 초안: ${path.basename(f.newDoc, '.md')} ↔ ${path.basename(f.existingDoc, '.md')}`,
|
||||
'',
|
||||
`> ⚔️ 모순: ${f.summary}`,
|
||||
`> ${f.recommend}`,
|
||||
'> ',
|
||||
'> ✋ **이 초안은 자동 반영되지 않습니다.** 검토 후 승인하면 아래 본문을 대상 문서에 직접 반영하고,',
|
||||
'> 다른 쪽 문서를 수정/삭제하세요. 거부하면 이 파일을 삭제하면 됩니다.',
|
||||
'',
|
||||
body.trim(),
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const COMPARE_SYSTEM = [
|
||||
'너는 지식 정합성 검사기다. [신규 문서]와 [기존 문서]가 같은 주제에 대해 *모순되는 사실*을 말하는지 판정하라.',
|
||||
'모순 = 같은 대상에 대해 양립 불가능한 수치·날짜·결론·정의. 관점 차이·세부 수준 차이·다른 주제는 모순이 아니다.',
|
||||
@@ -63,6 +100,8 @@ export interface ConflictFinding {
|
||||
summary: string;
|
||||
/** 신뢰도 비교 기반 우선 권고 (자동 삭제는 하지 않음 — 결정은 사람). */
|
||||
recommend: string;
|
||||
/** 생성된 통합 초안의 두뇌 상대 경로 (생성 실패/상한 초과 시 없음). */
|
||||
draftPath?: string;
|
||||
}
|
||||
|
||||
// ── 신뢰도 비교 — frontmatter(source_trust_level·confidence_score) + 최신성 ──
|
||||
@@ -132,6 +171,7 @@ export async function runConflictScanOnce(): Promise<string> {
|
||||
const orchestrator = new RetrievalOrchestrator();
|
||||
const findings: ConflictFinding[] = [];
|
||||
let compared = 0;
|
||||
let reconciled = 0;
|
||||
|
||||
for (const t of targets) {
|
||||
try {
|
||||
@@ -158,12 +198,41 @@ export async function runConflictScanOnce(): Promise<string> {
|
||||
if (parsed?.conflict === true && String(parsed?.summary || '').trim()) {
|
||||
let existingMtime = 0;
|
||||
try { existingMtime = fs.statSync(n.filePath).mtimeMs; } catch { /* 0 유지 */ }
|
||||
findings.push({
|
||||
const finding: ConflictFinding = {
|
||||
newDoc: rel,
|
||||
existingDoc: n.relativePath,
|
||||
summary: String(parsed.summary).slice(0, 200),
|
||||
recommend: buildTrustRecommendation(parseDocTrust(content, t.m), parseDocTrust(existing, existingMtime)),
|
||||
});
|
||||
};
|
||||
findings.push(finding);
|
||||
|
||||
// [지식 재구성] 감지에서 끝내지 않고 통합 초안 생성 → 사람 승인 대기.
|
||||
// 양쪽 본문이 지금 스코프에 있을 때 바로 생성 (런당 상한 — LLM 시간 보호).
|
||||
if (reconciled < MAX_RECONCILE_PER_RUN) {
|
||||
try {
|
||||
const draftBody = await simpleChatCompletion(
|
||||
RECONCILE_SYSTEM,
|
||||
[
|
||||
`[모순] ${finding.summary}`,
|
||||
`[신뢰 권고] ${finding.recommend}`,
|
||||
`[문서 A — 신규: ${path.basename(rel, '.md')}]\n${content.slice(0, 4000)}`,
|
||||
`[문서 B — 기존: ${path.basename(n.relativePath, '.md')}]\n${existing.slice(0, 4000)}`,
|
||||
].join('\n\n'),
|
||||
{ baseUrl: config.ollamaUrl, model: config.defaultModel, temperature: 0.2, maxTokens: 1600, timeoutMs: 180000 },
|
||||
);
|
||||
if (draftBody.trim()) {
|
||||
const dir = path.join(brainPath, RECONCILE_DIR_REL);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const slug = path.basename(rel, '.md').replace(/[^a-zA-Z0-9가-힣_-]+/g, '-').slice(0, 50);
|
||||
const draftFile = path.join(dir, `${new Date().toISOString().slice(0, 10)}-${slug}.md`);
|
||||
fs.writeFileSync(draftFile, reconcileDraftMarkdown(finding, draftBody, new Date().toISOString()), 'utf8');
|
||||
finding.draftPath = path.relative(brainPath, draftFile);
|
||||
reconciled++;
|
||||
}
|
||||
} catch (e: any) {
|
||||
logError('통합 초안 생성 실패 (감지 결과는 유지).', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -177,7 +246,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 → ${f.recommend}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`),
|
||||
...findings.map(f => `- ⚔️ 신규 **${f.newDoc}** ↔ 기존 **${f.existingDoc}**: ${f.summary}\n → ${f.recommend}${f.draftPath ? `\n → 📝 통합 초안: \`${f.draftPath}\` (검토 후 승인 시 직접 반영)` : ''}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`),
|
||||
].join('\n');
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(reportFile), { recursive: true });
|
||||
@@ -188,7 +257,7 @@ export async function runConflictScanOnce(): Promise<string> {
|
||||
`⚔️ 지식 충돌 ${findings.length}건 감지 — 예: "${findings[0].newDoc}" ↔ "${findings[0].existingDoc}". conflict-report.md 에서 확인하세요.`,
|
||||
);
|
||||
}
|
||||
const summary = `신규 ${targets.length}건 스캔 · 비교 ${compared}회 · 충돌 ${findings.length}건`;
|
||||
const summary = `신규 ${targets.length}건 스캔 · 비교 ${compared}회 · 충돌 ${findings.length}건${reconciled ? ` · 통합 초안 ${reconciled}건 (reconcile/)` : ''}`;
|
||||
logInfo('지식 충돌 스캔 완료.', { summary });
|
||||
return summary;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user