384 lines
20 KiB
TypeScript
384 lines
20 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import {
|
|
buildSecondBrainTrace,
|
|
enforceProjectClaimPolicyInAnswer,
|
|
renderSecondBrainTraceContext,
|
|
renderSecondBrainTraceMarkdown
|
|
} from '../src/features/secondBrainTrace';
|
|
|
|
describe('Second Brain Trace', () => {
|
|
let brainRoot: string;
|
|
|
|
beforeEach(() => {
|
|
brainRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'second-brain-trace-'));
|
|
fs.mkdirSync(path.join(brainRoot, 'records', 'ProjectChronicle', 'decisions'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'records', 'ProjectChronicle', 'decisions', 'ADR-0002-low-dependency-design.md'),
|
|
[
|
|
'# ADR-0002 Low Dependency Design',
|
|
'',
|
|
'Project Chronicle Guard should start with Markdown files and an independent module.',
|
|
'Vector DB and relational DB are later expansion options, not MVP dependencies.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'general-note.md'),
|
|
'# General Note\n\nThis unrelated note talks about coffee and weather.',
|
|
'utf8'
|
|
);
|
|
fs.mkdirSync(path.join(brainRoot, '02_Architecture_Principles'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, '02_Architecture_Principles', 'API Gateway.md'),
|
|
[
|
|
'# API Gateway',
|
|
'',
|
|
'General Knowledge: API Gateway can route requests in a microservice architecture.',
|
|
'This document is not evidence that any current project implements API Gateway.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.mkdirSync(path.join(brainRoot, 'UX'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'UX', 'Customer Journey Virtual Store.md'),
|
|
[
|
|
'# Customer Journey Virtual Store',
|
|
'',
|
|
'Customer-facing virtual stores should connect spatial experience to product discovery, product understanding, and purchase conversion.',
|
|
'Stakeholder approval often depends on requirement fit, business value, and acceptance criteria rather than visual novelty alone.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.mkdirSync(path.join(brainRoot, 'Strategy'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'Strategy', 'Report Evidence Mapping.md'),
|
|
[
|
|
'# Report Evidence Mapping',
|
|
'',
|
|
'Template-driven reports should map each section to evidence, insight, risk, and next action knowledge.',
|
|
'A schema should guide structure while Second Brain notes supply the actual content.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'Strategy', 'Risk and Action Playbook.md'),
|
|
[
|
|
'# Risk and Action Playbook',
|
|
'',
|
|
'Risk sections should capture limitations, validation gaps, and tradeoffs.',
|
|
'Action sections should turn knowledge into MVP steps, implementation recommendations, and next decisions.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.mkdirSync(path.join(brainRoot, 'Ontology'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'Ontology', 'Knowledge Graph Concepts.md'),
|
|
[
|
|
'# Knowledge Graph Concepts',
|
|
'',
|
|
'Ontology notes define concepts, relations, categories, and graph structure before writing.',
|
|
'They help a report decide which ideas are parent concepts, evidence, methods, and outcomes.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.mkdirSync(path.join(brainRoot, 'Writing'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'Writing', 'Report Narrative Structure.md'),
|
|
[
|
|
'# Report Narrative Structure',
|
|
'',
|
|
'Writing guidance should shape report structure, section order, narrative flow, and concise executive summaries.',
|
|
'It should not replace evidence; it organizes selected knowledge into a readable output.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.mkdirSync(path.join(brainRoot, 'Technical'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'Technical', 'Implementation Techniques.md'),
|
|
[
|
|
'# Implementation Techniques',
|
|
'',
|
|
'Technical technique notes explain implementation methods, architecture choices, and tooling tradeoffs.',
|
|
'They should support practical next actions after the report identifies risks and evidence.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.mkdirSync(path.join(brainRoot, '00_Raw', 'conversations'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, '00_Raw', 'conversations', '2026-05-01.md'),
|
|
[
|
|
'# Raw Conversation',
|
|
'',
|
|
'dependency complexity schema drift documentation gap recommendations',
|
|
'This is a noisy transcript and should not be selected before curated records.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(brainRoot, 'Index_692.md'),
|
|
'# Index\n\ndependency complexity schema drift documentation gap'
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(brainRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
it('retrieves and marks relevant Second Brain notes for project-specific questions', () => {
|
|
const trace = buildSecondBrainTrace('Project Chronicle Guard MVP에서 Vector DB는 어떻게 다뤄야 해?', brainRoot);
|
|
|
|
expect(trace.shouldUseSecondBrain).toBe(true);
|
|
expect(trace.secondBrainUsed).toBe(true);
|
|
expect(trace.retrievedDocuments[0].path).toContain('ADR-0002-low-dependency-design.md');
|
|
expect(trace.retrievedDocuments[0].usedInAnswer).toBe(true);
|
|
expect(trace.retrievedDocuments[0].selectedForAnswerContext).toBe(true);
|
|
expect(trace.retrievedDocuments[0].sourceType).toBe('User Decision');
|
|
expect(trace.retrievedDocuments[0].canSupportProjectClaim).toBe(true);
|
|
expect(trace.groundingScore).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('renders user-facing markdown and debug JSON', () => {
|
|
const trace = buildSecondBrainTrace('Second Brain을 참고해서 low dependency 원칙 알려줘', brainRoot, { force: true });
|
|
const markdown = renderSecondBrainTraceMarkdown(trace, true);
|
|
const context = renderSecondBrainTraceContext(trace);
|
|
|
|
expect(markdown).toContain('<details>');
|
|
expect(markdown).toContain('<summary>2nd Brain Trace: 사용함');
|
|
expect(markdown).toContain('## 2nd Brain 사용 여부');
|
|
expect(markdown).toContain('## 답변 컨텍스트로 선택된 2nd Brain 문서');
|
|
expect(markdown).toContain('## Second Brain Debug JSON');
|
|
expect(context).toContain('[SECOND BRAIN TRACE]');
|
|
expect(context).toContain('Retrieval query:');
|
|
expect(context).toContain('Do not imitate dramatic wording');
|
|
expect(context).toContain('No Evidence, No Project Claim');
|
|
});
|
|
|
|
it('explains when Second Brain is not needed', () => {
|
|
const trace = buildSecondBrainTrace('오늘 날짜가 뭐야?', brainRoot);
|
|
|
|
expect(trace.shouldUseSecondBrain).toBe(false);
|
|
expect(trace.secondBrainUsed).toBe(false);
|
|
expect(renderSecondBrainTraceMarkdown(trace)).toContain('사용하지 않음');
|
|
expect(renderSecondBrainTraceMarkdown(trace)).toContain('<details>');
|
|
});
|
|
|
|
it('prefers curated notes over raw conversations and index files', () => {
|
|
const trace = buildSecondBrainTrace(
|
|
'dependency complexity schema drift documentation gap 문제 대응 가이드',
|
|
brainRoot
|
|
);
|
|
|
|
expect(trace.retrievedDocuments[0].path).toContain('ADR-0002-low-dependency-design.md');
|
|
expect(trace.retrievedDocuments.find((doc) => doc.path.includes('00_Raw'))).toBeUndefined();
|
|
expect(trace.retrievedDocuments[0].path).not.toContain('Index_692.md');
|
|
});
|
|
|
|
it('classifies general architecture notes as unable to support project implementation claims', () => {
|
|
const trace = buildSecondBrainTrace(
|
|
'현재 프로젝트는 API Gateway 라우팅 구조를 갖추고 있어?',
|
|
brainRoot,
|
|
{ force: true }
|
|
);
|
|
const apiGateway = trace.retrievedDocuments.find((doc) => doc.path.includes('API Gateway.md'));
|
|
|
|
expect(apiGateway).toBeDefined();
|
|
expect(apiGateway?.sourceType).toBe('General Knowledge');
|
|
expect(apiGateway?.canSupportProjectClaim).toBe(false);
|
|
expect(apiGateway?.warning).toContain('실제 구현 근거가 아닙니다');
|
|
const markdown = renderSecondBrainTraceMarkdown(trace, true);
|
|
const context = renderSecondBrainTraceContext(trace);
|
|
expect(trace.projectClaimPolicy).not.toBe('allow');
|
|
expect(markdown).toContain('"canSupportProjectClaim": false');
|
|
expect(context).toContain('No Evidence, No Project Claim');
|
|
expect(context).toContain('현재 정보만으로는 기술 구조를 판단할 수 없습니다');
|
|
expect(context).toContain('아키텍처는 유연합니다');
|
|
});
|
|
|
|
it('uses general-only policy when selected notes cannot support project claims', () => {
|
|
const generalOnlyRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'second-brain-general-only-'));
|
|
try {
|
|
fs.mkdirSync(path.join(generalOnlyRoot, '02_Architecture_Principles'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(generalOnlyRoot, '02_Architecture_Principles', 'API Gateway.md'),
|
|
[
|
|
'# API Gateway',
|
|
'',
|
|
'General Knowledge: API Gateway can route requests in a microservice architecture.',
|
|
'This is a concept note, not current project evidence.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
|
|
const trace = buildSecondBrainTrace(
|
|
'현재 프로젝트는 API Gateway 라우팅 구조를 갖추고 있어?',
|
|
generalOnlyRoot,
|
|
{ force: true }
|
|
);
|
|
|
|
expect(trace.projectClaimPolicy).toBe('general-only');
|
|
expect(renderSecondBrainTraceContext(trace)).toContain('STRICT RULE');
|
|
} finally {
|
|
fs.rmSync(generalOnlyRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('removes unsupported technical structure claims from final answers under general-only policy', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'second-brain-policy-'));
|
|
try {
|
|
fs.mkdirSync(path.join(root, '02_Architecture_Principles'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, '02_Architecture_Principles', 'API Gateway.md'),
|
|
'# API Gateway\n\nGeneral Knowledge: API Gateway can route requests.',
|
|
'utf8'
|
|
);
|
|
const trace = buildSecondBrainTrace('현재 프로젝트는 API Gateway 라우팅 구조를 갖추고 있어?', root, { force: true });
|
|
const answer = [
|
|
'결론은 간단합니다. 현재 개발 방향은 기술적 기반 면에서는 안정적입니다.',
|
|
'',
|
|
'아키텍처는 유연하나 구매 전환 시나리오를 보완해야 합니다.',
|
|
'모듈화된 구조는 향후 기능 추가 시 유연성을 제공합니다.',
|
|
'',
|
|
'다만 UX 관점에서는 공간 경험과 상품 탐색 연결을 확인해야 합니다.'
|
|
].join('\n');
|
|
|
|
const sanitized = enforceProjectClaimPolicyInAnswer(answer, trace);
|
|
|
|
expect(sanitized).toContain('현재 정보만으로는 기술 구조를 판단할 수 없습니다');
|
|
expect(sanitized).not.toContain('기술적 기반 면에서는 안정적');
|
|
expect(sanitized).not.toContain('아키텍처는 유연');
|
|
expect(sanitized).not.toContain('모듈화된 구조');
|
|
expect(sanitized).toContain('UX 관점');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('prioritizes UX and business documents for approval and customer-experience questions', () => {
|
|
const trace = buildSecondBrainTrace(
|
|
'롯데 이노베이트가 고객 대상 버추얼 웹스토어에서 상품 중심이 아니라 공간 중심 개발 방향을 승인할 가능성이 있을까?',
|
|
brainRoot,
|
|
{ force: true }
|
|
);
|
|
|
|
expect(trace.queryIntent).toBe('ux-business');
|
|
expect(trace.retrievalQuery).toContain('customer journey');
|
|
expect(trace.retrievalQuery).toContain('approval');
|
|
expect(trace.retrievedDocuments[0].path).toContain('Customer Journey Virtual Store.md');
|
|
expect(trace.retrievedDocuments[0].path).not.toContain('API Gateway.md');
|
|
expect(renderSecondBrainTraceContext(trace)).toContain('Approval likelihood is an inference');
|
|
});
|
|
|
|
it('prioritizes notes for the project named in a local Antigravity path', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'second-brain-project-target-'));
|
|
try {
|
|
fs.mkdirSync(path.join(root, 'Project_Logs'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, 'Project_Logs', '2026-04-25-Datacollector_Fix.md'),
|
|
[
|
|
'# Datacollector Fix',
|
|
'',
|
|
'Project: Datacollector',
|
|
'Repository: `/Volumes/Data/project/Antigravity/Datacollector`',
|
|
'project antigravity repository documentation knowledge'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, 'Project_Logs', '2026-05-02-ConnectAI_Project_Knowledge.md'),
|
|
[
|
|
'# ConnectAI Project Knowledge',
|
|
'',
|
|
'Project: ConnectAI',
|
|
'Repository: `/Volumes/Data/project/Antigravity/ConnectAI`',
|
|
'ConnectAI project knowledge for local AI assistant, Second Brain Trace, and Project Chronicle.'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, 'Project_Logs', '2026-04-26-Skybound_User_Decision.md'),
|
|
[
|
|
'# Skybound User Decision',
|
|
'',
|
|
'## Decision',
|
|
'Reward card clarity was improved.',
|
|
'- `/Volumes/Data/project/Antigravity/Skybound/src/features/game/hooks/useGameEngine.ts`',
|
|
'project antigravity connectai knowledge documentation'
|
|
].join('\n'),
|
|
'utf8'
|
|
);
|
|
|
|
const trace = buildSecondBrainTrace(
|
|
'그러면 지금 /Volumes/Data/project/Antigravity/ConnectAI 프로젝트에 대한 지식을 만들어',
|
|
root,
|
|
{ force: true }
|
|
);
|
|
|
|
expect(trace.retrievedDocuments[0].path).toContain('ConnectAI_Project_Knowledge.md');
|
|
expect(trace.retrievedDocuments[0].path).not.toContain('Datacollector');
|
|
const skybound = trace.retrievedDocuments.find((doc) => doc.path.includes('Skybound'));
|
|
expect(skybound?.canSupportProjectClaim).not.toBe(true);
|
|
expect(trace.projectClaimPolicy).not.toBe('cautious');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('builds structured knowledge slots for report and template requests', () => {
|
|
const trace = buildSecondBrainTrace(
|
|
'제2뇌 지식을 사용해서 템플릿 기반 전략 보고서를 작성해줘. 근거, 핵심 분석, 리스크, 실행안을 포함해줘.',
|
|
brainRoot,
|
|
{ force: true }
|
|
);
|
|
const context = renderSecondBrainTraceContext(trace);
|
|
const markdown = renderSecondBrainTraceMarkdown(trace, true);
|
|
|
|
expect(trace.knowledgeSlots.length).toBeGreaterThanOrEqual(4);
|
|
expect(trace.knowledgeSlots.map((slot) => slot.id)).toEqual(expect.arrayContaining(['evidence', 'insight', 'risk', 'action']));
|
|
expect(trace.knowledgeSlots.some((slot) => slot.selectedPaths.some((pathName) => pathName.includes('Report Evidence Mapping.md')))).toBe(true);
|
|
expect(trace.retrievedDocuments.some((doc) => doc.usedFor?.includes('근거') || doc.usedFor?.includes('실행안'))).toBe(true);
|
|
expect(context).toContain('Structured knowledge slots');
|
|
expect(context).toContain('Do not merely follow a static template');
|
|
expect(markdown).toContain('## 구조화 지식 슬롯');
|
|
expect(markdown).toContain('"knowledgeSlots"');
|
|
});
|
|
|
|
it('plans ontology, writing, information, and technical materials before report synthesis', () => {
|
|
const trace = buildSecondBrainTrace(
|
|
'제2뇌 전체 지식을 버리지 말고 온톨로지, 글쓰기 지식, 정보, 테크닉, 기술 내용을 재료로 먼저 파악한 뒤 보고서를 작성해줘.',
|
|
brainRoot,
|
|
{ force: true }
|
|
);
|
|
const context = renderSecondBrainTraceContext(trace);
|
|
const slotIds = trace.knowledgeSlots.map((slot) => slot.id);
|
|
|
|
expect(slotIds).toEqual(expect.arrayContaining(['ontology', 'writing', 'information', 'technical']));
|
|
expect(trace.knowledgeSlots.find((slot) => slot.id === 'ontology')?.selectedPaths.join('\n')).toContain('Knowledge Graph Concepts.md');
|
|
expect(trace.knowledgeSlots.find((slot) => slot.id === 'writing')?.selectedPaths.join('\n')).toContain('Report Narrative Structure.md');
|
|
expect(trace.knowledgeSlots.find((slot) => slot.id === 'technical')?.selectedPaths.join('\n')).toContain('Implementation Techniques.md');
|
|
expect(context).toContain('Material planning rule');
|
|
expect(context).toContain('Coverage rule');
|
|
});
|
|
|
|
it('treats index documents as routing hints instead of overusing them as slot material', () => {
|
|
const trace = buildSecondBrainTrace(
|
|
'나는 /Volumes/Data/project/Antigravity/ConnectAI 여기에서 사용자가 질문이나 보고서를 작성해달라고 했을때 backend에 저장된 혹은 frontend에 저장된 template 말고 제2뇌 지식으로 최선의 결과물을 만들고 싶어',
|
|
brainRoot,
|
|
{ force: true }
|
|
);
|
|
const indexDoc = trace.retrievedDocuments.find((doc) => doc.path === 'AI_and_ML/AI_and_ML.md');
|
|
const context = renderSecondBrainTraceContext(trace);
|
|
|
|
if (indexDoc) {
|
|
expect(indexDoc.knowledgeRole).toBe('routing-hint');
|
|
expect(indexDoc.usedInAnswer).not.toBe(true);
|
|
}
|
|
expect(trace.knowledgeSlots.some((slot) => slot.selectedPaths.includes('AI_and_ML/AI_and_ML.md'))).toBe(false);
|
|
expect(trace.knowledgeSlots.find((slot) => slot.id === 'writing')?.retrievalQuery).not.toContain('/Volumes');
|
|
expect(trace.knowledgeSlots.find((slot) => slot.id === 'writing')?.retrievalQuery).not.toContain('사용자가');
|
|
expect(trace.knowledgeSlots.find((slot) => slot.id === 'writing')?.selectedPaths.join('\n')).toContain('Report Narrative Structure.md');
|
|
expect(context).toContain('Index rule');
|
|
});
|
|
});
|