diff --git a/package-lock.json b/package-lock.json index c435aea..ac533ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "astra", - "version": "2.2.224", + "version": "2.2.225", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "astra", - "version": "2.2.224", + "version": "2.2.225", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", diff --git a/package.json b/package.json index 7293912..0704b1f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.224", + "version": "2.2.225", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", @@ -78,6 +78,10 @@ "command": "g1nation.sleepDigest.runNow", "title": "Astra: 지식 사전 소화 지금 실행 (Sleep-time Digest)" }, + { + "command": "g1nation.conflictScan.runNow", + "title": "Astra: 지식 충돌 스캔 지금 실행 (신규 문서 ↔ 기존 지식)" + }, { "command": "g1nation.exportChat", "title": "Astra: Export Chat as Markdown" diff --git a/src/agent/postAnswerHooks/index.ts b/src/agent/postAnswerHooks/index.ts index 5ca5b52..7f52704 100644 --- a/src/agent/postAnswerHooks/index.ts +++ b/src/agent/postAnswerHooks/index.ts @@ -174,20 +174,28 @@ const criticLoopHook: PostAnswerHook = { if (cfg.criticLoopEnabled === false) return; if (!ctx.userPrompt.trim() || !ctx.assistantAnswer.trim()) return; - // 게이트 — 결정론적 검사가 문제를 신호한 업무 turn 에만 LLM 검수 1회 - // (로컬 모델 latency 보호: 깨끗한 답변에는 안 돈다). + // 게이트 — 문제 신호가 있는 turn 에만 LLM 검수 1회 (로컬 latency 보호). + // 트리거 2종: + // (a) 업무 turn: 결정론적 검사가 요소 누락 또는 저확신(<70)을 신호 + // (b) 일반 turn: 두뇌 근거 약함(topScore<0.25)인데 답변이 수치·날짜·단정 + // 표현을 포함 — intrinsic self-correction 은 효과 없으므로(2310.01798) + // 검수는 외부 신호(근거 약함이라는 검색 측정값)가 있을 때만 발동. const task = detectTaskType(ctx.userPrompt); - if (!task) return; - const coverage = checkRequirementCoverage(ctx.userPrompt, ctx.assistantAnswer); const retrievalSignals = ctx.confidenceSignals ?? { chunkCount: 0, topScore: 0, conflictCount: 0, ambiguityDetected: false, }; + const ASSERTIVE_RE = /\d{4}년|\d+월\s*\d+일|\d+(\.\d+)?\s*(%|배|건|개|원|달러)|입니다|확실|반드시|분명히/; + const weakAssertive = retrievalSignals.topScore < 0.25 + && ctx.assistantAnswer.length > 200 + && ASSERTIVE_RE.test(ctx.assistantAnswer); + if (!task && !weakAssertive) return; + const coverage = checkRequirementCoverage(ctx.userPrompt, ctx.assistantAnswer); const answerSignals = extractAnswerSignals( ctx.assistantAnswer, coverage.ran ? coverage.missing.length : null, ); const confidence = computeConfidence(retrievalSignals, answerSignals); - const needsReview = (coverage.ran && coverage.missing.length > 0) || confidence.score < 70; + const needsReview = (coverage.ran && coverage.missing.length > 0) || confidence.score < 70 || weakAssertive; if (!needsReview) return; const critique = await runCriticReview({ diff --git a/src/extension.ts b/src/extension.ts index d8d59b4..53471c7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -42,6 +42,7 @@ import { startStocksWatcher } from './features/stocks'; import { startDailyBriefingWatcher } from './features/briefing/dailyBriefing'; import { ensureDefaultBrainConfigured } from './extension/brainBootstrap'; import { ensureEmbeddingConfigured } from './extension/embeddingBootstrap'; +import { ensureFeatureInventory } from './extension/featureInventory'; import { startGrowthCycleWatcher, runGrowthCycleOnce } from './features/growth/growthCycleWatcher'; import { startSleepDigestWatcher, runSleepDigestOnce } from './features/growth/sleepDigest'; import { registerProviderCommands } from './extension/providerCommands'; @@ -78,6 +79,10 @@ export async function activate(context: vscode.ExtensionContext) { // 비차단 (TF-IDF 검색은 이미 동작 중 — 결과를 기다릴 필요 없음). void ensureEmbeddingConfigured(context); + // 기능 인벤토리 — 버전 변경 시 package.json 에서 자기 기능 문서를 기계 생성해 + // 두뇌에 기록. 자기 평가가 구식 스냅샷 대신 항상 현행 소스를 근거로 하게 한다. + void ensureFeatureInventory(context); + // Initialize Astra Path Resolver (.astra → ConnectAI/.astra/) initAstraPathResolver(context); @@ -341,6 +346,12 @@ export async function activate(context: vscode.ExtensionContext) { const summary = await runSleepDigestOnce(); vscode.window.showInformationMessage(`사전 소화 완료 — ${summary}`); })); + context.subscriptions.push(vscode.commands.registerCommand('g1nation.conflictScan.runNow', async () => { + const { runConflictScanOnce } = await import('./features/growth/conflictScan'); + vscode.window.showInformationMessage('지식 충돌 스캔 실행 중…'); + const summary = await runConflictScanOnce(); + vscode.window.showInformationMessage(`충돌 스캔 완료 — ${summary}`); + })); // 7. Auto-open all three Astra webviews as tabs in editor column 3. // The sidebar/activity-bar entry point was removed in 2.81 — all three views diff --git a/src/extension/featureInventory.ts b/src/extension/featureInventory.ts new file mode 100644 index 0000000..071ad71 --- /dev/null +++ b/src/extension/featureInventory.ts @@ -0,0 +1,95 @@ +/** + * ASTRA 기능 인벤토리 자동 생성 — 자기 지식 구식화의 근본 수정. + * + * 문제 (반복 발생한 심각 버그): ASTRA 의 자기 지식이 *사람이 쓴 스냅샷* + * (selfIdentity 블록, "ASTRA 자기 아키텍처" 위키 문서)에 의존했다. 스냅샷은 + * 작성 시점에 박제되므로 릴리스마다 구식이 되고, ASTRA 는 자기 평가·개선 제안에서 + * 이미 있는 기능을 "신규 제안"하거나(자가검증 구식 정보 사용) 없는 기능을 주장한다. + * + * 수정: 소스 오브 트루스(package.json 의 contributes.commands / configuration + + * POST_ANSWER_HOOKS 레지스트리)에서 인벤토리 문서를 **활성화 시점에 기계 생성**해 + * 두뇌에 쓴다. 버전이 바뀌면 자동 재생성 — 사람이 갱신을 잊을 수 없는 구조. + * RAG 가 이 문서를 검색하므로 자기 기능 질문·자기 개선 제안의 근거가 항상 현행이다. + */ +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getActiveBrainProfile, logError, logInfo } from '../utils'; +import { POST_ANSWER_HOOKS } from '../agent/postAnswerHooks'; + +export const INVENTORY_FILE = 'ASTRA 기능 인벤토리.md'; +const STATE_KEY = 'astra.inventoryVersion'; + +/** 답변 후 훅 id → 한 줄 설명. 레지스트리에 새 훅이 추가되면 id 는 자동 노출되고 + * 설명만 여기 한 줄 추가 (설명 누락 시에도 id 는 문서에 나타난다 — 침묵 누락 방지). */ +const HOOK_DESCRIPTIONS: Record = { + 'devil-rebuttal': 'Devil Agent 반박 카드 (활성화 시)', + 'self-check': '답변 검증 LLM 호출 — 검색 근거 대조 (opt-in)', + 'term-validator': '글로서리 금지 용어 결정론적 검사', + 'requirement-coverage': '업무 필수 요소 커버리지 결정론적 검사', + 'confidence-escalation': '확신도 산출 + 인간 검토 에스컬레이션 + Reflection 기록', + 'critic-loop': '문제 신호(요소 누락/저확신/근거 약함+단정) 턴만 Critic LLM 검수 1회', +}; + +function stripMd(s: string): string { + return (s || '').replace(/\*\*|`|\[|\]/g, '').replace(/\s+/g, ' ').trim(); +} + +/** package.json contributes 에서 인벤토리 마크다운 생성 (순수 — 테스트 가능). */ +export function buildInventoryMarkdown(pkg: any, nowIso: string): string { + const version = String(pkg?.version || '?'); + const commands: Array<{ command: string; title: string }> = pkg?.contributes?.commands || []; + const configProps: Record = pkg?.contributes?.configuration?.properties || {}; + + const lines: string[] = [ + '---', + 'type: reference', + 'title: "ASTRA 기능 인벤토리 (자동 생성)"', + `version: "${version}"`, + `generated_at: ${nowIso}`, + 'aliases: ["ASTRA 기능 목록", "ASTRA 명령어", "내 기능", "ASTRA가 할 수 있는 것", "기능 인벤토리", "ASTRA capabilities"]', + '---', + '', + `# ASTRA 기능 인벤토리 — v${version} (자동 생성)`, + '', + '> ⚙️ 이 문서는 Astra 활성화 시 **소스 코드(package.json)에서 기계 생성**됩니다 — 수동 편집 금지 (버전 변경 시 덮어씀).', + '> 자기 기능에 대한 질문·자기 개선 제안은 이 문서가 **항상 현행** 근거입니다. 서사적 설명은 [[ASTRA 자기 아키텍처]] 참고.', + '', + `## 사용자 명령 (${commands.length}개)`, + ...commands.map(c => `- ${stripMd(c.title)}`), + '', + `## 설정으로 제어되는 동작·자동화 (${Object.keys(configProps).length}개)`, + ...Object.entries(configProps).map(([key, prop]: [string, any]) => { + const desc = stripMd(String(prop?.markdownDescription || prop?.description || '')); + const first = desc.split(/(?<=[.다음])\s/)[0] || desc; + return `- \`${key.replace('g1nation.', '')}\` — ${first.slice(0, 160)}`; + }), + '', + `## 답변 후 자동 검증 훅 (${POST_ANSWER_HOOKS.length}단계 — 매 답변 후 실행)`, + ...POST_ANSWER_HOOKS.map(h => `- \`${h.id}\` — ${HOOK_DESCRIPTIONS[h.id] || '(설명 미등록 — 코드 참조)'}`), + '', + ]; + return lines.join('\n'); +} + +/** + * 버전이 바뀌었거나 문서가 없으면 두뇌에 인벤토리를 재생성. 활성화 시 1회 호출 + * (fire-and-forget — 실패해도 turn 에 영향 없음). + */ +export async function ensureFeatureInventory(context: vscode.ExtensionContext): Promise { + try { + const brain = getActiveBrainProfile(); + if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) return; + const pkg = vscode.extensions.getExtension('g1nation.astra')?.packageJSON; + if (!pkg) return; + const version = String(pkg.version || '?'); + const file = path.join(brain.localBrainPath, INVENTORY_FILE); + if (context.globalState.get(STATE_KEY) === version && fs.existsSync(file)) return; + + fs.writeFileSync(file, buildInventoryMarkdown(pkg, new Date().toISOString()), 'utf8'); + await context.globalState.update(STATE_KEY, version); + logInfo('기능 인벤토리 재생성.', { version, file }); + } catch (e: any) { + logError('기능 인벤토리 생성 실패 (무시).', { error: e?.message ?? String(e) }); + } +} diff --git a/src/features/growth/conflictScan.ts b/src/features/growth/conflictScan.ts new file mode 100644 index 0000000..b932ee9 --- /dev/null +++ b/src/features/growth/conflictScan.ts @@ -0,0 +1,145 @@ +/** + * 신규 지식 충돌 스캔 — 지식 정합성의 "쓰기 시점" 게이트. + * + * 기존 충돌 감지(검색 시점 [CONFLICT WARNING])는 두 문서가 *우연히 같이 검색될 때*만 + * 동작한다. 이 스캔은 새로 추가·변경된 두뇌 문서를 매일 능동적으로 기존 유사 문서와 + * 대조해, 검색되기 전에 모순을 표면화한다 (Mem0 의 UPDATE/DELETE 판단의 보수적 변형 — + * 시스템은 감지·보고만 하고 어느 쪽을 유지할지는 사람이 결정: Permission Based Learning). + * + * 흐름: [신규/변경 문서 감지(mtime)] → [유사 기존 문서 top-k 검색] → [LLM 모순 비교] + * → [충돌 시 .astra/growth/conflict-report.md 기록 + 알림 "기존 A vs 신규 B"] + * + * 위키를 쓰는 주체(Datacollect 브리지·수동 편집·Research Agent)와 무관하게 전부 포착 + * — 쓰기 가로채기 대신 사후 스캔을 택한 이유. + */ +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getConfig } from '../../config'; +import { findBrainFiles, getActiveBrainProfile, logError, logInfo } from '../../utils'; +import { RetrievalOrchestrator } from '../../retrieval'; +import { getBrainTokenIndex } from '../../retrieval/brainIndex'; +import { simpleChatCompletion } from '../../intelligence/llmCall'; +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 MAX_COMPARES_PER_RUN = 5; +const NEIGHBOR_K = 2; + +/** 스캔 제외 — 소화 노트·레슨·자동 생성 인벤토리는 지식 본문이 아니다. */ +export function isScanTarget(relPath: string): boolean { + return !new RegExp(`(^|[\\\\/])(${DIGEST_DIR}|lessons?|playbooks?|qa[-_]?findings?)([\\\\/])`, 'i').test(relPath) + && !/기능 인벤토리/.test(relPath) + && relPath.toLowerCase().endsWith('.md'); +} + +function loadLastScanMs(brainPath: string): number { + try { + const f = path.join(brainPath, STATE_REL); + if (!fs.existsSync(f)) return 0; + const v = JSON.parse(fs.readFileSync(f, 'utf8'))?.lastScanMs; + return typeof v === 'number' ? v : 0; + } catch { return 0; } +} + +function saveLastScanMs(brainPath: string, ms: number): void { + try { + const f = path.join(brainPath, STATE_REL); + fs.mkdirSync(path.dirname(f), { recursive: true }); + fs.writeFileSync(f, JSON.stringify({ lastScanMs: ms }) + '\n', 'utf8'); + } catch { /* 상태 저장 실패 — 다음 런이 더 넓게 스캔할 뿐 */ } +} + +const COMPARE_SYSTEM = [ + '너는 지식 정합성 검사기다. [신규 문서]와 [기존 문서]가 같은 주제에 대해 *모순되는 사실*을 말하는지 판정하라.', + '모순 = 같은 대상에 대해 양립 불가능한 수치·날짜·결론·정의. 관점 차이·세부 수준 차이·다른 주제는 모순이 아니다.', + '반드시 JSON 한 줄만 출력: {"conflict":true|false,"summary":"<모순 내용 한 문장, 없으면 빈 문자열>"}', +].join('\n'); + +export interface ConflictFinding { + newDoc: string; + existingDoc: string; + summary: string; +} + +/** 1회 스캔 실행. 반환: 요약 문자열. */ +export async function runConflictScanOnce(): Promise { + const brain = getActiveBrainProfile(); + if (!brain?.localBrainPath || !fs.existsSync(brain.localBrainPath)) return '두뇌 없음 — skip'; + const config = getConfig(); + if (!config.defaultModel || !config.ollamaUrl) return 'LLM 미설정 — skip'; + + const brainPath = brain.localBrainPath; + const lastScanMs = loadLastScanMs(brainPath); + const scanStartMs = Date.now(); + const allFiles = findBrainFiles(brainPath); + + // 신규/변경 대상 (최초 실행은 최근 24h 만 — 두뇌 전체 재스캔 방지) + const cutoff = lastScanMs > 0 ? lastScanMs : scanStartMs - 86_400_000; + const targets = allFiles + .map(f => { try { return { f, m: fs.statSync(f).mtimeMs }; } catch { return null; } }) + .filter((x): x is { f: string; m: number } => !!x && x.m > cutoff) + .filter(x => isScanTarget(path.relative(brainPath, x.f))) + .sort((a, b) => b.m - a.m) + .slice(0, MAX_COMPARES_PER_RUN); + + if (targets.length === 0) { saveLastScanMs(brainPath, scanStartMs); return '신규 문서 없음'; } + + getBrainTokenIndex(brainPath, allFiles); + const orchestrator = new RetrievalOrchestrator(); + const findings: ConflictFinding[] = []; + let compared = 0; + + for (const t of targets) { + try { + const rel = path.relative(brainPath, t.f); + const content = fs.readFileSync(t.f, 'utf8'); + const title = path.basename(t.f, '.md'); + // 유사 기존 문서 — 자신 제외 상위 NEIGHBOR_K + const neighbors = orchestrator + .rankBrainForEval(`${title} ${content.slice(0, 500)}`, brain, { limit: NEIGHBOR_K + 3 }) + .filter(r => path.resolve(r.filePath) !== path.resolve(t.f)) + .slice(0, NEIGHBOR_K); + for (const n of neighbors) { + let existing = ''; + try { existing = fs.readFileSync(n.filePath, 'utf8'); } catch { continue; } + const raw = await simpleChatCompletion( + COMPARE_SYSTEM, + `[신규 문서: ${title}]\n${content.slice(0, 3500)}\n\n[기존 문서: ${path.basename(n.relativePath, '.md')}]\n${existing.slice(0, 3500)}`, + { baseUrl: config.ollamaUrl, model: config.defaultModel, temperature: 0.1, maxTokens: 150, timeoutMs: 90000 }, + ); + compared++; + const m = raw.match(/\{[\s\S]*?\}/); + 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) }); + } + } + } catch (e: any) { + logError('충돌 스캔: 문서 비교 실패 (계속).', { file: t.f, error: e?.message ?? String(e) }); + } + } + + saveLastScanMs(brainPath, scanStartMs); + + if (findings.length > 0) { + 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 → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`), + ].join('\n'); + try { + fs.mkdirSync(path.dirname(reportFile), { recursive: true }); + if (!fs.existsSync(reportFile)) fs.writeFileSync(reportFile, '# 지식 충돌 리포트\n시스템은 감지·보고만 합니다 — 유지/수정 결정은 사람.\n', 'utf8'); + fs.appendFileSync(reportFile, block + '\n', 'utf8'); + } catch { /* 리포트 실패해도 알림은 전달 */ } + void vscode.window.showWarningMessage( + `⚔️ 지식 충돌 ${findings.length}건 감지 — 예: "${findings[0].newDoc}" ↔ "${findings[0].existingDoc}". conflict-report.md 에서 확인하세요.`, + ); + } + const summary = `신규 ${targets.length}건 스캔 · 비교 ${compared}회 · 충돌 ${findings.length}건`; + logInfo('지식 충돌 스캔 완료.', { summary }); + return summary; +} diff --git a/src/features/growth/growthCycleWatcher.ts b/src/features/growth/growthCycleWatcher.ts index f9e3b20..2325673 100644 --- a/src/features/growth/growthCycleWatcher.ts +++ b/src/features/growth/growthCycleWatcher.ts @@ -112,6 +112,38 @@ export async function runGrowthCycleOnce(context: vscode.ExtensionContext): Prom const stamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19); fs.writeFileSync(path.join(brain.localBrainPath, '.astra', 'eval', `report-${stamp}-auto.md`), md, 'utf8'); summary.push(`검색 recall@1 ${(report.recallAtK[1] * 100).toFixed(0)}% · MRR ${report.mrr.toFixed(2)}`); + + // [회귀 경보] 직전 사이클 지표와 비교 — 성장(지식 추가)이 검색 품질을 + // 퇴보시키지 않았는지 감시. 하락 시 그 기간에 추가된 문서를 용의자로 제시. + // 자동 롤백은 하지 않는다 (지식은 git 에 있고 판단은 사람 — 승인 원칙). + try { + const histFile = path.join(brain.localBrainPath, '.astra', 'eval', 'metrics-history.jsonl'); + let prev: { ts: string; recall1: number; mrr: number } | null = null; + if (fs.existsSync(histFile)) { + const rows = fs.readFileSync(histFile, 'utf8').trim().split('\n').filter(Boolean); + if (rows.length > 0) { try { prev = JSON.parse(rows[rows.length - 1]); } catch { /* skip */ } } + } + const cur = { ts: now.toISOString(), recall1: report.recallAtK[1], recall3: report.recallAtK[3], mrr: report.mrr, queries: entries.length }; + fs.appendFileSync(histFile, JSON.stringify(cur) + '\n', 'utf8'); + const R1_DROP = 0.10, MRR_DROP = 0.08; + if (prev && (prev.recall1 - cur.recall1 >= R1_DROP || prev.mrr - cur.mrr >= MRR_DROP)) { + const prevTs = Date.parse(prev.ts) || 0; + const suspects = allFiles + .map(f => { try { return { f, m: fs.statSync(f).mtimeMs }; } catch { return null; } }) + .filter((x): x is { f: string; m: number } => !!x && x.m > prevTs) + .sort((a, b) => b.m - a.m).slice(0, 10) + .map(x => path.relative(brain.localBrainPath, x.f)); + const alert = [ + `# ⚠️ 검색 회귀 감지 — ${now.toLocaleString()}`, '', + `recall@1 ${(prev.recall1 * 100).toFixed(0)}% → ${(cur.recall1 * 100).toFixed(0)}% · MRR ${prev.mrr.toFixed(2)} → ${cur.mrr.toFixed(2)}`, + '', `## 직전 사이클 이후 추가·변경된 문서 (용의자 ${suspects.length}건)`, + ...suspects.map(s => `- ${s}`), + '', '조치: 용의자 문서의 중복/제목 충돌 여부 확인. 복구는 git 으로 (자동 롤백 안 함 — 판단은 사람).', + ].join('\n'); + fs.writeFileSync(path.join(growthDir, 'regression-alert.md'), alert, 'utf8'); + summary.push(`⚠️ 검색 회귀 (recall@1 ${(prev.recall1 * 100).toFixed(0)}%→${(cur.recall1 * 100).toFixed(0)}%) — regression-alert.md 확인`); + } + } catch (e: any) { logError('성장 사이클: 회귀 비교 실패.', { error: e?.message ?? String(e) }); } } } catch (e: any) { logError('성장 사이클: 검색 평가 실패.', { error: e?.message ?? String(e) }); } diff --git a/src/features/growth/sleepDigest.ts b/src/features/growth/sleepDigest.ts index 9b3d141..274a64b 100644 --- a/src/features/growth/sleepDigest.ts +++ b/src/features/growth/sleepDigest.ts @@ -248,6 +248,13 @@ function scheduleNext(): void { try { await runSleepDigestOnce(); } catch (e: any) { logError('Sleep digest 사이클 실패.', { error: e?.message ?? String(e) }); } + // 같은 유휴 슬롯에 신규 지식 충돌 스캔도 수행 (쓰기 시점 정합성 게이트). + try { + const { runConflictScanOnce } = await import('./conflictScan'); + await runConflictScanOnce(); + } catch (e: any) { + logError('충돌 스캔 실패 (무시).', { error: e?.message ?? String(e) }); + } } scheduleNext(); }, ms); diff --git a/src/lib/contextBuilders/selfIdentity.ts b/src/lib/contextBuilders/selfIdentity.ts index bc927f7..ebb3e78 100644 --- a/src/lib/contextBuilders/selfIdentity.ts +++ b/src/lib/contextBuilders/selfIdentity.ts @@ -22,6 +22,7 @@ export function buildSelfIdentityBlock(): string { '- **레슨(경험 기억)**: 작업 경험에서 만들어진 교훈이 `lessons/`에 쌓여 이후 판단에 반영된다.', '- **평가·성장 루프(Self-Evolving OS)**: 골든셋 기반 검색/업무 평가, 성장 리포트, 학습 큐(Need Engine), 지식 노후 점검(Decay), 승인된 학습의 자동 실행(Research Agent)으로 지식의 질을 측정·개선한다.', '따라서 "스스로 성장하는가"라는 질문에는 "모델 가중치는 고정이지만, 시스템으로서 저는 위 메커니즘으로 세션을 넘어 지식·기억을 축적하며 성장합니다"가 정확한 답이다. "나는 학습하지 않는 정적 모델"이라는 일반론으로 답하지 말 것. 자신의 기능·구조에 대한 상세 질문은 두뇌의 "ASTRA 자기 아키텍처" 문서를 근거로 답하라.', + '[자기 평가·개선 제안 규칙] 자신의 기능을 평가하거나 새 기능을 제안하기 전에 반드시 두뇌의 "ASTRA 기능 인벤토리" 문서(소스 코드에서 자동 생성 — 항상 현행)와 대조하라. 인벤토리에 이미 있는 기능을 신규 제안하지 말고, "현재 X가 있고, 빠진 증분은 Y"의 형태로 말하라. 기억에 의존한 기능 목록 서술은 구식일 수 있다.', '[출력 위생] 자연스러운 한국어로 쓰고, 한 단어 안에 한글과 영문 알파벳을 섞지 마라("응다", "텍록", "결ently" 같은 깨진 합성 표기 금지). 외래어·기술 용어는 완전한 한글 표기 또는 완전한 영문 단어 중 하나로 일관되게 쓴다.', ].join('\n'); } diff --git a/tests/featureInventory.test.ts b/tests/featureInventory.test.ts new file mode 100644 index 0000000..3a4ad02 --- /dev/null +++ b/tests/featureInventory.test.ts @@ -0,0 +1,58 @@ +/** + * 기능 인벤토리 자동 생성 + 충돌 스캔 대상 필터 — 순수 로직 테스트. + * (자기 지식 구식화 버그의 근본 수정: 인벤토리가 package.json 에서 기계 생성되는지) + */ +import { buildInventoryMarkdown } from '../src/extension/featureInventory'; +import { isScanTarget } from '../src/features/growth/conflictScan'; + +describe('buildInventoryMarkdown — package.json 이 소스 오브 트루스', () => { + const pkg = { + version: '9.9.9', + contributes: { + commands: [ + { command: 'g1nation.a', title: 'Astra: 기능 A' }, + { command: 'g1nation.b', title: 'Astra: 기능 B (**굵게**)' }, + ], + configuration: { + properties: { + 'g1nation.x.enabled': { type: 'boolean', markdownDescription: '**X 자동화** — 매일 실행합니다. 두 번째 문장.' }, + 'g1nation.y': { type: 'string', description: 'Y 설정.' }, + }, + }, + }, + }; + + test('버전·명령·설정·훅이 모두 들어간다', () => { + const md = buildInventoryMarkdown(pkg, '2026-06-12T00:00:00Z'); + expect(md).toContain('v9.9.9'); + expect(md).toContain('사용자 명령 (2개)'); + expect(md).toContain('Astra: 기능 A'); + expect(md).toContain('기능 B (굵게)'); // 마크다운 장식 제거 + expect(md).toContain('`x.enabled`'); + expect(md).toContain('답변 후 자동 검증 훅'); + expect(md).toContain('`critic-loop`'); // 레지스트리에서 직접 — 코드가 곧 문서 + expect(md).toContain('수동 편집 금지'); + }); + + test('레지스트리의 모든 훅 id 가 노출된다 (침묵 누락 방지)', () => { + const md = buildInventoryMarkdown(pkg, '2026-06-12T00:00:00Z'); + for (const id of ['devil-rebuttal', 'self-check', 'term-validator', 'requirement-coverage', 'confidence-escalation', 'critic-loop']) { + expect(md).toContain(`\`${id}\``); + } + }); +}); + +describe('isScanTarget — 충돌 스캔 제외 규칙', () => { + test.each([ + 'Topics_Rag/새문서.md', + 'AI_and_ML/RAG.md', + ])('지식 본문은 대상: %s', (p) => expect(isScanTarget(p)).toBe(true)); + + test.each([ + 'Digests/Topics_Rag.md', + 'lessons/2026-06-12-correction-x.md', + 'playbooks/p.md', + 'ASTRA 기능 인벤토리.md', + 'Topics_Rag/이미지.png', + ])('소화 노트·레슨·인벤토리·비-md 제외: %s', (p) => expect(isScanTarget(p)).toBe(false)); +});