/** * DM-026 — a state value nobody reaches any more (the leftover of a state * change). Three corpora: one with a Flow that misses a value, one where a * kanban column keeps a value alive, one that has reached neither phase yet. */ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { loadCorpus } from '../corpus/model.js' import { runRules } from '../engine.js' import { DM_RULES } from '../rules/dm.js' import type { Finding } from '../types.js' const ENTITE = [ '### ENT-001 — Ticket (agrégat racine)', '- **Préfixe table** : `tk_`', '', '| Attribut | Type | Contraintes | Calculé |', '|----------|------|-------------|---------|', '| Id | Guid | PK | — |', '| Title | string(150) | requis | — |', '| Status | enum | draft/open/closed/archived, requis | — |', '', ].join('\n') const RULES = [ '### BR-001 — Cycle de vie du ticket', '- **Type** : state-transition', '- **Sévérité** : err', '- **Flow** :', ' - draft → open (by: Agent)', ' - open → closed (by: Agent)', '', ].join('\n') const SCREEN_KANBAN = [ '### SCR-SUP-TICKETS-tickets-001 — Tableau des tickets (SmartKanban)', '- **Entité** : Ticket', '- **Champ statut** : Status', '- **Colonnes** : open (Ouvert, blue), closed (Clos, gray), archived (Archivé, gray)', '', ].join('\n') function corpus(files: Record): { root: string; findings: Finding[] } { const root = mkdtempSync(join(tmpdir(), 'audit-ba-dm-026-')) for (const [rel, content] of Object.entries(files)) { const p = join(root, rel) mkdirSync(join(p, '..'), { recursive: true }) writeFileSync(p, content, 'utf8') } const findings = runRules(DM_RULES.filter((r) => r.id === 'DM-026'), loadCorpus(root), { strict: false }).findings return { root, findings } } describe('DM-026 — leftover state values', () => { it('warns on a value no Flow transition reaches (archived), not on the reached ones', () => { const { root, findings } = corpus({ 'SUP/TICKETS/entité.md': ENTITE, 'SUP/TICKETS/règles-métier.md': RULES, }) try { const f = findings.find((x) => x.ruleId === 'DM-026')! expect(f.severity).toBe('warn') expect(f.evidence).toEqual(['Ticket.Status : archived']) } finally { rmSync(root, { recursive: true, force: true }) } }) it('a kanban column keeps a value alive even when no transition reaches it', () => { const { root, findings } = corpus({ 'SUP/TICKETS/entité.md': ENTITE, 'SUP/TICKETS/règles-métier.md': RULES, 'SUP/TICKETS/tickets/screen.md': SCREEN_KANBAN, }) try { const f = findings.find((x) => x.ruleId === 'DM-026')! expect(f.severity).toBe('ok') } finally { rmSync(root, { recursive: true, force: true }) } }) it('stays silent (inventaire PARTIEL) when neither a Flow rule nor a screen exists yet', () => { const { root, findings } = corpus({ 'SUP/TICKETS/entité.md': ENTITE }) try { const f = findings.find((x) => x.ruleId === 'DM-026')! expect(f.severity).toBe('ok') expect(f.message).toContain('PARTIEL') } finally { rmSync(root, { recursive: true, force: true }) } }) })