/** * BR-009 — the upstream gate that was fail-OPEN on the shape its own pipeline * writes. * * BR-009 is declared `dedupOf: 'UC-003'`: same question ("does this UC carry a * main flow?"), asked one phase later so the rules author is not handed * title-only use cases. But the two rules did not share a predicate: * * - UC-003 filtered placeholders first (`'' | '—' | (aucun)`), * - BR-009 tested `mainFlow.length === 0`. * * `create-use-case/levels/discovery.md` prescribes writing the Cockburn fields * "left as `—`", and `lib/ba-use-cases` keeps that value (the field bullet has a * truthy value). So a discovery-level UC arrives as `mainFlow === ['—']`, * length 1 — and BR-009 waved through a module of title-only UCs while UC-003, * its own primary, flagged every one of them. * * Both rules now share `lib/ba-use-cases.undetailedUseCases`. */ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterAll, describe, expect, it } from 'vitest' import { loadCorpus } from '../corpus/model.js' import { runRules } from '../engine.js' import { BR_RULES } from '../rules/br.js' import { UC_RULES } from '../rules/uc.js' import type { Finding } from '../types.js' const BR009 = BR_RULES.filter((r) => r.id === 'BR-009') const UC003 = UC_RULES.filter((r) => r.id === 'UC-003') const tmpRoots: string[] = [] /** The three shapes a `Flux principal` can take on disk. */ type FlowShape = 'discovery-dash' | 'aucun' | 'detailed' const FLOW_LINES: Record = { // Exactly what levels/discovery.md tells the model to write. 'discovery-dash': ['- **Flux principal** : —'], 'aucun': ['- **Flux principal** : (aucun)'], 'detailed': [ '- **Flux principal** :', ' 1. Le gestionnaire ouvre la fiche du véhicule.', ' 2. Le système affiche les interventions en cours.', ' 3. Le gestionnaire enregistre le relevé kilométrique.', ], } function writeCorpus(shape: FlowShape): string { const root = mkdtempSync(join(tmpdir(), 'ss-br009-')) tmpRoots.push(root) const w = (rel: string, content: string): void => { const p = join(root, rel) mkdirSync(join(p, '..'), { recursive: true }) writeFileSync(p, content, 'utf8') } const idx = (title: string): string => `# ${title}\n\n## Contexte\n${title}.\n\n## Hors-périmètre\n_Aucune exclusion connue à ce stade._\n` w('LOG/index.md', idx('Logistique')) w('LOG/acteur.md', '# Acteurs — LOG\n\n### BA-001-AC-001 — Gestionnaire\n- **Type** : internal\n') w('LOG/PARC/index.md', idx('Parc')) w('LOG/PARC/vehicules/index.md', idx('Véhicules')) w( 'LOG/PARC/vehicules/use-case.md', [ '# Cas d’usage — Véhicules', '', '### UC-LOG-PARC-vehicules-001 — Suivre l’entretien d’un véhicule', '- **Niveau** : user-goal', '- **Acteur principal** : BA-001-AC-001 (Gestionnaire)', '- **Acteurs secondaires** : —', '- **Préconditions** : Le véhicule est immatriculé au parc.', ...FLOW_LINES[shape], '- **Postconditions** : Le relevé est enregistré.', '', ].join('\n'), ) return root } function severityOf(rule: typeof BR009, root: string): Finding { const { findings } = runRules(rule, loadCorpus(root, {}), { strict: false }) const hit = findings.find((f) => f.scope.module === 'PARC') expect(hit, 'no finding for LOG/PARC').toBeDefined() return hit! } afterAll(() => { for (const r of tmpRoots) rmSync(r, { recursive: true, force: true }) }) describe('BR-009 — undetailed use cases block the rules phase', () => { it('ERRS on the discovery shape `Flux principal : —` (the regression)', () => { const f = severityOf(BR009, writeCorpus('discovery-dash')) expect(f.severity).toBe('err') expect(f.evidence).toEqual(['UC-LOG-PARC-vehicules-001']) }) it('errs on the `(aucun)` placeholder too', () => { expect(severityOf(BR009, writeCorpus('aucun')).severity).toBe('err') }) it('is ok once the main flow carries real steps', () => { expect(severityOf(BR009, writeCorpus('detailed')).severity).toBe('ok') }) }) describe('BR-009 mirrors UC-003 — same predicate, or the mirror lies', () => { it.each(['discovery-dash', 'aucun', 'detailed'])( 'agrees with its dedupOf primary on %s', (shape) => { const root = writeCorpus(shape) const br = severityOf(BR009, root) const uc = severityOf(UC003, root) expect(br.severity).toBe(uc.severity) expect(br.evidence ?? []).toEqual(uc.evidence ?? []) // Only the err branch carries the mirror link (the ok branch has nothing // to dedup) — assert it exactly where it is meant to be. expect(br.dedupOf).toBe(br.severity === 'err' ? 'UC-003' : undefined) }, ) })