/** * Drift lock: the inline 3-tier proposal-taxonomy tables embedded in the BA * skills (deployed standalone, markdown-only) must stay consistent with the * canonical `lib/proposal-tiers.ts`. Edit ALL carriers or this suite fails. * * - `proposal-tiers:v1` → carried by _workflow/proposal-method.md (canonical * methodology, repo-only), create-menu, create-use-case (discovery), * create-business-rules (identify) and create-screen. */ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { PROPOSAL_TIERS, PROPOSAL_TIERS_MARKER, renderProposalTiersBlock } from '../proposal-tiers.js' const skillsRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..') const TIER_CARRIERS = [ 'business-analyse/_workflow/proposal-method.md', 'business-analyse/create-menu/SKILL.md', 'business-analyse/create-use-case/levels/discovery.md', 'business-analyse/create-business-rules/levels/identify.md', 'business-analyse/create-screen/SKILL.md', ] function readSkill(rel: string): string { return readFileSync(join(skillsRoot, rel), 'utf8') } function extractBlock(md: string, marker: string, file: string): string { const re = new RegExp(`([\\s\\S]*?)`) const m = md.match(re) if (!m) throw new Error(`marker "${marker}" not found in ${file}`) return m[1] } function tableRows(block: string): string[][] { return block .split('\n') .map(l => l.trim()) .filter(l => l.startsWith('|')) .filter(l => !/^\|(\s*:?-+:?\s*\|)+$/.test(l)) .slice(1) // drop the header row .map(l => l.split('|').slice(1, -1).map(c => c.trim())) } describe.each(TIER_CARRIERS)('proposal-tiers drift — %s', (file) => { it('carries the proposal-tiers:v1 table equal to PROPOSAL_TIERS', () => { const rows = tableRows(extractBlock(readSkill(file), PROPOSAL_TIERS_MARKER, file)).map(cells => ({ name: cells[0].replace(/\*\*/g, ''), meaning: cells[1], testQuestion: cells[2], })) const expected = PROPOSAL_TIERS.map(t => ({ name: t.name, meaning: t.meaning, testQuestion: t.testQuestion, })) expect(rows).toEqual(expected) }) }) describe('renderProposalTiersBlock', () => { it('round-trips through the same parser the carriers are checked with', () => { const rows = tableRows(extractBlock(renderProposalTiersBlock(), PROPOSAL_TIERS_MARKER, '')) expect(rows).toHaveLength(PROPOSAL_TIERS.length) expect(rows.map(c => c[0].replace(/\*\*/g, ''))).toEqual(PROPOSAL_TIERS.map(t => t.name)) }) })