Files
connectai/src/memory/ProjectMemory.ts
T

213 lines
6.4 KiB
TypeScript

/**
* ============================================================
* 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
};
}
}