0019 — Dérogations explicites au workspace audit
Contexte
Section intitulée « Contexte »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.
Décision
Section intitulée « Décision »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(champsknip.ignoreDependencies,knip.ignore,private: truejustifié) ; - soit en commentaire de configuration (CSP, rate-limit, etc.).
Workspace structure
Section intitulée « Workspace structure »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 (exposecore,extractor,comparatorviaexportset un bincrf-openapi). La règleaudit:structureinterditbindanspackages/. Une migration propre demande un split enpackages/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.
- Pourquoi pas migré sous
ui/atlas-ui— marquéprivate: true(ADR 0011) mais déclaresvelteenpeerDependencies. Lepeerest respecté par anticipation d’une future publication.
Dépendances knip
Section intitulée « Dépendances knip »packages/citation— 2 dépendancesignoreDependencies(@xenova/transformers,uuid) utilisées dynamiquement (lazy imports, code généré). Knip ne les voit pas. (Les anciennes entrées@effect/experimentalet@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— fichiercommands/api/commands.tsenignoreknip. Knip ne trace pas la chaîne d’imports Effect/CLI complète ; le fichier reste testé via mock direct depuiscommands.test.ts.
Hybride lib + CLI dans packages/
Section intitulée « Hybride lib + CLI dans packages/ »packages/citation-validate— déclare@clack/promptsendependenciesdirectes alors que c’est une bibliothèque. Le modulesrc/prompt/(input.ts, transformer.ts) implémente des prompts interactifs, etsrc/actions/*.ts+src/events/*.tsutilisentlogde@clack/promptspour la sortie utilisateur.- Pourquoi pas migré vers
cli/biblio: le moduleprompt/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/biblioconsommecitation-validate. Le couplage est documenté ; aucune autre app/lib ne hérite de@clack/promptsindirectement.
- Pourquoi pas migré vers
Apps : factorisation partielle
Section intitulée « Apps : factorisation partielle »apps/ecrin— ne migre pas vers@univ-lehavre/atlas-baaspartagé parce qu’elle utiliseTablesDB(non exposé par le package partagé). À uniformiser quandTablesDBdeviendra le standard du package.apps/ecrin—validateSignupEmailreste local plutôt que re-exporté du package partagé (lookupisAllianceasync, erreurNotPartOfAllianceErrorspécifique au lieu deNotAnEmailErrordu package). Le reste des validators est re-exporté normalement.
Web / sécurité
Section intitulée « Web / sécurité »- Cookies UI
find-an-expert(theme, font, dark-mode, locale) —SameSite=LaxsansSecure. 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 lesstyle=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 viadefaultCspDirectives()dans leursvelte.config.jsetapplySecurityHeaders()dans leurhooks.server.ts. La dérogationstyle-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.
- 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
Audits dépendances
Section intitulée « Audits dépendances »audit:securityà--audit-level=moderate— le seuil n’est paslow. Tightening au cas par cas : avant chaque montée du seuil, on vérifie qu’il y a 0 alerte moderate.
Seuils de couverture de tests
Section intitulée « Seuils de couverture de tests »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 (aucunvitest.config.ts, aucune dépendance vitest, aucun scripttest).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 depuisapps/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) :
| Paquet | Seuils actuels (S/B/F/L) | Cible Phase suivante | Raison de l’exemption temporaire |
|---|---|---|---|
apps/amarre | 50/48/32/53 | Phase ultérieure | Phase 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/ecrin | 52/32/37/53 | Phase ultérieure | Phase 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-expert | 22/12/15/25 | Phase ultérieure | Seuils 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-sveltekit | 80/95/35/80 | Stable | Helper 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 utilisantassertNoXss).
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.tsetsrc/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 ») derunDiagnostics(spinner@clack/prompts) est désormais testé ; le bloc d’orchestrationcommand/mainest marqué/* v8 ignore */(assemblage@effect/cli) et le binsrc/bin/atlas-net.tsexclu ; la branche!process.stdout.isTTYdedetectCi(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
Section intitulée « Rate-limit »- Rate-limit absent sur
/auth/login(secret magic URL haute entropie, pas de credentials énumérables) et/health(lightweight, idempotent). Rate-limit ailleurs :in-memorymono-instance — à migrer vers Redis/Upstash si scale-out (item sine die, voir ADR 0001).
Accepted.
Conséquences
Section intitulée « Conséquences »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.