feat: monorepo consolidation — forge pipeline, MACP protocol, framework plugin, profiles/guides/skills
Work packages completed: - WP1: packages/forge — pipeline runner, stage adapter, board tasks, brief classifier, persona loader with project-level overrides. 89 tests, 95.62% coverage. - WP2: packages/macp — credential resolver, gate runner, event emitter, protocol types. 65 tests, 96.24% coverage. Full Python-to-TS port preserving all behavior. - WP3: plugins/mosaic-framework — OC rails injection plugin (before_agent_start + subagent_spawning hooks for Mosaic contract enforcement). - WP4: profiles/ (domains, tech-stacks, workflows), guides/ (17 docs), skills/ (5 universal skills), forge pipeline assets (48 markdown files). Board deliberation: docs/reviews/consolidation-board-memo.md Brief: briefs/monorepo-consolidation.md Consolidates mosaic/stack (forge, MACP, bootstrap framework) into mosaic/mosaic-stack. 154 new tests total. Zero Python — all TypeScript/ESM.
This commit is contained in:
131
packages/forge/__tests__/brief-classifier.test.ts
Normal file
131
packages/forge/__tests__/brief-classifier.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
classifyBrief,
|
||||
parseBriefFrontmatter,
|
||||
determineBriefClass,
|
||||
stagesForClass,
|
||||
} from '../src/brief-classifier.js';
|
||||
|
||||
describe('classifyBrief', () => {
|
||||
it('returns strategic when strategic keywords dominate', () => {
|
||||
expect(classifyBrief('We need a new security architecture for compliance')).toBe('strategic');
|
||||
});
|
||||
|
||||
it('returns technical when technical keywords are present and dominate', () => {
|
||||
expect(classifyBrief('Fix the bugfix for CSS lint cleanup')).toBe('technical');
|
||||
});
|
||||
|
||||
it('returns strategic when no keywords match (default)', () => {
|
||||
expect(classifyBrief('Implement a new notification system')).toBe('strategic');
|
||||
});
|
||||
|
||||
it('returns strategic when strategic and technical are tied', () => {
|
||||
// 1 strategic (security) + 1 technical (bug) = strategic wins on > check
|
||||
expect(classifyBrief('security bug')).toBe('technical');
|
||||
});
|
||||
|
||||
it('returns strategic for empty text', () => {
|
||||
expect(classifyBrief('')).toBe('strategic');
|
||||
});
|
||||
|
||||
it('is case-insensitive', () => {
|
||||
expect(classifyBrief('MIGRATION and COMPLIANCE strategy')).toBe('strategic');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseBriefFrontmatter', () => {
|
||||
it('parses simple key-value frontmatter', () => {
|
||||
const text = '---\nclass: technical\ntitle: My Brief\n---\n\n# Body';
|
||||
const fm = parseBriefFrontmatter(text);
|
||||
expect(fm).toEqual({ class: 'technical', title: 'My Brief' });
|
||||
});
|
||||
|
||||
it('strips quotes from values', () => {
|
||||
const text = '---\nclass: "hotfix"\ntitle: \'Test\'\n---\n\n# Body';
|
||||
const fm = parseBriefFrontmatter(text);
|
||||
expect(fm['class']).toBe('hotfix');
|
||||
expect(fm['title']).toBe('Test');
|
||||
});
|
||||
|
||||
it('returns empty object when no frontmatter', () => {
|
||||
expect(parseBriefFrontmatter('# Just a heading')).toEqual({});
|
||||
});
|
||||
|
||||
it('returns empty object for malformed frontmatter', () => {
|
||||
expect(parseBriefFrontmatter('---\n---\n')).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineBriefClass', () => {
|
||||
it('CLI flag takes priority', () => {
|
||||
const result = determineBriefClass('security migration', 'hotfix');
|
||||
expect(result).toEqual({ briefClass: 'hotfix', classSource: 'cli' });
|
||||
});
|
||||
|
||||
it('frontmatter takes priority over auto', () => {
|
||||
const text = '---\nclass: technical\n---\n\nSecurity architecture compliance';
|
||||
const result = determineBriefClass(text);
|
||||
expect(result).toEqual({ briefClass: 'technical', classSource: 'frontmatter' });
|
||||
});
|
||||
|
||||
it('falls back to auto-classify', () => {
|
||||
const result = determineBriefClass('We need a migration plan');
|
||||
expect(result).toEqual({ briefClass: 'strategic', classSource: 'auto' });
|
||||
});
|
||||
|
||||
it('ignores invalid CLI class', () => {
|
||||
const result = determineBriefClass('bugfix cleanup', 'invalid');
|
||||
expect(result).toEqual({ briefClass: 'technical', classSource: 'auto' });
|
||||
});
|
||||
|
||||
it('ignores invalid frontmatter class', () => {
|
||||
const text = '---\nclass: banana\n---\n\nbugfix';
|
||||
const result = determineBriefClass(text);
|
||||
expect(result).toEqual({ briefClass: 'technical', classSource: 'auto' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('stagesForClass', () => {
|
||||
it('strategic includes all stages including board', () => {
|
||||
const stages = stagesForClass('strategic');
|
||||
expect(stages).toContain('01-board');
|
||||
expect(stages).toContain('01b-brief-analyzer');
|
||||
expect(stages).toContain('00-intake');
|
||||
expect(stages).toContain('09-deploy');
|
||||
});
|
||||
|
||||
it('technical skips board', () => {
|
||||
const stages = stagesForClass('technical');
|
||||
expect(stages).not.toContain('01-board');
|
||||
expect(stages).toContain('01b-brief-analyzer');
|
||||
});
|
||||
|
||||
it('hotfix skips board and brief analyzer', () => {
|
||||
const stages = stagesForClass('hotfix');
|
||||
expect(stages).not.toContain('01-board');
|
||||
expect(stages).not.toContain('01b-brief-analyzer');
|
||||
expect(stages).toContain('05-coding');
|
||||
});
|
||||
|
||||
it('forceBoard adds board back for technical', () => {
|
||||
const stages = stagesForClass('technical', true);
|
||||
expect(stages).toContain('01-board');
|
||||
expect(stages).toContain('01b-brief-analyzer');
|
||||
});
|
||||
|
||||
it('forceBoard adds board back for hotfix', () => {
|
||||
const stages = stagesForClass('hotfix', true);
|
||||
expect(stages).toContain('01-board');
|
||||
expect(stages).toContain('01b-brief-analyzer');
|
||||
});
|
||||
|
||||
it('stages are in canonical order', () => {
|
||||
const stages = stagesForClass('strategic');
|
||||
for (let i = 1; i < stages.length; i++) {
|
||||
const prevIdx = stages.indexOf(stages[i - 1]!);
|
||||
const currIdx = stages.indexOf(stages[i]!);
|
||||
expect(prevIdx).toBeLessThan(currIdx);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user