import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' import { ACTOR_RULES } from '../rules/actors.js' import { RBAC_RULES } from '../rules/rbac.js' import { DM_RULES } from '../rules/dm.js' import { CODE_RULES } from '../rules/code.js' import { UC_RULES } from '../rules/uc.js' import { BR_RULES } from '../rules/br.js' import { SCREEN_RULES } from '../rules/screens.js' import { MENU_RULES } from '../rules/menu.js' import { SECTION_RULES } from '../rules/sections.js' import { XD_RULES } from '../rules/xd.js' import { SOURCES_RULES } from '../rules/sources.js' import type { RuleDef } from '../rules/registry.js' // The SKILL.md rule texts are the AUTHORITATIVE spec; the registry is its // executable mirror. This suite locks the two together — same discipline as // the lib catalogue drift tests (core-catalog:v1, …). const HERE = dirname(fileURLToPath(import.meta.url)) const BA_DIR = join(HERE, '..', '..', '..', '..') const SUITES: { skill: string; rules: RuleDef[] }[] = [ { skill: 'audit-actors', rules: ACTOR_RULES }, { skill: 'audit-menu', rules: MENU_RULES }, { skill: 'audit-sources', rules: SOURCES_RULES }, { skill: 'audit-sections', rules: SECTION_RULES }, { skill: 'audit-use-cases', rules: UC_RULES }, { skill: 'audit-rules', rules: BR_RULES }, { skill: 'audit-rbac', rules: RBAC_RULES }, { skill: 'audit-data-model', rules: DM_RULES }, { skill: 'audit-screens', rules: SCREEN_RULES }, { skill: 'audit-cross-dimension', rules: XD_RULES }, { skill: 'audit-cross-ref-code', rules: CODE_RULES }, ] function skillMdPath(skill: string): string { // Source tree: business-analyse// ; deployed tree: skills/ba-/. const source = join(BA_DIR, skill, 'SKILL.md') try { readFileSync(source) return source } catch { return join(BA_DIR, `ba-${skill}`, 'SKILL.md') } } function skillRuleHeadings(skill: string): { active: string[]; retired: string[] } { const md = readFileSync(skillMdPath(skill), 'utf8') const active: string[] = [] const retired: string[] = [] for (const m of md.matchAll(/^###\s+((?:UC|BR|RBAC|DM|SCR|XD|MENU|SEC|XAPP|ACT|CODE|SRC)-\d{3})\s*—\s*(.*)$/gm)) { if (/RETIRED|relocated/i.test(m[2]!)) retired.push(m[1]!) else active.push(m[1]!) } return { active, retired } } describe('audit-ba / drift registre ↔ SKILL.md', () => { for (const suite of SUITES) { it(`${suite.skill} : chaque règle du registre est documentée, chaque règle active documentée est implémentée`, () => { const { active, retired } = skillRuleHeadings(suite.skill) const registry = suite.rules.map((r) => r.id) const documented = new Set(active) const implemented = new Set(registry) const undocumented = registry.filter((id) => !documented.has(id)) const unimplemented = active.filter((id) => !implemented.has(id)) const zombie = retired.filter((id) => implemented.has(id)) expect(undocumented, `règles du registre absentes de ${suite.skill}/SKILL.md`).toEqual([]) expect(unimplemented, `règles documentées dans ${suite.skill}/SKILL.md sans implémentation`).toEqual([]) expect(zombie, `règles RETIRÉES encore implémentées`).toEqual([]) }) } it('chaque SKILL.md recâblé porte la règle de fer (jamais lire le corpus) et l’invocation du moteur', () => { for (const suite of SUITES) { const md = readFileSync(skillMdPath(suite.skill), 'utf8') expect(md, `${suite.skill} sans section moteur`).toContain('## Deterministic engine') expect(md, `${suite.skill} sans invocation audit-ba`).toContain('audit-run/cli/audit-ba/index.ts') expect(md, `${suite.skill} sans l'interdit de lecture du corpus`).toContain('never by reading the corpus') } }) it('les kinds du registre couvrent les 3 familles (mechanical/hybrid/judgment) et les 9 règles de jugement pur sont marquées', () => { const all = SUITES.flatMap((s) => s.rules) const judgmentIds = all.filter((r) => r.kind === 'judgment').map((r) => r.id).sort() expect(judgmentIds).toEqual(['BR-012', 'MENU-001', 'MENU-005', 'SEC-003', 'SEC-004', 'UC-011', 'UC-023', 'XAPP-002', 'XAPP-003']) }) })