Bump version to 2.35.0: Knowledge Resilience & Standardization Milestone.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { ProjectChronicleManager, ProjectProfile } from '../src/features/projectChronicle';
|
||||
|
||||
function makeProfile(root: string): ProjectProfile {
|
||||
const now = '2026-05-02T00:00:00.000Z';
|
||||
return {
|
||||
projectId: 'test-project',
|
||||
projectName: 'Test Project',
|
||||
projectRoot: root,
|
||||
recordRoot: path.join(root, 'records', 'Test Project'),
|
||||
description: 'Test project records',
|
||||
corePurpose: 'Verify chronicle records',
|
||||
detailLevel: 'standard',
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
}
|
||||
|
||||
describe('ProjectChronicleManager', () => {
|
||||
let tempRoot: string;
|
||||
let manager: ProjectChronicleManager;
|
||||
let profile: ProjectProfile;
|
||||
|
||||
beforeEach(() => {
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'chronicle-'));
|
||||
manager = new ProjectChronicleManager();
|
||||
profile = makeProfile(tempRoot);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('creates the project record structure and seed files', () => {
|
||||
manager.ensureProject(profile);
|
||||
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'README.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'chronicle.config.json'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'project-profile.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'timeline.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'planning'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'decisions'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'development'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(profile.recordRoot, 'bugs'))).toBe(true);
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(path.join(profile.recordRoot, 'chronicle.config.json'), 'utf8'));
|
||||
expect(config.projectId).toBe(profile.projectId);
|
||||
expect(config.recordRoot).toBe(profile.recordRoot);
|
||||
});
|
||||
|
||||
it('writes planning records without overwriting existing files', () => {
|
||||
const first = manager.writePlanning(profile, {
|
||||
featureName: 'Record Guard',
|
||||
purpose: 'Purpose',
|
||||
background: 'Background',
|
||||
userIntent: 'Intent',
|
||||
scope: ['Scope'],
|
||||
outOfScope: ['Out'],
|
||||
developmentDirection: 'Direction',
|
||||
dependencyStrategy: 'Dependency',
|
||||
expectedValue: 'Value',
|
||||
successCriteria: ['Success'],
|
||||
developerInstruction: 'Instruction',
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
});
|
||||
|
||||
const second = manager.writePlanning(profile, {
|
||||
featureName: 'Record Guard',
|
||||
purpose: 'Purpose',
|
||||
background: 'Background',
|
||||
userIntent: 'Intent',
|
||||
scope: ['Scope'],
|
||||
outOfScope: ['Out'],
|
||||
developmentDirection: 'Direction',
|
||||
dependencyStrategy: 'Dependency',
|
||||
expectedValue: 'Value',
|
||||
successCriteria: ['Success'],
|
||||
developerInstruction: 'Instruction',
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
});
|
||||
|
||||
expect(first.filePath).not.toBe(second.filePath);
|
||||
expect(path.basename(second.filePath)).toBe('2026-05-02_record-guard-2.md');
|
||||
});
|
||||
|
||||
it('increments ADR and bug numbers from existing files', () => {
|
||||
manager.ensureProject(profile);
|
||||
fs.writeFileSync(path.join(profile.recordRoot, 'decisions', 'ADR-0007-existing.md'), '# Existing\n');
|
||||
fs.writeFileSync(path.join(profile.recordRoot, 'bugs', 'BUG-0003-existing.md'), '# Existing\n');
|
||||
|
||||
expect(manager.nextAdrNumber(profile)).toBe(8);
|
||||
expect(manager.nextBugNumber(profile)).toBe(4);
|
||||
});
|
||||
|
||||
it('writes discussion, decision, bug, development, and retrospective records', () => {
|
||||
const discussion = manager.writeDiscussion(profile, {
|
||||
title: 'Question Intent',
|
||||
userRequest: 'Record why the AI asked a question.',
|
||||
interpretedIntent: 'Preserve reasoning behind clarification.',
|
||||
questions: [{
|
||||
question: 'Which project should this be recorded under?',
|
||||
reason: 'Records must not be mixed across projects.',
|
||||
expectedInformation: 'The active project name and record path.',
|
||||
impactOnDecision: 'Determines where generated Markdown is written.'
|
||||
}],
|
||||
discussions: ['The project context must be explicit before writing files.'],
|
||||
decisions: [],
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
});
|
||||
|
||||
const decision = manager.writeDecision(profile, {
|
||||
title: 'Use Markdown Records',
|
||||
status: 'accepted',
|
||||
context: 'Records should be portable.',
|
||||
decision: 'Store records as Markdown.',
|
||||
reason: 'Markdown works without external services.',
|
||||
alternatives: ['External DB'],
|
||||
consequences: ['Easy to inspect in the repository.'],
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
}, manager.nextAdrNumber(profile));
|
||||
|
||||
const development = manager.writeDevelopmentLog(profile, {
|
||||
featureName: 'Chronicle Writer',
|
||||
purpose: 'Verify write paths.',
|
||||
implementationSummary: 'Wrote supported record types.',
|
||||
architecture: 'Manager -> Writer -> Markdown.',
|
||||
changedFiles: ['src/features/projectChronicle/index.ts'],
|
||||
dependencyNotes: 'No external services.',
|
||||
bugs: [],
|
||||
lessons: ['Keep record types explicit.'],
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
});
|
||||
|
||||
const bug = manager.writeBug(profile, {
|
||||
title: 'Missing Record Root',
|
||||
symptom: 'Write cannot proceed.',
|
||||
cause: 'Record root is empty.',
|
||||
fix: 'Validate profile before write.',
|
||||
prevention: 'Require selected project.',
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
}, manager.nextBugNumber(profile));
|
||||
|
||||
const retrospective = manager.writeRetrospective(profile, {
|
||||
title: 'Chronicle Iteration',
|
||||
summary: 'Record generation was expanded.',
|
||||
wentWell: ['Independent module stayed small.'],
|
||||
toImprove: ['Add richer UI later.'],
|
||||
nextActions: ['Capture events automatically.'],
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
});
|
||||
|
||||
for (const result of [discussion, decision, development, bug, retrospective]) {
|
||||
expect(fs.existsSync(result.filePath)).toBe(true);
|
||||
}
|
||||
|
||||
expect(discussion.relativePath).toContain('discussions/');
|
||||
expect(decision.relativePath).toContain('decisions/');
|
||||
expect(development.relativePath).toContain('development/');
|
||||
expect(bug.relativePath).toContain('bugs/');
|
||||
expect(retrospective.relativePath).toContain('retrospectives/');
|
||||
});
|
||||
|
||||
it('lists chronicle records across sections with relative paths', () => {
|
||||
manager.writePlanning(profile, {
|
||||
featureName: 'Record List',
|
||||
purpose: 'Purpose',
|
||||
background: 'Background',
|
||||
userIntent: 'Intent',
|
||||
scope: ['Scope'],
|
||||
outOfScope: ['Out'],
|
||||
developmentDirection: 'Direction',
|
||||
dependencyStrategy: 'Dependency',
|
||||
expectedValue: 'Value',
|
||||
successCriteria: ['Success'],
|
||||
developerInstruction: 'Instruction',
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
});
|
||||
manager.writeBug(profile, {
|
||||
title: 'List Bug',
|
||||
symptom: 'Symptom',
|
||||
cause: 'Cause',
|
||||
fix: 'Fix',
|
||||
prevention: 'Prevention',
|
||||
createdAt: '2026-05-02T00:00:00.000Z'
|
||||
}, 1);
|
||||
|
||||
const records = manager.listRecords(profile);
|
||||
|
||||
expect(records.length).toBe(2);
|
||||
expect(records.map(record => record.relativePath)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'planning/2026-05-02_record-list.md',
|
||||
'bugs/BUG-0001-list-bug.md'
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { buildProjectChronicleGuardContext, ProjectProfile } from '../src/features/projectChronicle';
|
||||
|
||||
const profile: ProjectProfile = {
|
||||
projectId: 'connectai',
|
||||
projectName: 'ConnectAI',
|
||||
projectRoot: '/workspace/ConnectAI',
|
||||
recordRoot: '/workspace/ConnectAI/docs/records/ConnectAI',
|
||||
description: 'Local AI assistant',
|
||||
corePurpose: 'Keep project knowledge traceable.',
|
||||
detailLevel: 'standard',
|
||||
createdAt: '2026-05-02T00:00:00.000Z',
|
||||
updatedAt: '2026-05-02T00:00:00.000Z'
|
||||
};
|
||||
|
||||
describe('buildProjectChronicleGuardContext', () => {
|
||||
it('requires project checks, question reasons, MVP-first scope, and candidate records', () => {
|
||||
const context = buildProjectChronicleGuardContext(profile);
|
||||
|
||||
expect(context).toContain('Project selection status: selected');
|
||||
expect(context).toContain('Project record target check');
|
||||
expect(context).toContain('Record path check');
|
||||
expect(context).toContain('Question reason');
|
||||
expect(context).toContain('Recommend a low-dependency MVP first');
|
||||
expect(context).toContain('Later expansion');
|
||||
expect(context).toContain('Candidate records for this discussion');
|
||||
expect(context).toContain('Do not mark a decision as accepted until the user confirms it');
|
||||
expect(context).toContain('Markdown, JSON, local files');
|
||||
});
|
||||
|
||||
it('handles missing project context explicitly', () => {
|
||||
const context = buildProjectChronicleGuardContext(null);
|
||||
|
||||
expect(context).toContain('Project selection status: not selected');
|
||||
expect(context).toContain('existing project, create a new project, or skip recording');
|
||||
expect(context).toContain('must be described but not written');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
buildSecondBrainTrace,
|
||||
renderSecondBrainTraceContext,
|
||||
renderSecondBrainTraceMarkdown
|
||||
} from '../src/features/secondBrainTrace';
|
||||
|
||||
describe('Second Brain Trace', () => {
|
||||
let brainRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
brainRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'second-brain-trace-'));
|
||||
fs.mkdirSync(path.join(brainRoot, 'records', 'ProjectChronicle', 'decisions'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(brainRoot, 'records', 'ProjectChronicle', 'decisions', 'ADR-0002-low-dependency-design.md'),
|
||||
[
|
||||
'# ADR-0002 Low Dependency Design',
|
||||
'',
|
||||
'Project Chronicle Guard should start with Markdown files and an independent module.',
|
||||
'Vector DB and relational DB are later expansion options, not MVP dependencies.'
|
||||
].join('\n'),
|
||||
'utf8'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(brainRoot, 'general-note.md'),
|
||||
'# General Note\n\nThis unrelated note talks about coffee and weather.',
|
||||
'utf8'
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(brainRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('retrieves and marks relevant Second Brain notes for project-specific questions', () => {
|
||||
const trace = buildSecondBrainTrace('Project Chronicle Guard MVP에서 Vector DB는 어떻게 다뤄야 해?', brainRoot);
|
||||
|
||||
expect(trace.shouldUseSecondBrain).toBe(true);
|
||||
expect(trace.secondBrainUsed).toBe(true);
|
||||
expect(trace.retrievedDocuments[0].path).toContain('ADR-0002-low-dependency-design.md');
|
||||
expect(trace.retrievedDocuments[0].usedInAnswer).toBe(true);
|
||||
expect(trace.groundingScore).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders user-facing markdown and debug JSON', () => {
|
||||
const trace = buildSecondBrainTrace('Second Brain을 참고해서 low dependency 원칙 알려줘', brainRoot, { force: true });
|
||||
const markdown = renderSecondBrainTraceMarkdown(trace, true);
|
||||
const context = renderSecondBrainTraceContext(trace);
|
||||
|
||||
expect(markdown).toContain('## 2nd Brain 사용 여부');
|
||||
expect(markdown).toContain('## 참고한 2nd Brain 문서');
|
||||
expect(markdown).toContain('## Second Brain Debug JSON');
|
||||
expect(context).toContain('[SECOND BRAIN TRACE]');
|
||||
expect(context).toContain('Retrieval query:');
|
||||
});
|
||||
|
||||
it('explains when Second Brain is not needed', () => {
|
||||
const trace = buildSecondBrainTrace('오늘 날짜가 뭐야?', brainRoot);
|
||||
|
||||
expect(trace.shouldUseSecondBrain).toBe(false);
|
||||
expect(trace.secondBrainUsed).toBe(false);
|
||||
expect(renderSecondBrainTraceMarkdown(trace)).toContain('사용하지 않음');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user