/** * ============================================================ * Project Memory (프로젝트 기억) * * 프로젝트별 요구사항, 코드 구조, 아키텍처 결정, 버그 기록 등을 * Astra 확장 프로그램 내부에 저장하고 관리합니다. * 저장 위치: {ConnectAI}/.astra/project_memory.json * (기존: {projectRoot}/.astra/ → 변경됨) * ============================================================ */ import * as fs from 'fs'; import * as path from 'path'; import * as crypto from 'crypto'; import { ProjectMemoryStore, ArchitectureDecision, BugRecord, MemoryContextResult } from './types'; import { getAstraDataDir } from '../core/astraPath'; export class ProjectMemory { private store: ProjectMemoryStore; private filePath: string; private dirty = false; constructor(projectRoot: string) { // .astra 디렉토리를 ConnectAI 내부에서 해결 (사용자 프로젝트 루트에 생성하지 않음) const astraDir = getAstraDataDir(); this.filePath = path.join(astraDir, 'project_memory.json'); this.store = this.load(projectRoot); } // ─── Persistence ─── private load(projectRoot: string): ProjectMemoryStore { try { if (fs.existsSync(this.filePath)) { const raw = fs.readFileSync(this.filePath, 'utf-8'); return JSON.parse(raw) as ProjectMemoryStore; } } catch { /* start fresh */ } return { version: 1, projectId: this.hashPath(projectRoot), projectName: path.basename(projectRoot), techStack: [], architectureDecisions: [], bugRecords: [], requirements: [], designDirection: '', codeConventions: [], lastUpdated: Date.now() }; } public save(): void { if (!this.dirty) return; try { this.store.lastUpdated = Date.now(); fs.writeFileSync(this.filePath, JSON.stringify(this.store, null, 2), 'utf-8'); this.dirty = false; } catch { /* silently fail */ } } private hashPath(p: string): string { return crypto.createHash('sha256').update(p).digest('hex').slice(0, 12); } // ─── Getters ─── public getStore(): ProjectMemoryStore { return { ...this.store }; } // ─── Tech Stack ─── public setTechStack(stack: string[]): void { this.store.techStack = [...new Set(stack)]; this.dirty = true; this.save(); } public addTechStack(tech: string): void { if (!this.store.techStack.includes(tech)) { this.store.techStack.push(tech); this.dirty = true; this.save(); } } // ─── Architecture Decisions ─── public addArchitectureDecision( title: string, decision: string, rationale: string, alternatives: string[] = [] ): ArchitectureDecision { const entry: ArchitectureDecision = { id: crypto.randomUUID(), title, decision, rationale, alternatives, date: Date.now() }; this.store.architectureDecisions.push(entry); this.dirty = true; this.save(); return entry; } // ─── Bug Records ─── public addBugRecord( description: string, rootCause: string, fix: string, relatedFiles: string[] = [] ): BugRecord { const entry: BugRecord = { id: crypto.randomUUID(), description, rootCause, fix, date: Date.now(), relatedFiles }; this.store.bugRecords.push(entry); this.dirty = true; this.save(); return entry; } // ─── Requirements ─── public addRequirement(req: string): void { if (!this.store.requirements.includes(req)) { this.store.requirements.push(req); this.dirty = true; this.save(); } } // ─── Design Direction ─── public setDesignDirection(direction: string): void { this.store.designDirection = direction; this.dirty = true; this.save(); } // ─── Code Conventions ─── public addCodeConvention(convention: string): void { if (!this.store.codeConventions.includes(convention)) { this.store.codeConventions.push(convention); this.dirty = true; this.save(); } } // ─── Context Building ─── public buildContext(currentPrompt: string): MemoryContextResult | null { const sections: string[] = []; if (this.store.techStack.length > 0) { sections.push(`Tech Stack: ${this.store.techStack.join(', ')}`); } if (this.store.designDirection) { sections.push(`Design Direction: ${this.store.designDirection}`); } if (this.store.codeConventions.length > 0) { sections.push(`Code Conventions:\n${this.store.codeConventions.map((c) => ` - ${c}`).join('\n')}`); } if (this.store.requirements.length > 0) { const reqs = this.store.requirements.slice(-5); sections.push(`Recent Requirements:\n${reqs.map((r) => ` - ${r}`).join('\n')}`); } // Show recent architecture decisions (last 3) if (this.store.architectureDecisions.length > 0) { const recent = this.store.architectureDecisions .sort((a, b) => b.date - a.date) .slice(0, 3); sections.push( `Architecture Decisions:\n${recent.map((d) => ` - ${d.title}: ${d.decision}`).join('\n')}` ); } // Show recent bugs (last 3) if (this.store.bugRecords.length > 0) { const recent = this.store.bugRecords .sort((a, b) => b.date - a.date) .slice(0, 3); sections.push( `Recent Bugs:\n${recent.map((b) => ` - ${b.description} → ${b.fix}`).join('\n')}` ); } if (sections.length === 0) return null; return { layer: 'project', label: `Project Memory (${this.store.projectName})`, content: sections.join('\n'), relevance: 0.8 }; } }