/** * CODE-009 — the ACCESS request (data sensitivity doctrine 2026-10-03). A * module reaching a ≥ confidentiel entity of ANOTHER application (cross-module * FK, 360 tab, 4-segment permission): err until `/demande-extension.md` * carries a block of nature « accès » for the pair; warn while it is * pending/refused (development frozen); ok once validated. CODE-008 never * reports the access nature (one GELÉ per nature). BA leg only — no code scan. */ 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 type { Finding } from '../types.js' const RULES = CODE_RULES.filter((r) => r.id === 'CODE-008' || r.id === 'CODE-009') const entity = (code: string, name: string, bullets: string[], attrs: string[], tail: string[] = []): string => [`### ${code} — ${name} (agrégat racine)`, '- **Description** : x.', '- **Périmètre** : y.', ...bullets, '', '| Attribut | Type | Contraintes | Calculé |', '|---|---|---|---|', '| Id | Guid | PK | — |', ...attrs.map((a) => `| ${a} | string(50) | requis | — |`), '', ...tail, ''].join('\n') const idx = (t: string): string => `# ${t}\n\n## Contexte\n${t}.\n\n## Hors-périmètre\n_Aucune._\n` let dir: string const w = (rel: string, content: string): void => { const p = join(dir, rel) mkdirSync(join(p, '..'), { recursive: true }) writeFileSync(p, content, 'utf8') } const run = (scope: { app?: string; module?: string } = {}): Finding[] => runRules(RULES, loadCorpus(dir, scope), { strict: false }).findings.filter((f) => f.scope.module === 'PLANNING') const of = (fs: Finding[], id: string): Finding[] => fs.filter((f) => f.ruleId === id) beforeAll(() => { dir = mkdtempSync(join(tmpdir(), 'ss-code-009-')) // Owner application RH: FichePaie (confidentiel) in PAIE, section « fiches » with a list screen; Tarif (restreint). w('RH/index.md', idx('RH')) w('RH/PAIE/index.md', idx('Paie')) w('RH/PAIE/entité.md', [entity('ENT-001', 'FichePaie', ['- **Sensibilité** : confidentiel — financier'], ['Montant']), entity('ENT-002', 'Tarif', ['- **Sensibilité** : restreint — commercial'], ['Prix'])].join('\n')) w('RH/PAIE/fiches/index.md', idx('Fiches')) w('RH/PAIE/fiches/screen.md', ['### SCR-RH-PAIE-fiches-001 — Fiches de paie (SmartListView)', '- **Entité** : FichePaie (ENT-001)', '- **Permission** : paie.fiches.read', ''].join('\n')) w('RH/PAIE/tarifs/index.md', idx('Tarifs')) // Requesting application NOTES / PLANNING: FK onto FichePaie, a 360 tab onto Tarif (app token), a 4-segment row onto fiches. w('NOTES/index.md', idx('NOTES')) w('NOTES/PLANNING/index.md', idx('Planning')) w( 'NOTES/PLANNING/entité.md', entity('ENT-001', 'Affectation', ['- **Traçabilité** : UC-NOTES-PLANNING-001'], ['Libelle'], [ '- **Relations** : Affectation *→1 FichePaie — FK FichePaieId, scope cross-module (RH/PAIE), onDelete restrict.', ]), ) w('NOTES/PLANNING/affectations/index.md', idx('Affectations')) w( 'NOTES/PLANNING/affectations/screen.md', [ '### SCR-NOTES-PLANNING-DETAIL-001 — Affectation (SmartForm)', '- **Entité** : Affectation (ENT-001)', '- **Mode** : detail', '- **Onglet lié « Tarifs »** : entité Tarif, FK affectationId, app RH, affichage table → SCR-RH-PAIE-TARIFS-LIST-001 (`paie.tarifs.read`)', '', ].join('\n'), ) w('NOTES/PLANNING/rbac.md', ['# RBAC', '', '| Acteur | Permission | Portée |', '|---|---|---|', '| BA-001-AC-001 (Gestionnaire) | `rh.paie.fiches.read` | toutes |', ''].join('\n')) }) afterAll(() => rmSync(dir, { recursive: true, force: true })) describe('CODE-009 — access request', () => { it('no doc → err naming each (sensitive entity, owner) pair with its level and the facts that reach it; CODE-008 asks nothing (no column)', () => { const f = run() const c9 = of(f, 'CODE-009') expect(c9).toHaveLength(1) expect(c9[0]!.severity).toBe('err') expect(c9[0]!.file).toBe('NOTES/demande-extension.md') expect(c9[0]!.relatedTo).toBe('DM-034') expect(c9[0]!.evidence).toEqual([ 'ENT-001 FichePaie → RH/PAIE (confidentiel — financier) : accès en lecture via FK Affectation.FichePaieId → FichePaie — Traçabilité : UC-NOTES-PLANNING-001 ; accès via la permission `rh.paie.fiches.read` (BA-001-AC-001) — AUCUN bloc REQ (nature accès) ne la porte', 'ENT-002 Tarif → RH/PAIE (restreint — commercial) : accès via l’onglet lié « Tarifs » de SCR-NOTES-PLANNING-DETAIL-001 — AUCUN bloc REQ (nature accès) ne la porte', ]) // CODE-008 asks nothing (no column) — ok, its BA leg alone (projectRoot absent is said, never an err). expect(of(f, 'CODE-008').map((x) => [x.severity, x.message.includes('jambe code non scannée')])).toEqual([['ok', true]]) }) it('renders the SAME verdict under a module-scoped run (the owner app is read from disk)', () => { const scoped = of(run({ app: 'NOTES', module: 'PLANNING' }), 'CODE-009')[0]! const full = of(run(), 'CODE-009')[0]! expect({ sev: scoped.severity, ev: scoped.evidence }).toEqual({ sev: full.severity, ev: full.evidence }) }) it('pending access blocks → warn GELÉ on CODE-009 only; CODE-008 stays silent about them', () => { w( 'NOTES/demande-extension.md', [ '', "# Demandes d'extension — NOTES", '', '### REQ-001 — FichePaie → RH/PAIE', '- **Nature** : accès', '- **Statut** : en attente', '- **Demandeurs** : PLANNING', '- **Sensibilité** : confidentiel — financier', '- **Classe** : non développée — entité.md RH/PAIE', '- **Motif** : accès en lecture via FK Affectation.FichePaieId → FichePaie', '- **Date** : 2026-10-03', '', '### REQ-002 — Tarif → RH/PAIE', '- **Nature** : accès', '- **Statut** : validée — Studio 2026-10-04', '- **Demandeurs** : PLANNING', '- **Sensibilité** : restreint — commercial', '- **Classe** : non développée — entité.md RH/PAIE', '- **Motif** : accès via l’onglet lié « Tarifs »', '- **Date** : 2026-10-03', '', ].join('\n'), ) const f = run() const c9 = of(f, 'CODE-009') expect(c9.map((x) => x.severity)).toEqual(['warn']) expect(c9[0]!.message).toContain('GELÉ') expect(c9[0]!.evidence).toEqual(['REQ-001 FichePaie → RH/PAIE (en attente — confidentiel — financier) — demandeurs : PLANNING']) expect(of(f, 'CODE-008').some((x) => x.message.includes('GELÉ'))).toBe(false) }) it('every access validated → ok with the count', () => { w( 'NOTES/demande-extension.md', [ '', "# Demandes d'extension — NOTES", '', '### REQ-001 — FichePaie → RH/PAIE', '- **Nature** : accès', '- **Statut** : validée', '- **Demandeurs** : PLANNING', '- **Classe** : non développée — entité.md RH/PAIE', '- **Motif** : x', '- **Date** : 2026-10-03', '', '### REQ-002 — Tarif → RH/PAIE', '- **Nature** : accès', '- **Statut** : validée', '- **Demandeurs** : PLANNING', '- **Classe** : non développée — entité.md RH/PAIE', '- **Motif** : x', '- **Date** : 2026-10-03', '', ].join('\n'), ) const c9 = of(run(), 'CODE-009') expect(c9.map((x) => [x.severity, x.message])).toEqual([['ok', '2 demande(s) d’accès couverte(s) (2 validée(s)).']]) }) })