/** * Drift lock: the inline detail-cadence tables embedded in the BA skills * (deployed standalone, markdown-only) must stay consistent with the canonical * `lib/detail-cadence.ts`. Edit ALL carriers or this suite fails. * * - `detail-cadence:v1` → carried by _workflow/detail-cadence.md (canonical * methodology, repo-only), create-use-case (detail pass) and * create-business-rules (elaborate pass). * * `_workflow/` is NOT deployed (installer.ts returns null for it), so the two * level files are the only copies that exist at runtime — a table that drifts * there is a cadence the deployed skill does not have. */ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { DEFAULT_DETAIL_CADENCE_KEY, DETAIL_CADENCES, DETAIL_CADENCE_KEYS, DETAIL_CADENCE_MARKER, NON_INTERACTIVE_DETAIL_CADENCE_KEY, renderDetailCadenceBlock, } from '../detail-cadence.js' const skillsRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..') const CADENCE_CARRIERS = [ 'business-analyse/_workflow/detail-cadence.md', 'business-analyse/create-use-case/levels/detail.md', 'business-analyse/create-business-rules/levels/elaborate.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(CADENCE_CARRIERS)('detail-cadence drift — %s', (file) => { it('carries the detail-cadence:v1 table equal to DETAIL_CADENCES', () => { const rows = tableRows(extractBlock(readSkill(file), DETAIL_CADENCE_MARKER, file)).map(cells => ({ name: cells[0].replace(/\*\*/g, ''), meaning: cells[1], gate: cells[2], })) const expected = DETAIL_CADENCES.map(c => ({ name: c.name, meaning: c.meaning, gate: c.gate, })) expect(rows).toEqual(expected) }) }) describe('renderDetailCadenceBlock', () => { it('round-trips through the same parser the carriers are checked with', () => { const rows = tableRows(extractBlock(renderDetailCadenceBlock(), DETAIL_CADENCE_MARKER, '')) expect(rows).toHaveLength(DETAIL_CADENCES.length) expect(rows.map(c => c[0].replace(/\*\*/g, ''))).toEqual(DETAIL_CADENCES.map(c => c.name)) }) }) describe('cadence keys', () => { it('pairs one machine key per cadence, in the same order', () => { expect(DETAIL_CADENCE_KEYS).toHaveLength(DETAIL_CADENCES.length) }) it('defaults to the batch cadence, and falls back to chained without a user', () => { expect(DEFAULT_DETAIL_CADENCE_KEY).toBe('batch') expect(NON_INTERACTIVE_DETAIL_CADENCE_KEY).toBe('chained') }) })