Files
connectai/tests/secondBrainTrace.test.ts
T

408 lines
22 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.',
'Customer journey mapping reveals how users navigate from initial interest to final purchase decision.',
'The approval process evaluates customer experience quality, conversion flow effectiveness, and business value proposition.',
'Product discovery in virtual stores requires intuitive spatial navigation and curated customer journey touchpoints.',
'Stakeholder approval criteria include requirement fit assessment, business value validation, and acceptance criteria verification.',
'Virtual store UX should prioritize customer journey continuity, product discovery efficiency, and conversion optimization.',
'Business value is measured through customer engagement metrics, approval rates, and conversion funnel analysis.'
].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.',
'Evidence mapping connects report sections to factual source documents and verified project records.',
'Each report template section should link to concrete evidence, analytical insight, identified risk, and actionable next steps.',
'The evidence layer provides facts and source documentation for the report body.',
'Insight sections synthesize patterns from evidence into strategic analysis and interpretation.',
'Risk sections document limitations, validation gaps, tradeoffs, and items requiring verification.',
'Action sections translate knowledge into MVP implementation steps, recommendations, and decision items.'
].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.',
'An ontology provides a taxonomy of concept types, relation types, and category hierarchies.',
'Knowledge graph structure organizes domain concepts into navigable networks of meaning.',
'Ontology-driven classification helps reports maintain consistent concept definitions and relation mappings.',
'Graph-based concept organization enables systematic categorization and cross-referencing of knowledge.'
].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.',
'Report writing structure defines the narrative arc from executive summary through detailed analysis to conclusions.',
'Template-based writing organizes content into headline, body, evidence citation, and recommendation sections.',
'Good report narrative maintains logical flow between sections while preserving analytical rigor.',
'Writing style guidance ensures consistent tone, appropriate formality, and reader-oriented structure across reports.'
].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.',
'Implementation technique documentation covers method selection, architecture decision rationale, and tool evaluation.',
'Technical references provide concrete implementation patterns, code architecture examples, and integration approaches.',
'Technique notes bridge the gap between strategic analysis and practical engineering execution.',
'Architecture technique guides help translate report findings into actionable technical implementation plans.'
].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');
});
});