Aller au contenu

0019 — Dérogations explicites au workspace audit

Les règles transverses du monorepo — structure des catégories (ADR 0002), CLIs thins (ADR 0008), hygiène des dépendances (knip), durcissement sécurité (CSP, rate-limit) — sont vérifiées en CI et en pre-push. Quelques paquets ou pratiques ont des raisons légitimes de ne pas suivre ces règles.

Sans liste explicite, deux dérives apparaissent : les exceptions s’accumulent silencieusement (chacun découvre la sienne et la code en dur), ou les règles deviennent floues à force d’être contournées. La discipline est de lister chaque dérogation avec sa raison, pour que l’écart soit visible et révisable.

Note sur les « Phase X.Y ». Plusieurs entrées ci-dessous renvoient à une « Phase X.Y » : il s’agit des étapes du plan de résorption 2026-05-30, le chantier de nettoyage technique et documentaire conduit de mai à juin 2026. Ces mentions sont des repères historiques (quand et pourquoi une dérogation a été introduite ou révisée) ; le plan reste la référence pour leur signification.

Les dérogations actives sont listées ci-dessous, regroupées par règle. Toute dérogation doit être enregistrée :

  • soit dans le script audit concerné (scripts/audit/workspace-structure.mjs) avec un commentaire « pourquoi » ;
  • soit dans package.json (champs knip.ignoreDependencies, knip.ignore, private: true justifié) ;
  • soit en commentaire de configuration (CSP, rate-limit, etc.).
  • cli/crf-openapi — nom de paquet sans suffixe -cli (historique antérieur à la règle). Exception listée dans le script audit.
    • Pourquoi pas migré sous packages/ : le paquet est hybride lib + bin (expose core, extractor, comparator via exports et un bin crf-openapi). La règle audit:structure interdit bin dans packages/. Une migration propre demande un split en packages/crf-openapi (lib pure) + cli/crf-openapi (thin bin), chantier qui dépasse Phase 7 du plan de résorption 2026-05-30. Report explicite, à reprendre en Phase ultérieure.
  • ui/atlas-ui — marqué private: true (ADR 0011) mais déclare svelte en peerDependencies. Le peer est respecté par anticipation d’une future publication.
  • packages/citation — 2 dépendances ignoreDependencies (@xenova/transformers, uuid) utilisées dynamiquement (lazy imports, code généré). Knip ne les voit pas. (Les anciennes entrées @effect/experimental et @effect/platform-node étaient des phantoms — déclarées sans être importées — retirées par l’écart E4 du socle Effect ; voir ADR 0050 pour le mécanisme de l’angle mort knip qui les avait masquées.)
  • cli/crf — fichier commands/api/commands.ts en ignore knip. Knip ne trace pas la chaîne d’imports Effect/CLI complète ; le fichier reste testé via mock direct depuis commands.test.ts.
  • packages/citation-validate — déclare @clack/prompts en dependencies directes alors que c’est une bibliothèque. Le module src/prompt/ (input.ts, transformer.ts) implémente des prompts interactifs, et src/actions/*.ts + src/events/*.ts utilisent log de @clack/prompts pour la sortie utilisateur.
    • Pourquoi pas migré vers cli/biblio : le module prompt/ est profondément couplé à actions/, events/, context/, store/ (architecture Effect/Layer). Le déplacement propre demande d’extraire un logger injectable et de découpler les prompts du métier, refactor qui dépasse Phase 7 du plan de résorption 2026-05-30. Report explicite, à reprendre en Phase ultérieure (probablement avec un agent spécialisé Effect).
    • Aujourd’hui : seul cli/biblio consomme citation-validate. Le couplage est documenté ; aucune autre app/lib ne hérite de @clack/prompts indirectement.
  • apps/ecrin — ne migre pas vers @univ-lehavre/atlas-baas partagé parce qu’elle utilise TablesDB (non exposé par le package partagé). À uniformiser quand TablesDB deviendra le standard du package.
  • apps/ecrinvalidateSignupEmail reste local plutôt que re-exporté du package partagé (lookup isAlliance async, erreur NotPartOfAllianceError spécifique au lieu de NotAnEmailError du package). Le reste des validators est re-exporté normalement.
  • Cookies UI find-an-expert (theme, font, dark-mode, locale) — SameSite=Lax sans Secure. Cookies non sensibles, lus côté client par design (rendu de la home page hors-ligne, sans session).
  • CSP style-src 'unsafe-inline' — conservé pour les style= inline générés par Svelte et Bootstrap (voir ADR 0006). Le retrait est tracé sous Phase 5.3-tightening (sine die, voir ADR 0001).
    • Depuis Phase 9.2 (2026-05-31), la liste des directives CSP par défaut et les cinq security headers statiques (HSTS, X-Content-Type- Options, Referrer-Policy, Permissions-Policy, X-Frame-Options) sont factorisés dans packages/sveltekit-csp (@univ-lehavre/atlas-sveltekit-csp). Toutes les apps SvelteKit (amarre, ecrin, find-an-expert, sillage, atlas-dashboard, crf-dashboard) consomment ce helper via defaultCspDirectives() dans leur svelte.config.js et applySecurityHeaders() dans leur hooks.server.ts. La dérogation style-src 'unsafe-inline' reste explicite — elle est commentée à l’emplacement où elle est définie (packages/sveltekit-csp/src/csp.ts) et pointe vers le présent ADR.
  • audit:security à --audit-level=moderate — le seuil n’est pas low. Tightening au cas par cas : avant chaque montée du seuil, on vérifie qu’il y a 0 alerte moderate.

Cible générale : pnpm coverage:report 80 exécuté en CI et pre-push exige qu’un paquet publié ou déployé atteigne 80% sur les quatre métriques (statements/branches/functions/lines ; voir scripts/audit/coverage-report.mjs).

Comment les dérogations sont respectées par le garde-fou (pour qu’un seuil global à 80 ne bloque pas les paquets légitimement en dessous) : le script lit le seuil déclaré par chaque paquet dans son vitest.config.ts (ou vite.config.ts pour les apps SvelteKit) et applique le seuil effectif suivant — un paquet n’est en échec que s’il tombe sous CE seuil :

  • seuil déclaré sous la cible globale → jugé sur son seuil (dérogation active ci-dessous) ; reste un garde-fou anti-régression (descendre sous son propre seuil échoue) ;
  • aucun seuil déclaré et paquet private (non publié, ADR 0011) → exempté du plancher (se renforce au fil des migrations, ex. ui/atlas-ui) ;
  • aucun seuil déclaré mais paquet publié → tenu à la cible globale (force à déclarer un seuil) ;
  • seuil déclaré au-dessus de la cible → pas d’auto-exemption, la cible prime.

Les paquets ci-dessous sont explicitement exemptés ou autorisés à déclarer un seuil inférieur, avec la raison.

Exemptés par nature (aucun code exécutable à couvrir, ou code expérimental non publié) :

  • assets/logos — paquet d’assets statiques (SVG/PNG) sans logique. Vitest entièrement retiré en Phase 2.5 (aucun vitest.config.ts, aucune dépendance vitest, aucun script test).
  • apps/atlas-dashboard, apps/crf-dashboard — dashboards internes, private: true (ADR 0011), pas déployés, contenu visualisation pure.
  • ui/atlas-ui — bibliothèque de composants Svelte 5 partagée, private: true (ADR 0011), non publiée. Depuis Phase 10.2/10.3 elle a une infra de tests level-1 (vitest + happy-dom + @testing-library/svelte) et des tests a11y axe-core (vitest-axe), mais la couverture globale reste basse : seuls les composants migrés depuis apps/amarre (TopNavbar, Signup, CreateRequest) sont couverts (≈77–95% en propre) ; la majorité des composants (home pages, carousels, tiles) restent à couvrir au fil des migrations level-1 des apps consommatrices. Pas de seuil global imposé tant que le paquet n’est pas publié.
  • sandbox/crf-sandbox — banc d’essai par construction, hors périmètre tests.

Temporairement sous-testés (renforcement planifié, voir plan de résorption 2026-05-30) :

PaquetSeuils actuels (S/B/F/L)Cible Phase suivanteRaison de l’exemption temporaire
apps/amarre50/48/32/53Phase ultérieurePhase 9.1 (réel 52.36/56/34.37/55.55, migration atlas-sveltekit-handler) puis Phase 13.3 : branches 54 → 48 car l’init Sentry opt-in (if (dsn) dans hooks.server/client) ajoute des branches non couvertes en unit. UI Svelte et services métier restent à couvrir.
apps/ecrin52/32/37/53Phase ultérieurePhase 4.3 (réel 54.18/36.56/39.81/55.78) puis Phase 13.3 : branches 34 → 32 car l’init Sentry opt-in (if (dsn) dans hooks.server/client) ajoute des branches non couvertes en unit. 14 endpoints API couverts ; UI Svelte et services à couvrir.
apps/find-an-expert22/12/15/25Phase ultérieureSeuils resserrés en Phase 4.4 (réel 24.80/14.58/17.89/27.38). 17 endpoints API couverts ; routes Svelte et content dominent encore le dénominateur.
packages/test-utils-sveltekit80/95/35/80StableHelper paquet créé en Phase 4.2. functions à 35 parce que noopCookies.{get,set,…} (stubs requis par le type RequestEvent['cookies']) ne sont jamais appelés.

Renforcés en Phase 3 — historique : la Phase 3 du plan de résorption a fait passer 6 paquets de 0–17% à 93–100% statements ; ils sortent donc de ce tableau et passent à la cible générale 80% :

  • services/crf : 17.54% → 93.56% (5 fichiers test routes + middleware ajoutés).
  • packages/atlas-stats : 6.72% → 95.96% (4 fichiers test cache/cli/github/npm).
  • cli/biblio : 0% → 100% (commands/index intégralement couvert).
  • cli/citation : 0% → 98.13% (config/prompts/commands testés).
  • cli/atlas-stats : 0% → 94.73% (config/output/commands testés).
  • cli/crf-stats : 0% → 94.90% (config/output/commands testés).
  • cli/researcher-profiles : 0% → 94.49% (9 fichiers test sur 10 modules).

Renforcés en Phase 4 — historique : la Phase 4 a couvert tous les endpoints SvelteKit des 3 apps déployées avec un trio 200/401/payload-malformé. Les seuils des 3 apps remontent en conséquence :

  • apps/amarre : 50.92% → 54.27% (9/9 endpoints couverts ; 4 nouveaux fichiers test + 3 complétés).
  • apps/ecrin : 40.14% → 54.18% (14/14 endpoints couverts ; 10 nouveaux fichiers test).
  • apps/find-an-expert : 19.34% → 24.80% (17/17 endpoints couverts ; 14 nouveaux fichiers test, dont 8 utilisant assertNoXss).

Renforcés par exclusion des bin entry points : les CLI @effect/cli ont un point d’entrée fait d’orchestration pure (Command.make + Command.run + serve()/flux interactif @clack/prompts), non instrumentable en test unitaire — seul un e2e via process.argv le couvrirait. La logique métier, elle, est extraite dans des modules dédiés (déjà couverts). On exclut donc ces entry points du dénominateur de couverture (même posture que services/crf qui exclut src/server/index.ts), ce qui ramène les deux paquets au-dessus de 80% sur les quatre métriques sans test artificiel :

  • cli/crf : 64.59% → 90.21% statements. Exclus : src/commands/api/index.ts et src/commands/server/index.ts (déclaration @effect/cli + serve()). Le code testable (commands.ts) était déjà couvert. Seuils relevés à 88/80/95/88.
  • cli/net : 50.48% → 100% statements. Le mode interactif (« human ») de runDiagnostics (spinner @clack/prompts) est désormais testé ; le bloc d’orchestration command/main est marqué /* v8 ignore */ (assemblage @effect/cli) et le bin src/bin/atlas-net.ts exclu ; la branche !process.stdout.isTTY de detectCi (tautologie sans TTY sous vitest) est ignorée. Seuils relevés à 95/85/95/95.

Toute exemption supplémentaire doit être ajoutée à ce tableau dans la PR qui l’introduit. Tout seuil temporairement abaissé doit pointer la phase qui le rétablira.

  • Rate-limit absent sur /auth/login (secret magic URL haute entropie, pas de credentials énumérables) et /health (lightweight, idempotent). Rate-limit ailleurs : in-memory mono-instance — à migrer vers Redis/Upstash si scale-out (item sine die, voir ADR 0001).

Accepted.

Bénéfices. Chaque écart à une règle générale a un nom, une raison et un endroit où il vit. La revue est facile : on relit cet ADR pour voir si une exception est encore justifiée. Une dérogation « oubliée » sans raison se détecte rapidement.

Prix à payer. Cet ADR doit être tenu à jour à chaque dérogation ajoutée ou retirée. Quand le script audit évolue, il faut vérifier que la liste reste cohérente. Le risque qu’une exception « temporaire » devienne permanente est réel.

Garde-fous.

  • Toute nouvelle dérogation doit être ajoutée à cet ADR dans la PR qui l’introduit.
  • L’audit semestriel passe la liste en revue et challenge chaque entrée (« est-ce encore justifié ? »).
  • Si une dérogation devient majoritaire (la règle est en réalité l’exception), c’est la règle qu’il faut changer — par un ADR de remplacement.