Bump version to 2.35.0: Knowledge Resilience & Standardization Milestone.

This commit is contained in:
g1nation
2026-05-02 13:18:07 +09:00
parent 11f25534d0
commit 8bb8c065d7
27 changed files with 2452 additions and 14 deletions
@@ -0,0 +1,50 @@
import { ProjectProfile } from './types';
export function buildProjectChronicleGuardContext(project: ProjectProfile | null): string {
const hasUsableProject = !!project?.recordRoot?.trim();
const projectLines = project ? [
`Project selection status: selected`,
`Active record project: ${project.projectName}`,
`Project root: ${project.projectRoot || 'Not set'}`,
`Record root: ${project.recordRoot}`,
`Core purpose: ${project.corePurpose || project.description || 'Not captured yet.'}`,
`Record detail level: ${project.detailLevel}`
] : [
'Project selection status: not selected',
'No active record project is selected. Before writing records, ask the user to select or create one.'
];
return [
...projectLines,
'',
'This guard is active for project ideas, feature requests, architecture proposals, implementation planning, and design decisions.',
'',
'Required response order for new ideas or feature requests:',
'1. Request summary.',
'2. Inferred user intent.',
'3. Project record target check. If no project is selected, ask whether to use an existing project, create a new project, or skip recording.',
'4. Record path check. If no record root is available, say a Markdown record path is required before writing files.',
'5. Ask only 1 to 3 blocking questions.',
'6. For every question, include "Question reason" explaining why it changes storage, scope, dependencies, or implementation direction.',
'7. Direction review focused on project fit and dependency risk.',
'8. Recommend a low-dependency MVP first.',
'9. Put Vector DB, relational DB, knowledge graph, semantic search, and complex automation only under "Later expansion" unless the user explicitly asks for them now.',
'10. End with "Candidate records for this discussion" and list planning, discussions, decisions, development, bugs, or retrospectives paths as candidates.',
'',
'Decision policy:',
'- Do not mark a decision as accepted until the user confirms it.',
'- Before confirmation, call decisions "candidates" or "pending".',
'- Prefer "reduced adoption" when the idea is useful but too large for the MVP.',
'',
'Tone and scope:',
'- Be practical and plain-spoken.',
'- Avoid grand phrases like advanced cognitive architecture, compounding knowledge, perfect graph, or ultimate knowledge distiller.',
'- When the user wants low dependency, keep the first proposal to Markdown, JSON, local files, and explicit user save actions.',
'- Do not jump directly to large architectures. Narrow direction before expanding.',
'',
hasUsableProject
? 'The current project and record root are available, so candidate record paths may use this project.'
: 'The project or record root is missing, so candidate records must be described but not written.',
'Do not claim a Markdown file was written unless a tool or explicit sidebar action actually wrote it.'
].join('\n');
}
+189
View File
@@ -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);
}
}
@@ -0,0 +1,41 @@
import * as fs from 'fs';
import * as path from 'path';
export class MarkdownFileWriter {
public ensureDir(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
public writeMarkdown(filePath: string, content: string): string {
this.ensureDir(path.dirname(filePath));
const finalPath = this.getAvailablePath(filePath);
fs.writeFileSync(finalPath, this.normalize(content), 'utf8');
return finalPath;
}
public appendMarkdown(filePath: string, content: string): void {
this.ensureDir(path.dirname(filePath));
fs.appendFileSync(filePath, this.normalize(content), 'utf8');
}
private getAvailablePath(filePath: string): string {
if (!fs.existsSync(filePath)) return filePath;
const dir = path.dirname(filePath);
const ext = path.extname(filePath);
const base = path.basename(filePath, ext);
let index = 2;
while (true) {
const candidate = path.join(dir, `${base}-${index}${ext}`);
if (!fs.existsSync(candidate)) return candidate;
index += 1;
}
}
private normalize(content: string): string {
return content.replace(/\r\n/g, '\n').trimEnd() + '\n';
}
}
+258
View File
@@ -0,0 +1,258 @@
import {
BugRecord,
DecisionRecord,
DevelopmentLog,
DiscussionRecord,
PlanningDocument,
ProjectProfile,
RetrospectiveRecord
} from './types';
const list = (items: string[] | undefined, fallback: string = 'Not captured yet.') => {
if (!items || items.length === 0) return fallback;
return items.map(item => `- ${item}`).join('\n');
};
const dateOnly = (iso?: string) => (iso || new Date().toISOString()).slice(0, 10);
export function renderProjectReadme(profile: ProjectProfile): string {
return [
`# ${profile.projectName} Chronicle Records`,
'',
'## Project',
`- ID: ${profile.projectId}`,
`- Root: ${profile.projectRoot || 'Not set'}`,
`- Record root: ${profile.recordRoot}`,
`- Detail level: ${profile.detailLevel}`,
'',
'## Purpose',
profile.corePurpose || profile.description || 'Not captured yet.',
'',
'## Folders',
'- `planning/`',
'- `discussions/`',
'- `decisions/`',
'- `development/`',
'- `bugs/`',
'- `retrospectives/`'
].join('\n');
}
export function renderProjectProfile(profile: ProjectProfile): string {
return [
'# Project Profile',
'',
'## Project Name',
profile.projectName,
'',
'## Description',
profile.description || 'Not captured yet.',
'',
'## Project Root',
profile.projectRoot || 'Not set',
'',
'## Record Root',
profile.recordRoot,
'',
'## Core Purpose',
profile.corePurpose || 'Not captured yet.',
'',
'## Target Users',
list(profile.targetUsers),
'',
'## Avoid Directions',
list(profile.avoidDirections),
'',
'## Record Detail Level',
profile.detailLevel,
'',
'## Created',
profile.createdAt,
'',
'## Updated',
profile.updatedAt
].join('\n');
}
export function renderTimelineSeed(profile: ProjectProfile): string {
return [
'# Project Timeline',
'',
`## ${dateOnly(profile.createdAt)}`,
`- Project Chronicle record folder initialized for ${profile.projectName}.`
].join('\n');
}
export function renderPlanningDocument(doc: PlanningDocument): string {
return [
`# Feature Plan: ${doc.featureName}`,
'',
'## 1. Feature Name',
doc.featureName,
'',
'## 2. Reason',
doc.purpose,
'',
'## 3. Original User Request',
doc.sourceRequest || 'Not captured yet.',
'',
'## 4. Interpreted User Intent',
doc.userIntent,
'',
'## 5. Background',
doc.background,
'',
'## 6. Scope',
list(doc.scope),
'',
'## 7. Out Of Scope',
list(doc.outOfScope),
'',
'## 8. Development Direction',
doc.developmentDirection,
'',
'## 9. Dependency Strategy',
doc.dependencyStrategy,
'',
'## 10. Expected Value',
doc.expectedValue,
'',
'## 11. Success Criteria',
list(doc.successCriteria),
'',
'## 12. Developer Instruction',
doc.developerInstruction
].join('\n');
}
export function renderDiscussionRecord(record: DiscussionRecord): string {
const questions = record.questions.length
? record.questions.map((q, index) => [
`### Question ${index + 1}`,
q.question,
'',
'### Reason',
q.reason,
'',
'### Expected Information',
q.expectedInformation,
'',
'### Impact On Decision',
q.impactOnDecision,
'',
q.userAnswer ? `### User Answer\n${q.userAnswer}` : '',
q.result ? `### Result\n${q.result}` : ''
].filter(Boolean).join('\n')).join('\n\n')
: 'No explicit question was captured.';
return [
`# Discussion: ${record.title}`,
'',
'## User Request Summary',
record.userRequest,
'',
'## Interpreted Intent',
record.interpretedIntent,
'',
'## Questions',
questions,
'',
'## Main Discussion',
list(record.discussions),
'',
'## Decisions',
record.decisions.length
? record.decisions.map(decision => `- ${decision.title}: ${decision.decision}`).join('\n')
: 'No decisions captured yet.'
].join('\n');
}
export function renderDecisionRecord(record: DecisionRecord): string {
return [
`# ADR: ${record.title}`,
'',
'## Status',
record.status,
'',
'## Context',
record.context,
'',
'## Decision',
record.decision,
'',
'## Reason',
record.reason,
'',
'## Alternatives',
list(record.alternatives),
'',
'## Consequences',
list(record.consequences)
].join('\n');
}
export function renderDevelopmentLog(log: DevelopmentLog): string {
return [
`# Development Log: ${log.featureName}`,
'',
'## Purpose',
log.purpose,
'',
'## Implementation Summary',
log.implementationSummary,
'',
'## Architecture',
log.architecture,
'',
'## Changed Files',
list(log.changedFiles),
'',
'## Dependency Notes',
log.dependencyNotes,
'',
'## Bugs',
log.bugs.length ? log.bugs.map(bug => `- ${bug.title}: ${bug.symptom}`).join('\n') : 'No bugs recorded.',
'',
'## Lessons',
list(log.lessons)
].join('\n');
}
export function renderBugRecord(record: BugRecord): string {
return [
`# Bug: ${record.title}`,
'',
'## Date',
dateOnly(record.createdAt),
'',
'## Symptom',
record.symptom,
'',
'## Cause',
record.cause,
'',
'## Fix',
record.fix,
'',
'## Prevention',
record.prevention
].join('\n');
}
export function renderRetrospective(record: RetrospectiveRecord): string {
return [
`# Retrospective: ${record.title}`,
'',
'## Summary',
record.summary,
'',
'## Went Well',
list(record.wentWell),
'',
'## To Improve',
list(record.toImprove),
'',
'## Next Actions',
list(record.nextActions)
].join('\n');
}
+104
View File
@@ -0,0 +1,104 @@
export type ChronicleDetailLevel = 'simple' | 'standard' | 'detailed';
export interface ProjectProfile {
projectId: string;
projectName: string;
projectRoot?: string;
recordRoot: string;
description?: string;
corePurpose?: string;
targetUsers?: string[];
avoidDirections?: string[];
detailLevel: ChronicleDetailLevel;
createdAt: string;
updatedAt: string;
}
export interface QuestionRecord {
question: string;
reason: string;
expectedInformation: string;
impactOnDecision: string;
userAnswer?: string;
result?: string;
}
export interface DecisionRecord {
title: string;
status: 'accepted' | 'rejected' | 'deferred' | 'changed';
context: string;
decision: string;
reason: string;
alternatives?: string[];
consequences?: string[];
createdAt?: string;
}
export interface PlanningDocument {
featureName: string;
purpose: string;
background: string;
userIntent: string;
scope: string[];
outOfScope: string[];
developmentDirection: string;
dependencyStrategy: string;
expectedValue: string;
successCriteria: string[];
developerInstruction: string;
sourceRequest?: string;
createdAt?: string;
}
export interface DiscussionRecord {
title: string;
userRequest: string;
interpretedIntent: string;
questions: QuestionRecord[];
discussions: string[];
decisions: DecisionRecord[];
createdAt?: string;
}
export interface DevelopmentLog {
featureName: string;
purpose: string;
implementationSummary: string;
architecture: string;
changedFiles: string[];
dependencyNotes: string;
bugs: BugRecord[];
lessons: string[];
createdAt?: string;
}
export interface BugRecord {
title: string;
symptom: string;
cause: string;
fix: string;
prevention: string;
createdAt?: string;
}
export interface RetrospectiveRecord {
title: string;
summary: string;
wentWell: string[];
toImprove: string[];
nextActions: string[];
createdAt?: string;
}
export interface ChronicleWriteResult {
filePath: string;
relativePath: string;
}
export interface ChronicleRecordEntry {
section: string;
fileName: string;
filePath: string;
relativePath: string;
updatedAt: number;
}