Files
connectai/tests/secondBrainTrace.test.ts
T
2026-05-03 20:40:40 +09:00

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'),
[
'# Astra 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');
});
});