7584c6bbc1
deep research(3표 검증) 1순위 적용: sleep-time compute (arXiv 2504.13171). 유휴 시간에 로컬 LLM이 두뇌의 raw context를 learned context로 미리 소화해, 응답 시점 RAG가 고밀도 소화 노트를 검색하게 한다 — 로컬 LLM의 최대 약점 (느린 추론)의 비용을 사용자 응대 시점에서 유휴 시간으로 구조적으로 이동. - sleepDigest.ts: 매일 03:00 KST(설정 가능) 최근 7일 변경 파일이 많은 폴더 순으로 소화 노트 생성 (<두뇌>/Digests/<슬러그>.md, 런당 ≤5건). 노트 = 예상 질의 Q&A + 핵심 사실 + 문서 간 연결 (출처 제목 인용 강제, "원문에 없는 내용 지어내지 마라" — 환각 방지 동일 원칙). - 노후화 자동 감지: 소스 mtime > generated_at 이면 재생성, 아니면 skip (steady-state 비용 0). 노트는 삭제해도 안전 (자동 재생성). - 승인 게이트 불요 근거: 외부 지식 유입이 아니라 기존 두뇌의 재구성. 원문 우선 원칙을 노트 머리에 명기. - 수동 명령 "Astra: 지식 사전 소화 지금 실행" + sleepDigest.enabled/time 설정. - 실 LLM(gemma-4-26b)+실 위키 3문서로 프롬프트 품질 검증 완료 (출처 인용· 무환각 확인). 테스트 8건 추가. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
4.4 KiB
TypeScript
102 lines
4.4 KiB
TypeScript
/**
|
|
* Sleep-time 사전 소화 — 순수 로직 테스트 (대상 선정·노후화 판정·노트 형식).
|
|
* LLM 호출(runSleepDigestOnce)은 제외 — 통합 검증은 수동 명령으로.
|
|
*/
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import {
|
|
selectDigestTargets, folderSlug, isDigestFresh, digestMarkdown, DIGEST_DIR,
|
|
type DigestTarget,
|
|
} from '../src/features/growth/sleepDigest';
|
|
|
|
const tmpBrain = () => fs.mkdtempSync(path.join(os.tmpdir(), 'astra-digest-'));
|
|
|
|
function mkFile(brain: string, rel: string, ageDays: number): string {
|
|
const abs = path.join(brain, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, `# ${path.basename(rel)}\n내용`, 'utf8');
|
|
const t = new Date(Date.now() - ageDays * 86_400_000);
|
|
fs.utimesSync(abs, t, t);
|
|
return abs;
|
|
}
|
|
|
|
describe('selectDigestTargets', () => {
|
|
test('최근 변경 파일이 많은 폴더 우선, 오래된 파일 제외', () => {
|
|
const brain = tmpBrain();
|
|
const files = [
|
|
mkFile(brain, 'Topics_Rag/a.md', 1),
|
|
mkFile(brain, 'Topics_Rag/b.md', 2),
|
|
mkFile(brain, 'Topics_Rag/c.md', 3),
|
|
mkFile(brain, 'Coding/Python/x.md', 1),
|
|
mkFile(brain, 'Coding/Python/y.md', 2),
|
|
mkFile(brain, 'Old_Folder/z.md', 30), // 윈도우 밖
|
|
];
|
|
const targets = selectDigestTargets(brain, files, Date.now(), 7);
|
|
expect(targets[0].folder).toBe('Topics_Rag');
|
|
expect(targets[0].files).toHaveLength(3);
|
|
expect(targets[1].folder).toBe('Coding/Python');
|
|
expect(targets.find(t => t.folder === 'Old_Folder')).toBeUndefined();
|
|
});
|
|
test('Digests 폴더 자신은 재소화하지 않음 + 루트 직속은 빈 키', () => {
|
|
const brain = tmpBrain();
|
|
const files = [
|
|
mkFile(brain, `${DIGEST_DIR}/old-digest.md`, 1),
|
|
mkFile(brain, 'root-note.md', 1),
|
|
];
|
|
const targets = selectDigestTargets(brain, files, Date.now(), 7);
|
|
expect(targets.find(t => t.folder.includes(DIGEST_DIR))).toBeUndefined();
|
|
expect(targets.find(t => t.folder === '')).toBeDefined();
|
|
});
|
|
test('파일 상한 8개', () => {
|
|
const brain = tmpBrain();
|
|
const files = Array.from({ length: 12 }, (_, i) => mkFile(brain, `Big/f${i}.md`, 1));
|
|
const targets = selectDigestTargets(brain, files, Date.now(), 7);
|
|
expect(targets[0].files.length).toBeLessThanOrEqual(8);
|
|
});
|
|
});
|
|
|
|
describe('folderSlug', () => {
|
|
test('경로 구분자·특수문자 정리', () => {
|
|
expect(folderSlug('Coding/Python')).toBe('Coding--Python');
|
|
expect(folderSlug('')).toBe('root');
|
|
expect(folderSlug('AI & ML!')).toBe('AI-ML-');
|
|
});
|
|
});
|
|
|
|
describe('isDigestFresh — 노후화 판정', () => {
|
|
const target = (over: Partial<DigestTarget> = {}): DigestTarget => ({
|
|
folder: 'X', slug: 'X', files: [], newestMtimeMs: Date.now(), ...over,
|
|
});
|
|
test('노트 없음 → 재생성 필요', () => {
|
|
expect(isDigestFresh(path.join(tmpBrain(), 'no.md'), Date.now())).toBe(false);
|
|
});
|
|
test('generated_at ≥ 소스 mtime → fresh', () => {
|
|
const brain = tmpBrain();
|
|
const f = path.join(brain, 'd.md');
|
|
const t = target();
|
|
fs.writeFileSync(f, digestMarkdown(t, '본문', ['s1'], new Date(t.newestMtimeMs + 1000).toISOString()), 'utf8');
|
|
expect(isDigestFresh(f, t.newestMtimeMs)).toBe(true);
|
|
});
|
|
test('소스가 노트보다 최신 → 재생성 필요', () => {
|
|
const brain = tmpBrain();
|
|
const f = path.join(brain, 'd.md');
|
|
fs.writeFileSync(f, digestMarkdown(target(), '본문', ['s1'], new Date(Date.now() - 86_400_000).toISOString()), 'utf8');
|
|
expect(isDigestFresh(f, Date.now())).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('digestMarkdown', () => {
|
|
test('frontmatter(type: digest, sources) + 원문 우선 고지', () => {
|
|
const md = digestMarkdown(
|
|
{ folder: 'Topics_Rag', slug: 'Topics_Rag', files: [], newestMtimeMs: 0 },
|
|
'## 예상 질문과 답\n- Q…', ['문서 "A"', 'B'], '2026-06-12T03:00:00Z',
|
|
);
|
|
expect(md).toMatch(/^---\ntype: digest/);
|
|
expect(md).toContain('generated_at: 2026-06-12T03:00:00Z');
|
|
expect(md).toContain("문서 'A'"); // 따옴표 이스케이프
|
|
expect(md).toContain('원문이 항상 우선');
|
|
expect(md).toContain('# 소화 노트: Topics_Rag');
|
|
});
|
|
});
|