Bump version to 2.35.0: Knowledge Resilience & Standardization Milestone.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { MarkdownFileWriter } from './markdownFileWriter';
|
||||
import {
|
||||
BugRecord,
|
||||
ChronicleRecordEntry,
|
||||
ChronicleWriteResult,
|
||||
DecisionRecord,
|
||||
DevelopmentLog,
|
||||
DiscussionRecord,
|
||||
PlanningDocument,
|
||||
ProjectProfile,
|
||||
RetrospectiveRecord
|
||||
} from './types';
|
||||
import {
|
||||
renderBugRecord,
|
||||
renderDecisionRecord,
|
||||
renderDevelopmentLog,
|
||||
renderDiscussionRecord,
|
||||
renderPlanningDocument,
|
||||
renderProjectProfile,
|
||||
renderProjectReadme,
|
||||
renderRetrospective,
|
||||
renderTimelineSeed
|
||||
} from './templates';
|
||||
|
||||
export * from './types';
|
||||
export * from './guardPrompt';
|
||||
|
||||
const sectionDirs = ['planning', 'discussions', 'decisions', 'development', 'bugs', 'retrospectives'];
|
||||
|
||||
export class ProjectChronicleManager {
|
||||
private readonly writer = new MarkdownFileWriter();
|
||||
|
||||
public ensureProject(profile: ProjectProfile): void {
|
||||
if (!profile.recordRoot || !profile.recordRoot.trim()) {
|
||||
throw new Error('Record root is required before writing chronicle documents.');
|
||||
}
|
||||
|
||||
this.writer.ensureDir(profile.recordRoot);
|
||||
for (const dir of sectionDirs) {
|
||||
this.writer.ensureDir(path.join(profile.recordRoot, dir));
|
||||
}
|
||||
|
||||
this.writeSeedFile(path.join(profile.recordRoot, 'README.md'), renderProjectReadme(profile));
|
||||
this.writeSeedFile(path.join(profile.recordRoot, 'project-profile.md'), renderProjectProfile(profile));
|
||||
this.writeSeedFile(path.join(profile.recordRoot, 'timeline.md'), renderTimelineSeed(profile));
|
||||
this.writeProjectConfig(profile);
|
||||
}
|
||||
|
||||
public writePlanning(profile: ProjectProfile, doc: PlanningDocument): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(doc.createdAt);
|
||||
const fileName = `${date}_${this.slug(doc.featureName)}.md`;
|
||||
return this.write(profile, 'planning', fileName, renderPlanningDocument(doc));
|
||||
}
|
||||
|
||||
public writeDiscussion(profile: ProjectProfile, record: DiscussionRecord): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(record.createdAt);
|
||||
const fileName = `${date}_${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'discussions', fileName, renderDiscussionRecord(record));
|
||||
}
|
||||
|
||||
public writeDecision(profile: ProjectProfile, record: DecisionRecord, adrNumber: number): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const padded = String(adrNumber).padStart(4, '0');
|
||||
const fileName = `ADR-${padded}-${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'decisions', fileName, renderDecisionRecord(record));
|
||||
}
|
||||
|
||||
public writeDevelopmentLog(profile: ProjectProfile, log: DevelopmentLog): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(log.createdAt);
|
||||
const fileName = `${date}_${this.slug(log.featureName)}_implementation.md`;
|
||||
return this.write(profile, 'development', fileName, renderDevelopmentLog(log));
|
||||
}
|
||||
|
||||
public writeBug(profile: ProjectProfile, record: BugRecord, bugNumber: number): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const padded = String(bugNumber).padStart(4, '0');
|
||||
const fileName = `BUG-${padded}-${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'bugs', fileName, renderBugRecord(record));
|
||||
}
|
||||
|
||||
public writeRetrospective(profile: ProjectProfile, record: RetrospectiveRecord): ChronicleWriteResult {
|
||||
this.ensureProject(profile);
|
||||
const date = this.datePart(record.createdAt);
|
||||
const fileName = `${date}_${this.slug(record.title)}.md`;
|
||||
return this.write(profile, 'retrospectives', fileName, renderRetrospective(record));
|
||||
}
|
||||
|
||||
public appendTimeline(profile: ProjectProfile, lines: string[], createdAt: string = new Date().toISOString()): void {
|
||||
this.ensureProject(profile);
|
||||
const timelinePath = path.join(profile.recordRoot, 'timeline.md');
|
||||
const markdown = [
|
||||
'',
|
||||
`## ${this.datePart(createdAt)}`,
|
||||
...lines.map(line => `- ${line}`)
|
||||
].join('\n');
|
||||
this.writer.appendMarkdown(timelinePath, markdown);
|
||||
}
|
||||
|
||||
public nextAdrNumber(profile: ProjectProfile): number {
|
||||
return this.nextNumber(path.join(profile.recordRoot, 'decisions'), /^ADR-(\d+)/);
|
||||
}
|
||||
|
||||
public nextBugNumber(profile: ProjectProfile): number {
|
||||
return this.nextNumber(path.join(profile.recordRoot, 'bugs'), /^BUG-(\d+)/);
|
||||
}
|
||||
|
||||
public listRecords(profile: ProjectProfile): ChronicleRecordEntry[] {
|
||||
this.ensureProject(profile);
|
||||
const records: ChronicleRecordEntry[] = [];
|
||||
|
||||
for (const section of sectionDirs) {
|
||||
const sectionPath = path.join(profile.recordRoot, section);
|
||||
if (!fs.existsSync(sectionPath)) continue;
|
||||
|
||||
for (const fileName of fs.readdirSync(sectionPath)) {
|
||||
if (!fileName.endsWith('.md')) continue;
|
||||
const filePath = path.join(sectionPath, fileName);
|
||||
const stat = fs.statSync(filePath);
|
||||
records.push({
|
||||
section,
|
||||
fileName,
|
||||
filePath,
|
||||
relativePath: path.relative(profile.recordRoot, filePath),
|
||||
updatedAt: stat.mtimeMs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return records.sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
}
|
||||
|
||||
private write(profile: ProjectProfile, section: string, fileName: string, markdown: string): ChronicleWriteResult {
|
||||
const filePath = this.writer.writeMarkdown(path.join(profile.recordRoot, section, fileName), markdown);
|
||||
return {
|
||||
filePath,
|
||||
relativePath: path.relative(profile.recordRoot, filePath)
|
||||
};
|
||||
}
|
||||
|
||||
private writeSeedFile(filePath: string, content: string): void {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
this.writer.writeMarkdown(filePath, content);
|
||||
}
|
||||
}
|
||||
|
||||
private writeProjectConfig(profile: ProjectProfile): void {
|
||||
const configPath = path.join(profile.recordRoot, 'chronicle.config.json');
|
||||
const config = {
|
||||
projectId: profile.projectId,
|
||||
projectName: profile.projectName,
|
||||
projectRoot: profile.projectRoot || '',
|
||||
recordRoot: profile.recordRoot,
|
||||
description: profile.description || '',
|
||||
corePurpose: profile.corePurpose || '',
|
||||
detailLevel: profile.detailLevel,
|
||||
createdAt: profile.createdAt,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
private nextNumber(dir: string, pattern: RegExp): number {
|
||||
if (!fs.existsSync(dir)) return 1;
|
||||
const max = fs.readdirSync(dir).reduce((current, fileName) => {
|
||||
const match = fileName.match(pattern);
|
||||
if (!match) return current;
|
||||
return Math.max(current, Number(match[1]) || 0);
|
||||
}, 0);
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
private slug(value: string): string {
|
||||
const slug = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9가-힣]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 60);
|
||||
return slug || 'record';
|
||||
}
|
||||
|
||||
private datePart(iso?: string): string {
|
||||
return (iso || new Date().toISOString()).slice(0, 10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user