/** * CODE-001 at the APPLICATION grain (doctrine 2026-10-03). The folder * `Entities///` is the act of ownership: * own module → ok (the entity's own generated class) * own application, other mod → err (one declaring module per application) * other application → warn (reuse ; extra attributes = extension request) * no owner (flat layout) → warn (indeterminable) * Before this, CODE-001 matched by bare name over every Domain class and warned * on a module's OWN classes after its first /ba-develop run — forever (client * NOTES/MEMOS). Multi-word codes are pinned: `GESTION_FLOTTE` (BA folder) must * match `GestionFlotte` (generated segment) — a plain fold keeps the `_` and * every multi-word application would see its own classes as another's. */ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { loadCorpus } from '../corpus/model.js' import { runRules } from '../engine.js' import { CODE_RULES } from '../rules/code.js' import { resetDomainScanCache } from '../../../../../lib/domain-scan.js' import type { Finding } from '../types.js' const entityDoc = (code: string, name: string, attrs: string[]): string => [ `### ${code} — ${name} (agrégat racine)`, `- **Préfixe table** : \`${name.slice(0, 3).toLowerCase()}_\``, '', '| Attribut | Type | Contraintes | Calculé |', '|---|---|---|---|', '| Id | Guid | PK | — |', ...attrs.map((a) => `| ${a} | string(50) | requis | — |`), '', ].join('\n') const csClass = (name: string, props: string[]): string => [ 'namespace Demo.Domain.Entities;', '', `public class ${name} : ExtensionBaseEntity`, '{', ...props.map((p) => ` public string ${p} { get; set; } = string.Empty;`), '}', '', ].join('\n') let dir: string let byModule: Map beforeAll(() => { dir = mkdtempSync(join(tmpdir(), 'ss-code-001-')) const w = (rel: string, content: string): void => { const p = join(dir, rel) mkdirSync(join(p, '..'), { recursive: true }) writeFileSync(p, content, 'utf8') } // BA corpus — two applications, a multi-word one. w('ba/GESTION_FLOTTE/index.md', '# GESTION_FLOTTE\n') w('ba/GESTION_FLOTTE/VEHICULES/index.md', '# VEHICULES\n') w('ba/GESTION_FLOTTE/VEHICULES/entité.md', entityDoc('ENT-001', 'Vehicule', ['Plaque'])) w('ba/NOTES/index.md', '# NOTES\n') w('ba/NOTES/MEMOS/index.md', '# MEMOS\n') w( 'ba/NOTES/MEMOS/entité.md', [ entityDoc('ENT-001', 'Memo', ['Titre']), entityDoc('ENT-002', 'Contact', ['Nom']), entityDoc('ENT-003', 'Vehicule', ['Plaque', 'Immatriculation']), entityDoc('ENT-004', 'Legacy', ['Label']), ].join('\n'), ) // Generated Domain — the folder is the owner. w('src/Demo.Domain/Entities/GestionFlotte/Vehicules/Vehicule.cs', csClass('Vehicule', ['Plaque'])) w('src/Demo.Domain/Entities/Notes/Memos/Memo.cs', csClass('Memo', ['Titre'])) w('src/Demo.Domain/Entities/Notes/Agenda/Contact.cs', csClass('Contact', ['Nom'])) w('src/Demo.Domain/Entities/Legacy.cs', csClass('Legacy', ['Label'])) resetDomainScanCache() const model = loadCorpus(join(dir, 'ba')) const out = runRules(CODE_RULES, model, { strict: false, projectRoot: dir }) byModule = new Map() for (const f of out.findings.filter((x) => x.ruleId === 'CODE-001')) { const key = `${f.scope.app}/${f.scope.module}` byModule.set(key, [...(byModule.get(key) ?? []), f]) } }) afterAll(() => rmSync(dir, { recursive: true, force: true })) describe('CODE-001 — ownership at the application grain', () => { it('a module re-audited after its own /ba-develop sees its OWN classes: ok, said as such (multi-word app GESTION_FLOTTE ≡ GestionFlotte)', () => { const f = byModule.get('GESTION_FLOTTE/VEHICULES')! expect(f).toHaveLength(1) expect(f[0]!.severity).toBe('ok') expect(f[0]!.message).toContain('application elle-même') expect(f[0]!.evidence?.[0]).toContain('Entities/GestionFlotte/Vehicules/Vehicule.cs') }) it('same application, another module → err (one declaring module per application)', () => { const f = byModule.get('NOTES/MEMOS')!.find((x) => x.severity === 'err')! expect(f).toBeDefined() expect(f.message).toContain('Doublon intra-application') expect(f.evidence).toEqual(['Contact — src/Demo.Domain/Entities/Notes/Agenda/Contact.cs (module Agenda de la même application)']) }) it('another application → warn, naming the owner and the attributes declared IN ADDITION (the extension request)', () => { const warns = byModule.get('NOTES/MEMOS')!.filter((x) => x.severity === 'warn') const foreign = warns.find((x) => x.message.includes('AUTRE application'))! expect(foreign).toBeDefined() expect(foreign.evidence).toHaveLength(1) expect(foreign.evidence![0]).toContain('Vehicule — src/Demo.Domain/Entities/GestionFlotte/Vehicules/Vehicule.cs (application GestionFlotte/Vehicules)') expect(foreign.evidence![0]).toContain('attributs en plus déclarés ici : Immatriculation') expect(foreign.evidence![0]).not.toContain('Plaque') expect(foreign.message).toContain('demande-extension.md') }) it('a class outside Entities/// (flat legacy layout) → warn, indeterminable', () => { const warns = byModule.get('NOTES/MEMOS')!.filter((x) => x.severity === 'warn') const flat = warns.find((x) => x.message.includes('indéterminable'))! expect(flat).toBeDefined() expect(flat.evidence).toEqual(['Legacy — src/Demo.Domain/Entities/Legacy.cs']) }) it('the module\'s own class (Memo) never appears in any err/warn evidence', () => { const texts = byModule.get('NOTES/MEMOS')!.flatMap((x) => x.evidence ?? []) expect(texts.some((t) => t.startsWith('Memo —'))).toBe(false) expect(byModule.get('NOTES/MEMOS')!.some((x) => x.severity === 'ok')).toBe(false) }) })