0051 — Rétrospective du chantier socle Effect (E1–E14)
Contexte
Section intitulée « Contexte »Ce document trace le chantier de résorption du socle Effect mené sur la
branche effect/socle (une seule grosse PR), du
plan du 2026-06-04 à sa réalisation. Il
n’introduit pas de décision nouvelle : c’est un ADR rétrospectif qui acte
ce qui a été fait, les écueils rencontrés et les choix de mise en œuvre
là où la réalité a divergé du plan — pour que la prochaine campagne parte du
réel, pas de l’intention.
Le plan découpait les 14 écarts de l’audit Effect en 6 phases (0 à 5). Les décisions structurantes ont été actées en ADR de cadrage AVANT le code qu’elles gouvernent (0045 runtime central, 0046 frontière SvelteKit, 0047 Schema vs zod, 0048 modèle d’erreur HTTP, 0049 convention de test, 0050 limite knip/peer-deps).
Ce qui a été fait
Section intitulée « Ce qui a été fait »Synthèse phase par phase (référence de commit entre crochets).
- Phase 0 — Hygiène & faux verts (E1, E2, E4). Faux-vert
csv.test.tscorrigé et vérifié par mutation [252427c1] ; patternsRECORD_IDalignés et record-id mort retiré [279d93f6] ; phantom-deps@effect/*(cluster/rpc/sql) retirées [bf1c78bb]. - Phase 1 — Décisions de cadrage (E3). Les six ADR 0045–0050 [3621779b].
- Phase 2 — Frontière étanche (E6, E5). Adaptateur
sveltekit-handlerEffect→SvelteKit préservant les erreurs typées [e3672720] ; knip durci contre les phantoms masqués par peerDependency [fc157173]. - Phase 3 — Runtime & injection (E10, E11, E7, E8). Socle d’exécution partagé (runtime central + logger) [0a425ad8] ; service CRF exécuté sur runtime central, client injecté [7a77274f] ; CLIs unifiées sur un runner unique [e752a31e] ; serveur find-an-expert sur runtime Effect central [000cd6bd].
- Phase 4 — Observabilité (E9). Pont OpenTelemetry↔Effect, tracing métier du client REDCap [6c4ac35e].
- Phase 5 — Validation & tests unifiés (E12, E13, E14). Schema-as-brand
dans
crf-core[0545fbf1] ; décodage des réponses externes via Schema [c2955369] ; packagetest-utils-effect+ garde-fou lint anti-faux-vert [d0f68cdc] ; serviceFetchOnePageinjecté [92d8ecb3] ; migrationit.effectdes tests Effect restants [49875d8e].
Les 14 écarts sont traités. Aucun bypass de hook git n’a été utilisé ; chaque
commit passe ci:checks + ci:audit.
Écueils rencontrés
Section intitulée « Écueils rencontrés »Les pièges qui ont coûté du temps ou changé une décision — à connaître pour la suite.
Outillage & CI
Section intitulée « Outillage & CI »- Course d’ordre de build dans turbo (pre-commit). Après chaque
pnpm install(cache turbo invalidé), le hookpre-commita parfois échoué sur unbuild/typecheckà froid : un paquet aval lançait sonDTS/tscavant que le.d.tsd’un paquet amont (errors→validators,baas→auth) ne soit visible. Symptôme :TS2307 Cannot find module '@univ-lehavre/atlas-…'alors queci:checkspasse. Parade : « réchauffer » le build du paquet amont (pnpm --filter <amont> run build) puis relancer le commit. Ce n’est pas un défaut du code committé. commitlintstrict.subject-caseimpose un sujet bas-de-casse : « Effect », « ADR », « CLI », « OpenAlex », « Schema », « E13 » en tête de sujet sont rejetés.scope-enumn’accepte que les scopes déclarés :fetchn’existe pas, c’estfetch-one-api-page. Plusieurs commits ont dû être reformulés.audit:structure. Un nouveau paquet danspackages/doit être publiable (pas deprivate: true) ou inscrit dansPRIVATE_INTERNAL_ALLOWEDavec justification ;test-utils-effect(helper de test interne, jamais publié) y a été ajouté commetest-utils-sveltekit.packages-map& couverture. Toute modification de dépendances exigepnpm docs:generate(sinondocs:generate:checkbloque). Et un seuil de couverture par branches peut tomber sous le plancher quand on retire du code couvert (E13 a retiréisValidAPIResponse, faisant passer citation-fetch de 90 % à 88,46 % de branches) : il a fallu un test ciblé du fallbackapiURLpour repasser au-dessus.
Effect — pièges de conception
Section intitulée « Effect — pièges de conception »- E9 —
@effect/opentelemetryn’enregistre pas le provider global.NodeSdk.layerne pose pas le provider global qu’attend@hono/otel. Solution retenue : conserver leNodeSDKbrut (enregistrement global) etTracer.layerGlobalqui ponte Effect vers ce même provider global — un seul provider, pas de double SDK. - E13 — décoder casse un contrat de test silencieux. En remplaçant le
as TparSchema.decodeUnknownEither, un court-circuitinstanceof ResponseParseErrora brièvement rompu le contrat « content-type non-JSON » (le test attend le message externe enveloppé aveccause.message = texte). La parade : laisser l’enveloppecatchinconditionnelle, sans court-circuit. - E13 — fileter le
Schemasans couplerfetch-one-api-pageà OpenAlex. Le schéma de réponse est dérivé du schéma d’item chez l’appelant et fileté à traversmakeRateLimitedFetcher → apiResponseSchema → fetchOnePage; la couche basse reste agnostique du domaine. - E14 — le canal
Rse propage jusqu’aux racines. TransformerfetchOnePageen serviceContext.TagajouteFetchOnePageau canalRde tous les consommateurs, en cascade jusqu’aux signatures publiques (searchAuthorsByName,fetchAPI, …) puis aux racines de composition (CLIs, handler SvelteKit, frontières de lib) qui doivent fournirFetchOnePageLive. Le compilateur guide la cascade : on suit lesTS2322 … 'FetchOnePage' is not assignable to 'never'un fichier à la fois. Piège associé : un aliasCitationEffect<A>de find-an-expert inféraitEviaextends Effect.Effect<unknown, infer E>(2 paramètres) — il a falluinfer _R(3 paramètres) pour tolérer le nouveau canal et ne pas dégénérer ennever. - E14 —
vi.mockau runtime casse quand la source importe une nouvelle valeur. Dès que la source provient à fournirFetchOnePageLiveimporté decitation-fetch, les tests quivi.mock-aient ce paquet plantent (« No “FetchOnePageLive” export »). Parade : ajouterFetchOnePageLive: Layer.emptyà la fabrique du mock (les wrappers étant déjà mockés, la fabrique réelle ne tourne jamais). RateLimiteren temps réel sousit.effect= timeout. Un test paginant 2 pages avecRateLimiter(limite 1/s) bloque 5 s puis échoue sous le faux-temps d’it.effect. La parade idiomatique (ADR 0049) :TestClock—Effect.forkdu fetch puisTestClock.adjust("2 seconds")libère le jeton, sans attente réelle.
Choix de mise en œuvre
Section intitulée « Choix de mise en œuvre »Les décisions de réalisation, surtout là où elles affinent ou dévient du plan.
- E12 — zéro-rupture sur le type d’erreur.
makeStringBranddérive type+pattern+prédicat+décodeur+constructeur d’un seulSchema. Le type d’erreur de marque change (BrandErrors→ParseError) mais aucun consommateur ne l’inspecte ; les 18 exports nommés sont préservés (sur- ensemble). La migration est donc transparente. - E13 — schéma source unique + ré-exports de compat (choix utilisateur,
zéro-rupture) ;
fetchOnePage(…, schema)générique, schémas OpenAlex chez l’appelant. - E14 — service
FetchOnePagefourni à la frontière, pas au sommet. Plutôt que de propagerFetchOnePagejusqu’au tout dernierrunMain, leLiveentre à la frontière de chaque lib/CLI/handler (researcher-profiles, citation-validate, citation-cli, researcher-profiles-cli, find-an-expert). Les signatures publiques de ces wrappers restentR = never; seuls les tests decitation-fetch(qui appellent les fonctions de bas niveau) injectent unLayerde test.citation-fetchré-exporteFetchOnePage/FetchOnePageLivepour que les consommateurs fournissent leLayersans dépendre du paquet bas niveau. - E14 —
test-utils-effect, source-only. Le paquet n’est ni buildé ni publié (main/types→src), consommé uniquement par les tests. Il exposeTestLoggerLayer(ré-export du logger silencieux du socle, E8),recordingLayer(tag, impl)etmakeRecorder()— un double de service qui enregistre chaque appel et se fournit enLayer, remplaçant layer-natif devi.mocked(fn).mock.calls. - E14 — garde-fou outillé, pas déclaratif. Le faux-vert est interdit par une
règle
no-restricted-syntax(override vitest deshared-config) qui ne vise que lesit/test/fit/xitnus à corps-expressionEffect.*;it.effect(() => Effect.gen(...))et les fabriquesvi.fn(() => Effect.succeed(...))ne sont pas touchés. Aucun faux-vert existant dans le dépôt — la règle est préventive. - E14 — migration des tests « par valeur », pas par dogme (ADR 0049). Tout
test exécutant un Effect de domaine passe à
it.effect. Mais n’ont pas été transformés en services /it.effect, faute de valeur :- DuckDB (
citation/db) : module sans consommateur prod ;connect/runprennent déjà leur dépendance en argument. SeulDuckDBInstance.createest mocké. →vi.mockconservé, test passé enit.effect. - DocumentExtractor (
researcher-profiles/file-extractor) :extractTextest déjà injectable chez le consommateur (match-row mockeextractText, pas les 5 libs de parsing) ; ces libs ne sont mockées que dans son propre test de dispatch — frontière légitime. → pas de service, test enit.effect. net/diagnostics.spec.ts: mockenode:dns/net/tlsavec un timing par callbacks ; conversion risquée pour gain nul. Laissé enasync it.sveltekit-handler/effect.test.tset les tests CLIrunEffectCli: le handler / runner renvoie unePromise, pas un Effect de domaine —async itest correct ; l’envelopper dansEffect.promiseserait de la cérémonie sans bénéfice.
- DuckDB (
Accepted (2026-06-07). Rétrospectif : trace le chantier socle Effect (E1–E14)
réalisé sur la branche effect/socle (commits 252427c1…49875d8e). Ne
remplace ni ne modifie les ADR 0045–0050 ; il les complète par le réel de la
mise en œuvre.
Conséquences
Section intitulée « Conséquences »Bénéfices. Le socle Effect est unifié : runtime central, frontières
étanches, erreurs typées préservées, validation par Schema, observabilité de
bout en bout, et une convention de test outillée (fin du faux-vert garantie
par lint). Les dépendances externes critiques (réseau via FetchOnePage) sont
injectées et testables par Layer, sans vi.mock ni temps réel (TestClock).
Prix payé. La transformation R-channel d’un service touche en cascade tous
ses consommateurs jusqu’aux racines ; c’est mécanique mais large. La migration
des tests s’est faite par valeur : tout n’est pas devenu service, et c’est
délibéré — sur-mocker ou sur-servicer aurait coûté sans bénéfice (l’audit
le signalait comme risque qualité).
À surveiller.
- La course d’ordre de build turbo peut refaire échouer un
pre-commità froid après unpnpm install; réchauffer le paquet amont, ne jamais bypasser le hook. - Le garde-fou anti-faux-vert ne couvre que le pattern
it(() => Effect.xxx); d’autres formes de faux-vert (assertions jamais atteintes) restent du ressort de la revue. - Réévaluation à la cadence d’audit transverse (ADR 0039).