/** * DM-028 — the entity says its tenancy. The rule VIBE-002 / attributes.md * always demanded with no grammar to satisfy it; the scaffolder's `strict` * default was deciding the tenant-composite shape of every unique index. */ 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 entity = (code: string, name: string, bullets: string[] = []): string => [ `### ${code} — ${name} (agrégat racine)`, '- **Préfixe table** : `t_`', ...bullets, '', '| Attribut | Type | Contraintes | Calculé |', '|---|---|---|---|', '| Id | Guid | PK | — |', '| Name | string(100) | requis | — |', '', ].join('\n') function run(doc: string, strict = false): { root: string; f: Finding[] } { const root = mkdtempSync(join(tmpdir(), 'audit-ba-dm-028-')) const p = join(root, 'APP', 'MOD', 'entité.md') mkdirSync(join(p, '..'), { recursive: true }) writeFileSync(p, doc, 'utf8') const f = runRules(DM_RULES.filter((r) => r.id === 'DM-028'), loadCorpus(root), { strict }).findings return { root, f } } describe('DM-028 — Portée (tenancy) declared', () => { it('no bullet anywhere → ONE warn per module, listing the entities, naming the inherited default', () => { const { root, f } = run(entity('ENT-001', 'Order') + entity('ENT-002', 'Client')) try { expect(f).toHaveLength(1) expect(f[0]!.severity).toBe('warn') expect(f[0]!.message).toContain('retombe sur strict') expect(f[0]!.evidence).toEqual(['Order', 'Client']) } finally { rmSync(root, { recursive: true, force: true }) } }) it('--strict turns the missing bullet into an err (the BR-011 ramp)', () => { const { root, f } = run(entity('ENT-001', 'Order'), true) try { expect(f[0]!.severity).toBe('err') } finally { rmSync(root, { recursive: true, force: true }) } }) it('a document-level bullet before the first heading covers every entity', () => { const { root, f } = run('# Modèle\n\n- **Portée** : none — catalogue plateforme\n\n' + entity('ENT-001', 'Country') + entity('ENT-002', 'Language')) try { expect(f[0]!.severity).toBe('ok') } finally { rmSync(root, { recursive: true, force: true }) } }) it('an entity bullet overrides the document one; an unreadable value is an err near-miss', () => { const { root, f } = run( '- **Portée** : strict\n\n' + entity('ENT-001', 'Order', ['- **Portée** : optional — modèles partagés']) + entity('ENT-002', 'Weird', ['- **Portée** : partagé']), ) try { const err = f.find((x) => x.severity === 'err')! expect(err).toBeDefined() expect(err.evidence![0]).toContain('Weird') expect(err.evidence![0]).toContain('partagé') // Order resolved (optional) → it is NOT in any finding. expect(f.some((x) => x.evidence?.some((e) => e.startsWith('Order')))).toBe(false) } finally { rmSync(root, { recursive: true, force: true }) } }) })