213 lines
6.4 KiB
TypeScript
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
|
|
};
|
|
}
|
|
}
|