From ce364b734d2360233f875cb875e204b63a49aa66 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:41:39 +0200 Subject: [PATCH 01/33] docs: draft ADR-0023 for the one-time editorial refactoring --- ...-editorial-refactoring-of-accepted-adrs.md | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md diff --git a/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md b/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md new file mode 100644 index 00000000..6f1855e3 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md @@ -0,0 +1,75 @@ +# ADR-0023 | Allow a one-time editorial refactoring of accepted ADRs + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) + +**Status:** Accepted +**Date:** 2026-07-19 +**Decision Makers:** Reefact + +## Context + +The ADR corpus defines accepted records as immutable historical decisions and also defines ADRs as decision records rather than implementation specifications. + +Several accepted ADRs predate or imperfectly apply that separation. They contain exact project paths, configuration properties, workflow steps, command sequences, API signatures, algorithm mechanics, or maintenance procedures that can change while the underlying architectural decision remains valid. + +Leaving those details in place creates two conflicting forms of governance: accepted ADRs cannot be edited, but implementation details embedded in them inevitably become stale. It also makes it difficult to distinguish the durable decision from the current technical realization. + +The maintainer has reviewed the corpus and authorized a one-time editorial migration provided that it does not change any decision, rationale, alternative, consequence, historical status, or attribution. + +## Decision + +The repository will permit one traceable editorial refactoring of existing accepted ADRs to move implementation specifications into dedicated reference documentation without changing their architectural meaning. + +## Rationale + +The migration resolves a contradiction inside the current governance model while preserving the historical value of the records. The durable decision and its reasoning remain in each ADR; volatile mechanics move to documentation that is expected to evolve with the implementation. + +Treating the migration as an explicit architectural decision keeps the exception visible and bounded. It prevents the work from becoming an informal precedent for silently rewriting accepted decisions. + +A single thematic reference is preferable to scattering implementation details across replacement ADRs because those details describe current contracts and procedures rather than new architectural choices. + +## Alternatives Considered + +### Leave accepted ADRs unchanged + +This would preserve strict immutability, but it would also preserve stale or overly detailed implementation material and continue violating the repository's own distinction between decisions and specifications. + +### Supersede every affected ADR + +This would maintain strict historical immutability, but it would create many artificial successor ADRs despite no decision having changed. The resulting history would suggest architectural reversals where only editorial separation occurred. + +### Remove the details without recording an exception + +This would be simpler, but it would make the repository's governance internally inconsistent and establish an undocumented precedent for rewriting accepted records. + +## Consequences + +### Positive + +* Existing ADRs become shorter, more durable, and easier to review as architectural records. +* Implementation details gain a maintainable home that can evolve without rewriting history. +* The repository's ADR policy becomes internally consistent. +* Cross-links can make refinements and later decisions explicit without changing the original meaning. + +### Negative + +* The historical text of affected ADR files changes once, even though their decisions do not. +* Reviewers must verify that no architectural meaning was lost during extraction. +* The migration creates and maintains an additional reference document. + +### Risks + +* Editorial rewriting could accidentally alter the force or scope of a decision. Mitigation: preserve each decision sentence, rationale, alternatives, consequences, status, date, and decision makers unless a separately authorized governance correction applies. +* The exception could be reused later as justification for rewriting accepted decisions. Mitigation: this ADR authorizes only the migration identified in its references; future decision changes still require a superseding ADR. + +## Follow-up Actions + +* Extract implementation-specific material from the affected ADRs into the bilingual ADR implementation reference. +* Add explicit links between ADRs that refine, revisit, or update the API shape of earlier decisions. +* Correct statuses for decisions already implemented and approved by the maintainer. +* Review the final diff specifically for semantic changes to accepted decisions. + +## References + +* [ADR implementation reference](../specifications/adr-implementation-reference.md) +* [ADR corpus and conventions](README.md) From 69cddcf71d48fb2e25c0e74d4597240840bfdb02 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:42:06 +0200 Subject: [PATCH 02/33] docs: translate ADR-0023 to French --- ...itorial-refactoring-of-accepted-adrs.fr.md | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md diff --git a/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md b/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md new file mode 100644 index 00000000..86c9df11 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md @@ -0,0 +1,75 @@ +# ADR-0023 | Autoriser un refactoring éditorial unique des ADR acceptés + +🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) + +**Statut :** Accepté +**Date :** 2026-07-19 +**Décideurs :** Reefact + +## Contexte + +Le corpus d'ADR définit les décisions acceptées comme des archives historiques immuables et précise également qu'un ADR est un relevé de décision, pas une spécification d'implémentation. + +Plusieurs ADR acceptés sont antérieurs à cette séparation ou ne l'appliquent qu'imparfaitement. Ils contiennent des chemins de projets exacts, des propriétés de configuration, des étapes de workflows, des séquences de commandes, des signatures d'API, des détails algorithmiques ou des procédures de maintenance susceptibles d'évoluer alors que la décision architecturale demeure valide. + +Conserver ces détails crée deux règles contradictoires : les ADR acceptés ne peuvent pas être modifiés, mais les détails d'implémentation qu'ils contiennent finissent inévitablement par devenir obsolètes. Cela rend aussi plus difficile la distinction entre la décision durable et sa réalisation technique actuelle. + +Le mainteneur a examiné le corpus et autorisé une migration éditoriale unique à condition qu'elle ne modifie aucune décision, justification, alternative, conséquence, statut historique ni attribution. + +## Décision + +Le dépôt autorisera un refactoring éditorial traçable et unique des ADR acceptés existants afin de déplacer les spécifications d'implémentation vers une documentation de référence dédiée sans modifier leur sens architectural. + +## Justification + +Cette migration résout une contradiction du modèle de gouvernance actuel tout en préservant la valeur historique des enregistrements. La décision durable et son raisonnement restent dans chaque ADR ; les mécanismes volatils sont déplacés vers une documentation appelée à évoluer avec l'implémentation. + +Formaliser la migration comme une décision architecturale rend l'exception visible et limitée. Cela évite qu'elle devienne un précédent informel permettant de réécrire silencieusement des décisions acceptées. + +Une référence thématique unique est préférable à une multiplication d'ADR de remplacement, car ces détails décrivent des contrats et procédures actuels plutôt que de nouvelles décisions architecturales. + +## Alternatives envisagées + +### Laisser les ADR acceptés inchangés + +Cette option préserverait une immutabilité stricte, mais conserverait également des éléments d'implémentation obsolètes ou excessivement détaillés et continuerait de contredire la distinction entre décisions et spécifications. + +### Remplacer chaque ADR concerné par un nouvel ADR + +Cette option préserverait l'immutabilité historique, mais créerait de nombreux successeurs artificiels alors qu'aucune décision n'a changé. L'historique donnerait l'impression de revirements architecturaux là où seul un travail éditorial a eu lieu. + +### Retirer les détails sans enregistrer d'exception + +Cette option serait plus simple, mais rendrait la gouvernance du dépôt incohérente et créerait un précédent non documenté de réécriture des ADR acceptés. + +## Conséquences + +### Positives + +* Les ADR existants deviennent plus courts, plus durables et plus faciles à relire comme décisions architecturales. +* Les détails d'implémentation disposent d'un emplacement maintenable qui peut évoluer sans réécrire l'historique. +* La politique ADR du dépôt devient cohérente avec elle-même. +* Les liens croisés peuvent rendre explicites les raffinements et décisions ultérieures sans modifier le sens initial. + +### Négatives + +* Le texte historique des ADR concernés change une fois, même si leurs décisions ne changent pas. +* Les relecteurs doivent vérifier qu'aucun sens architectural n'a été perdu pendant l'extraction. +* La migration crée et impose de maintenir un document de référence supplémentaire. + +### Risques + +* Une réécriture éditoriale pourrait modifier involontairement la portée ou la force d'une décision. Mesure : conserver la phrase de décision, la justification, les alternatives, les conséquences, le statut, la date et les décideurs sauf correction de gouvernance autorisée séparément. +* L'exception pourrait être réutilisée plus tard pour justifier la réécriture de décisions acceptées. Mesure : cet ADR n'autorise que la migration identifiée dans ses références ; toute modification future d'une décision exige toujours un ADR qui la remplace. + +## Actions de suivi + +* Extraire les éléments spécifiques à l'implémentation des ADR concernés vers la référence bilingue d'implémentation des ADR. +* Ajouter des liens explicites entre les ADR qui raffinent, réexaminent ou mettent à jour la forme d'API de décisions antérieures. +* Corriger le statut des décisions déjà implémentées et approuvées par le mainteneur. +* Relire le diff final spécifiquement pour détecter toute modification sémantique d'une décision acceptée. + +## Références + +* [Référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md) +* [Corpus et conventions des ADR](README.fr.md) From 09e32fca72e060d7de7e2494e8d141ad180bcf9e Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:42:40 +0200 Subject: [PATCH 03/33] docs: add the ADR implementation reference --- .../adr-implementation-reference.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md diff --git a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md new file mode 100644 index 00000000..d4dcf33d --- /dev/null +++ b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md @@ -0,0 +1,81 @@ +# ADR implementation reference + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](adr-implementation-reference.fr.md) + +This document owns implementation details extracted from Architecture Decision Records. ADRs remain the authoritative source for **what was decided and why**; this reference describes the current technical realization and may evolve without changing those decisions. + +## Analyzer compatibility floor + +Related decisions: [ADR-0001](../adr/0001-lock-the-analyzer-roslyn-floor.md). + +The analyzer is compiled against the Roslyn floor declared by `RoslynFloorVersion` in `Directory.Build.props`. The package keeps the analyzer under `analyzers/dotnet/cs/`. + +The current realization uses complementary guards: + +* the analyzer package reference is pinned to the declared floor; +* `RoslynFloorTests` inspects assembly metadata and rejects newer `Microsoft.CodeAnalysis*` references; +* the analyzer workflow packs the real NuGet artifact and builds a sample with the floor SDK, proving both loading and packaging; +* Dependabot ignores automated updates for the floor-defining Roslyn packages. + +When the floor changes, update the central property, the floor SDK used by the workflow and floor-check project, and the documented compiler requirement. The architectural change itself requires a new ADR that supersedes ADR-0001. + +## Tooling runtime floor + +Related decisions: [ADR-0002](../adr/0002-floor-the-tooling-runtime.md), [ADR-0022](../adr/0022-floor-the-library-on-net-framework-4-7-2.md). + +The command-line tooling and out-of-process worker target the oldest supported .NET LTS runtime. The ordinary CI suite runs on the current development SDK, while dedicated floor jobs execute the shipped tooling on the oldest supported runtime. + +The netstandard2.0 libraries have a separate support floor: .NET Framework 4.7.2. Dedicated Windows tests exercise the relevant libraries on the real .NET Framework runtime. Tooling projects remain modern-.NET-only. + +## ADR pull-request check + +Related decision: [ADR-0004](../adr/0004-check-every-pull-request-against-the-adr-base.md). + +The ADR check is a maintainer and agent procedure, documented in `AGENTS.md`, that compares a change against accepted decisions and identifies whether it records, supersedes, or conflicts with an ADR. + +The current GitHub workflow is manually dispatchable and therefore supports the procedure but does not, by itself, guarantee that every pull request was checked. Any future automated enforcement belongs in the workflow documentation and configuration rather than ADR-0004. + +## Request Binder implementation contracts + +Related decisions: [ADR-0007](../adr/0007-name-the-binder-terminals-new-and-create.md), [ADR-0008](../adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md), [ADR-0012](../adr/0012-fix-the-binder-options-before-binding-begins.md), [ADR-0014](../adr/0014-bind-a-required-list-by-presence-not-cardinality.md), [ADR-0017](../adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md), [ADR-0018](../adr/0018-bundle-the-binders-structural-error-code-and-messages.md), [ADR-0019](../adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md), [ADR-0021](../adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md). + +Nullable value-type properties are selected through struct-constrained overloads so converter method groups operate on the underlying value type rather than `Nullable`. + +Binder options are fixed before binding starts. `Bind.WithOptions(...)` returns a reusable configured entry point and stores no per-request state. The application-wide default is frozen on first read and rejects later mutation. + +Structural binder failures are represented by bundled definitions containing the error code and public/diagnostic messages. Consumers that override these definitions document them in their own catalog through the public documentation surface described by ADR-0019. + +Out-of-DTO values enter through the source-agnostic binding entry and participate as peers in the same accumulation and construction flow as DTO-derived values. Exact overloads, generic constraints, names, and examples are API reference material and belong in the Request Binder user documentation and source code. + +## GenDoc catalog compatibility + +Related decision: [ADR-0010](../adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md). + +The generated error catalog is treated as a versioned compatibility artifact. Release automation compares the generated catalog against the baseline associated with the last compatible release and reports incompatible changes before publication. + +The baseline is updated only by the release process after a successful compatible release. Workflow steps, commands, artifact paths, and recovery procedures are maintained in the workflow reference. In particular, maintainers must account for the failure mode where publication succeeds but the subsequent baseline update does not. + +## Dummies generation contracts + +Related decisions: [ADR-0006](../adr/0006-supply-arbitrary-test-values-from-a-seedable-source.md), [ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.md), [ADR-0013](../adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md), [ADR-0015](../adr/0015-cap-any-combine-at-arity-eight.md), [ADR-0020](../adr/0020-materialize-dummies-only-through-generate.md). + +Dummies is shipped as a standalone package with no dependency on the FirstClassErrors runtime package. Generation is unseeded by default; reproducible generation is selected explicitly and exposes the seed needed to replay failures. + +Distinct collection generation first uses a cardinality hint when the source can provide one. When cardinality is unknown, generation uses a bounded draw and fails explicitly rather than looping forever. The bound is a safety mechanism, not a proof that every foreign or biased generator will succeed whenever enough distinct values theoretically exist. + +`Any.Combine` provides overloads up to arity eight. Higher arities are intentionally outside the supported convenience surface and should use composition or a domain-specific factory. + +Materialization occurs only through `Generate()`. Builder operations describe generation and do not produce hidden side effects. + +## Documentation-only public surfaces + +Related decision: [ADR-0019](../adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md). + +Public members introduced solely to make analyzer or documentation extraction possible must remain minimal, stable, and clearly tied to the catalog contract. Before adding another such member, consider whether metadata, generated descriptors, or analyzer-side discovery can satisfy the same need without expanding the runtime API. + +## Maintenance rules + +* Change this reference when implementation mechanics change but the decisions remain valid. +* Write a new ADR when the architectural choice, compatibility promise, or accepted trade-off changes. +* Keep links from each affected ADR to the relevant section of this reference. +* Do not move rationale, rejected alternatives, or architectural consequences out of ADRs. From 26534af9e63b9a25ed51c558444ec839a36f84a6 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:43:18 +0200 Subject: [PATCH 04/33] docs: translate the ADR implementation reference to French --- .../adr-implementation-reference.fr.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md diff --git a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md new file mode 100644 index 00000000..16f3c568 --- /dev/null +++ b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md @@ -0,0 +1,81 @@ +# Référence d'implémentation des ADR + +🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](adr-implementation-reference.md) + +Ce document contient les détails d'implémentation extraits des Architecture Decision Records. Les ADR restent la source de vérité pour **ce qui a été décidé et pourquoi** ; cette référence décrit la réalisation technique actuelle et peut évoluer sans modifier ces décisions. + +## Plancher de compatibilité de l'analyseur + +Décision liée : [ADR-0001](../adr/0001-lock-the-analyzer-roslyn-floor.fr.md). + +L'analyseur est compilé contre le plancher Roslyn déclaré par `RoslynFloorVersion` dans `Directory.Build.props`. Le package conserve l'analyseur sous `analyzers/dotnet/cs/`. + +La réalisation actuelle utilise plusieurs protections complémentaires : + +* la référence de package de l'analyseur est épinglée sur le plancher déclaré ; +* `RoslynFloorTests` inspecte les métadonnées d'assembly et refuse les références `Microsoft.CodeAnalysis*` plus récentes ; +* le workflow de l'analyseur construit le véritable package NuGet puis compile un exemple avec le SDK plancher, ce qui vérifie à la fois le chargement et l'empaquetage ; +* Dependabot ignore les mises à jour automatiques des packages Roslyn qui définissent le plancher. + +Lors d'un changement de plancher, il faut mettre à jour la propriété centrale, le SDK plancher utilisé par le workflow et le projet de vérification, ainsi que l'exigence de compilateur documentée. Le changement architectural lui-même exige un nouvel ADR remplaçant l'ADR-0001. + +## Plancher d'exécution des outils + +Décisions liées : [ADR-0002](../adr/0002-floor-the-tooling-runtime.fr.md), [ADR-0022](../adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md). + +Les outils en ligne de commande et le worker hors processus ciblent le plus ancien runtime .NET LTS pris en charge. La CI ordinaire s'exécute avec le SDK de développement courant, tandis que des jobs dédiés exécutent les outils livrés sur le plus ancien runtime pris en charge. + +Les bibliothèques netstandard2.0 ont un plancher distinct : .NET Framework 4.7.2. Des tests Windows dédiés exercent les bibliothèques concernées sur le véritable runtime .NET Framework. Les projets d'outillage restent réservés au .NET moderne. + +## Vérification ADR des pull requests + +Décision liée : [ADR-0004](../adr/0004-check-every-pull-request-against-the-adr-base.fr.md). + +La vérification ADR est une procédure destinée au mainteneur et aux agents, documentée dans `AGENTS.md`, qui compare une modification aux décisions acceptées et détermine si elle enregistre, remplace ou contredit un ADR. + +Le workflow GitHub actuel est déclenché manuellement. Il soutient donc la procédure, mais ne garantit pas à lui seul que chaque pull request a été vérifiée. Toute automatisation future de cette obligation relève de la documentation et de la configuration des workflows, pas de l'ADR-0004. + +## Contrats d'implémentation du Request Binder + +Décisions liées : [ADR-0007](../adr/0007-name-the-binder-terminals-new-and-create.fr.md), [ADR-0008](../adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md), [ADR-0012](../adr/0012-fix-the-binder-options-before-binding-begins.fr.md), [ADR-0014](../adr/0014-bind-a-required-list-by-presence-not-cardinality.fr.md), [ADR-0017](../adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md), [ADR-0018](../adr/0018-bundle-the-binders-structural-error-code-and-messages.fr.md), [ADR-0019](../adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md), [ADR-0021](../adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md). + +Les propriétés de types valeur nullables sont sélectionnées au moyen de surcharges contraintes aux structures afin que les groupes de méthodes de conversion opèrent sur le type sous-jacent plutôt que sur `Nullable`. + +Les options du binder sont fixées avant le début du binding. `Bind.WithOptions(...)` renvoie un point d'entrée configuré réutilisable et ne conserve aucun état propre à une requête. La valeur par défaut applicative est figée à sa première lecture et refuse toute modification ultérieure. + +Les échecs structurels du binder sont représentés par des définitions regroupant le code d'erreur et les messages public et diagnostique. Les consommateurs qui remplacent ces définitions les documentent dans leur propre catalogue au moyen de la surface publique décrite par l'ADR-0019. + +Les valeurs extérieures au DTO entrent par le point de binding indépendant de la source et participent comme des pairs au même flux d'accumulation et de construction que les valeurs issues du DTO. Les surcharges exactes, contraintes génériques, noms et exemples sont de la documentation d'API et appartiennent à la documentation utilisateur du Request Binder et au code source. + +## Compatibilité du catalogue GenDoc + +Décision liée : [ADR-0010](../adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md). + +Le catalogue d'erreurs généré est traité comme un artefact de compatibilité versionné. L'automatisation de release compare le catalogue généré à la baseline associée à la dernière version compatible et signale les incompatibilités avant publication. + +La baseline n'est mise à jour par le processus de release qu'après une publication compatible réussie. Les étapes de workflow, commandes, chemins d'artefacts et procédures de reprise sont maintenus dans la référence des workflows. Les mainteneurs doivent notamment prendre en compte le cas où la publication réussit mais où la mise à jour suivante de la baseline échoue. + +## Contrats de génération de Dummies + +Décisions liées : [ADR-0006](../adr/0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md), [ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.fr.md), [ADR-0013](../adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md), [ADR-0015](../adr/0015-cap-any-combine-at-arity-eight.fr.md), [ADR-0020](../adr/0020-materialize-dummies-only-through-generate.fr.md). + +Dummies est livré comme package autonome sans dépendance sur le package d'exécution FirstClassErrors. La génération n'est pas seedée par défaut ; la génération reproductible est choisie explicitement et expose la seed nécessaire pour rejouer les échecs. + +La génération de collections distinctes utilise d'abord une indication de cardinalité lorsque la source sait la fournir. Lorsque la cardinalité est inconnue, elle effectue un nombre borné de tirages et échoue explicitement plutôt que de boucler indéfiniment. Cette borne est un mécanisme de sûreté, pas une preuve que tout générateur externe ou biaisé réussira dès lors qu'un nombre suffisant de valeurs distinctes existe théoriquement. + +`Any.Combine` fournit des surcharges jusqu'à l'arité huit. Les arités supérieures sont volontairement exclues de cette surface de confort et doivent utiliser la composition ou une factory spécifique au domaine. + +La matérialisation s'effectue uniquement par `Generate()`. Les opérations du builder décrivent la génération et ne produisent pas d'effets de bord cachés. + +## Surfaces publiques uniquement destinées à la documentation + +Décision liée : [ADR-0019](../adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md). + +Les membres publics ajoutés uniquement pour rendre possible l'analyse ou l'extraction de documentation doivent rester minimaux, stables et clairement liés au contrat du catalogue. Avant d'ajouter un membre similaire, il faut vérifier si des métadonnées, des descripteurs générés ou une découverte côté analyseur peuvent répondre au besoin sans étendre l'API d'exécution. + +## Règles de maintenance + +* Modifier cette référence lorsque les mécanismes d'implémentation changent mais que les décisions restent valides. +* Écrire un nouvel ADR lorsque le choix architectural, la promesse de compatibilité ou le compromis accepté change. +* Conserver depuis chaque ADR concerné un lien vers la section pertinente de cette référence. +* Ne pas déplacer hors des ADR la justification, les alternatives rejetées ni les conséquences architecturales. From 8d599a77131cd56e9adaecef31be6e66c5841d4f Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:43:51 +0200 Subject: [PATCH 05/33] docs: record the ADR editorial exception in the index --- doc/handwritten/for-maintainers/adr/README.md | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index d3e9a179..22891edf 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -6,6 +6,12 @@ place; a decision is revisited by writing a **new** ADR that supersedes the old one, and the old one's status changes to *Superseded* with a link to its successor. +[ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) +authorizes one bounded exception to this rule: a traceable editorial migration +that moves implementation specifications out of existing ADRs without changing +any decision, rationale, alternative, consequence, status, date, or attribution. +It is not a precedent for changing accepted decisions in place. + ## When is an ADR written? Every pull request is checked against this base — the moment new decisions enter @@ -23,12 +29,13 @@ An ADR captures a **decision and the reasoning behind it** — not how that decision is implemented. Implementation mechanics (code, configuration, YAML, exact flags, XML or command snippets, guard-by-guard or step-by-step walkthroughs) live in the code and in the reference documentation the ADR links -to — for example the [workflow reference](../workflows/README.md) — never in the -ADR itself. In particular, **Rationale is argument, not a design document**: if -a paragraph explains *how something is built* rather than *why the decision is -right*, it belongs in the reference docs, and the ADR links to it. A useful -test: if the implementation changed but the decision stood, the ADR should not -need editing. +to — for example the [workflow reference](../workflows/README.md) and the +[ADR implementation reference](../specifications/adr-implementation-reference.md) +— never in the ADR itself. In particular, **Rationale is argument, not a design +document**: if a paragraph explains *how something is built* rather than *why the +decision is right*, it belongs in the reference docs, and the ADR links to it. A +useful test: if the implementation changed but the decision stood, the ADR +should not need editing. ## File conventions @@ -118,10 +125,12 @@ This section explains: It is **argument only**. It does **not** contain implementation detail — no code, configuration, YAML, exact flags, or XML/command snippets, and no guard-by-guard or step-by-step "how it is built". That is specification: link -to where it actually lives (the code, or the [workflow -reference](../workflows/README.md)) instead of pasting it here. Naming a -guard's *role* and *why it exists* is argument and belongs here; documenting -*how the guard is wired* is specification and does not. +to where it actually lives (the code, the [workflow +reference](../workflows/README.md), or the [ADR implementation +reference](../specifications/adr-implementation-reference.md)) instead of +pasting it here. Naming a guard's *role* and *why it exists* is argument and +belongs here; documenting *how the guard is wired* is specification and does +not. ### Alternatives Considered @@ -197,3 +206,4 @@ Optional supporting material: | [ADR-0021](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md) | Bind out-of-DTO arguments as peers through a source-agnostic untyped entry | Proposed | | [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) | Floor the library's .NET Framework support at 4.7.2 | Accepted | | [ADR-0023](0023-keep-expression-tree-selectors-for-the-v1-binder-api.md) | Keep expression-tree selectors for the v1 binder API | Accepted | +| [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) | Allow a one-time editorial refactoring of accepted ADRs | Accepted | From df8a969350921977ba40ba4e3d6bc2fccaf11a6e Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:45:22 +0200 Subject: [PATCH 06/33] docs: separate the Roslyn floor decision from its implementation --- .../0001-lock-the-analyzer-roslyn-floor.md | 158 ++++-------------- 1 file changed, 28 insertions(+), 130 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md index a39b2fdc..7a5fca69 100644 --- a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md +++ b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md @@ -8,168 +8,66 @@ ## Context -`FirstClassErrors.Analyzers` is a Roslyn analyzer that ships **bundled inside -the `FirstClassErrors` NuGet package** (at `analyzers/dotnet/cs/`), so that -consumers who reference the package get the `FCExxx` diagnostics automatically, -with no extra install. +`FirstClassErrors.Analyzers` is bundled inside the `FirstClassErrors` NuGet package and is loaded by each consumer's compiler. The Roslyn version against which the analyzer is compiled therefore becomes the minimum compiler capable of loading it. -A bundled analyzer is loaded by **each consumer's host compiler** — the Roslyn -that comes with their .NET SDK or IDE. The `Microsoft.CodeAnalysis.*` version -the analyzer is *compiled against* therefore becomes the **minimum** Roslyn -able to load it: +A routine Roslyn dependency update can silently raise that minimum. Modern CI and maintainer IDEs do not expose the regression because they already satisfy the newer floor. The repository previously experienced this drift when the analyzer came to require Roslyn 5.6. -* if the analyzer references a Roslyn **newer** than the host, the host refuses - to load it and emits **`CS8032`** (and the analyzer silently does nothing); -* if the analyzer throws while loading, the host emits **`AD0001`**. - -A routine dependency bump of `Microsoft.CodeAnalysis.*` therefore silently -raises the minimum SDK/IDE every consumer must have. This exact regression -happened once: the analyzer drifted to requiring Roslyn 5.6. Dependabot -proposes such bumps automatically, like any other dependency update. - -The load contract is invisible on modern toolchains — CI on the latest SDK, -and the maintainer's own IDE, both satisfy any floor — so it can regress -without any red signal. - -The oldest host FirstClassErrors supports is the **.NET 8.0.100 SDK / -Visual Studio 2022 17.8**, whose compiler is **Roslyn 4.8**. - -`CS8032` is emitted only when a load is *attempted*: an analyzer that is never -wired into a compilation at all leaves the build green. And the analyzer -reaches consumers through a packaging path (`analyzers/dotnet/cs/`) that can -break independently of the analyzer's own references. - -`Microsoft.CodeAnalysis.Analyzers` (5.6.0) is a build-time authoring analyzer -(`PrivateAssets="all"`), not a runtime reference of the shipped assembly, so it -does not affect the load contract. +The oldest supported analyzer host is the .NET 8.0.100 SDK / Visual Studio 2022 17.8, which carries Roslyn 4.8. A complete compatibility guarantee must cover both the analyzer's references and the packaged artifact consumers actually load. ## Decision -The analyzer's Roslyn floor is fixed at **4.8.0** — the Roslyn of the oldest -supported host — declared once as `RoslynFloorVersion` in -`Directory.Build.props` and enforced by four independent guards. +The analyzer's Roslyn floor is fixed at **4.8.0**, the Roslyn version of the oldest supported host, and is protected by independent build-time, test-time, packaging, and dependency-management guards. ## Rationale -The floor value is dictated by the context: 4.8.0 is exactly the Roslyn that -ships with the oldest host we claim to support (.NET 8.0.100 SDK / VS 2022 -17.8). A lower floor buys nothing; a higher one silently drops supported -hosts. - -The contract needs more than a version pin because it regresses without any -red signal on modern toolchains. A pin alone still *looks* like routine -maintenance, and a too-new reference can slip in three distinct ways that no -single check catches: the *reference version* can drift, the shipped analyzer -can fail to *load* on an old host, and it can fail to be *packaged* where -consumers look. The decision therefore layers **defense in depth** — one -source of truth and four guards, each closing a gap the others cannot: - -* the floor is declared **once**, as `RoslynFloorVersion` in - `Directory.Build.props`, so the pin, the test and the CI job track a single - value and can never disagree; -* **the pin** compiles the analyzer against exactly that Roslyn, setting the - floor at its source; -* **the unit test** (`RoslynFloorTests`) reads the floor back from assembly - metadata and fails, fast and in-process, if any referenced - `Microsoft.CodeAnalysis*` assembly exceeds it — catching *reference* drift; -* **the floor-check CI job** packs the library and rebuilds the sample against - the packed analyzer under the floor SDK, proving the **shipped artifact** - both **loads** and is **packaged** correctly on the **oldest supported - compiler** — the two gaps the unit test cannot reach; -* **the Dependabot ignore** keeps an automated PR from ever proposing the bump, - so raising the floor stays a conscious act rather than a rubber-stamped - update. - -Two of the guards fail **loudly** (the unit test and the CI job); the pin and -the Dependabot ignore work silently by construction. Together they satisfy the -requirement the context sets out: a guard that fails on the **oldest** host, on -the **exact artifact we ship**, before a consumer ever sees `CS8032`. The -trade-off accepted is the upkeep of a deliberately intricate CI job and a -non-solution `tools/floor-check/` project; the mechanics of that job — the -two-SDK split, the load proof, and the NuGet traps it closes — are documented -in the [`analyzers` workflow reference](../workflows/analyzers.en.md), and the -pin and metadata live in `FirstClassErrors.Analyzers.csproj`. +The floor follows directly from the compatibility promise: a higher version would silently exclude a supported host, while a lower version would add no supported environment. -## Alternatives Considered +A version pin alone is insufficient because reference drift, analyzer loading, and package placement are distinct failure modes. Independent guards provide defense in depth and ensure that the real shipped artifact is exercised on the oldest supported compiler rather than only on a modern development environment. -### Track the current Roslyn and let dependency bumps flow (status quo) +The additional maintenance cost is justified because analyzer load failures are otherwise silent to maintainers and surface to consumers only after publication. -Considered because it is the default, zero-effort behavior: Roslyn bumps -arrive as routine dependency updates, and nothing in the toolchain objects. +The current technical realization and floor-raising procedure are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#analyzer-compatibility-floor) and the [`analyzers` workflow reference](../workflows/analyzers.en.md). -Rejected because each bump silently raises the minimum SDK/IDE every consumer -must have — the drift to Roslyn 5.6 happened exactly this way, with no red -signal anywhere. +## Alternatives Considered -### Pin the Roslyn version, without further guards +### Track the current Roslyn version -Considered as the minimal fix: a one-line pin stops the drift. +Considered because it is the default dependency-maintenance path. Rejected because each update silently raises the minimum SDK and IDE required by consumers. -Rejected because a bare pin still *looks* like routine maintenance — Dependabot -keeps proposing the bump, and accepting one fails nothing: no test reads the -floor back, and no build runs on the oldest host. The regression would return -with the next well-meaning update. +### Pin the version without additional guards -### Rely on the unit test alone (no floor-check CI job) +Considered as the smallest correction. Rejected because it neither proves that the packaged analyzer loads on the floor host nor prevents a well-intentioned future update from restoring the regression. -Considered because the test is fast, in-process, and runs in the ordinary -`dotnet test`. +### Rely only on an assembly-reference test -Rejected because it only catches *reference* version drift. It cannot prove -that the shipped artifact actually **loads** on the oldest supported host, nor -that the analyzer is actually **packaged** at `analyzers/dotnet/cs/` — both of -which can break while every reference version stays at the floor. +Considered because it is fast and deterministic. Rejected because it cannot prove that the analyzer is packaged at the expected location or that the shipped artifact loads on the oldest compiler. ## Consequences ### Positive -* The load contract cannot silently regress: a too-new Roslyn reference fails - the unit test (fast) **and** the floor-check job (authentic), and a broken - packaging path fails the floor-check job. -* The floor-check job tests the *shipped artifact* on the *oldest supported - host*, not a proxy. -* The floor is a one-line, self-documenting decision (`RoslynFloorVersion`). +* The analyzer's minimum compiler cannot drift through routine dependency maintenance. +* The compatibility claim is verified against the packaged artifact on the oldest supported host. +* Raising the floor remains an explicit architectural decision. ### Negative -* Two extra guards to keep green, and a non-solution `tools/floor-check/` - project with intentionally intricate NuGet configuration (documented in the - [`analyzers` workflow reference](../workflows/analyzers.en.md)). -* The floor-check job downloads the 8.0.100 SDK on every run (~a few seconds). -* Raising the floor is a deliberate, multi-step act (by design). +* Several complementary guards must remain maintained. +* Raising the floor requires coordinated changes across build, test, packaging, and documentation. ### Risks -* The floor-check's NuGet configuration looks over-engineered to a reader who - does not know the traps it closes; a future "simplification" would - reintroduce one of them as a silent bug. Mitigation: every subtlety is - recorded in the [`analyzers` workflow reference](../workflows/analyzers.en.md) - and in the workflow's own YAML comments. -* When the floor is raised, the floor SDK pinned in `analyzers.yml` and in - `tools/floor-check/global.json` must be moved by hand; forgetting them - leaves the job validating the old floor. Mitigation: the raise procedure - below lists them explicitly. +* A future maintainer could remove a guard whose purpose is not obvious. Mitigation: the mechanics are documented in the implementation and workflow references. +* A guard could continue validating an obsolete floor after an intentional change. Mitigation: a floor change must supersede this ADR and follow the documented maintenance procedure. ## Follow-up Actions -* None immediate: the four guards (pin, `RoslynFloorTests`, the `analyzers.yml` - floor-check job, the Dependabot ignore) shipped with this decision. -* When the floor is ever raised: - 1. Bump `` in `Directory.Build.props`. - 2. Update the floor SDK in `analyzers.yml` (`dotnet-version` and the nested - `tools/floor-check/global.json`) to the SDK whose Roslyn matches the new - floor. - 3. Update the README / `doc/handwritten/for-users/README.fr.md` compiler-requirement note. - 4. Supersede this ADR (new floor, new minimum SDK/IDE). - - The pin, the unit test and the Dependabot ignore need no change — they all - track `$(RoslynFloorVersion)` or the package ids. +* None immediate. +* Supersede this ADR when the supported Roslyn floor changes. ## References -* Shaped by #69 (initial lock) and #74 / #75 / #77 (floor-check hardening). -* [ADR-0002](0002-floor-the-tooling-runtime.md) — the tooling runtime floor, - the run-time sibling of this build-time decision. -* [`analyzers` workflow reference](../workflows/analyzers.en.md) — the - floor-check job, structurally. +* [ADR implementation reference — Analyzer compatibility floor](../specifications/adr-implementation-reference.md#analyzer-compatibility-floor) +* [`analyzers` workflow reference](../workflows/analyzers.en.md) +* [ADR-0002](0002-floor-the-tooling-runtime.md) — the runtime counterpart of this compatibility decision. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From 172fda4fbf620679538b6d2e12dc942b7cbbecce Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:45:44 +0200 Subject: [PATCH 07/33] docs: translate ADR-0001's editorial rewrite to French --- .../0001-lock-the-analyzer-roslyn-floor.fr.md | 178 +++--------------- 1 file changed, 29 insertions(+), 149 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md index 715d5616..dd17c9f2 100644 --- a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md +++ b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md @@ -1,4 +1,4 @@ -# ADR-0001 | Verrouiller le floor Roslyn de l'analyzer +# ADR-0001 | Verrouiller le plancher Roslyn de l'analyseur 🌍 🇬🇧 [English](0001-lock-the-analyzer-roslyn-floor.md) · 🇫🇷 Français (ce fichier) @@ -8,186 +8,66 @@ ## Contexte -`FirstClassErrors.Analyzers` est un analyzer Roslyn livré **intégré dans le -package NuGet `FirstClassErrors`** (à `analyzers/dotnet/cs/`), de sorte que les -consommateurs qui référencent le package obtiennent automatiquement les -diagnostics `FCExxx`, sans installation supplémentaire. +`FirstClassErrors.Analyzers` est intégré au package NuGet `FirstClassErrors` et chargé par le compilateur de chaque consommateur. La version de Roslyn contre laquelle l'analyseur est compilé devient donc la version minimale du compilateur capable de le charger. -Un analyzer intégré est chargé par **le compilateur hôte de chaque -consommateur** — le Roslyn fourni avec leur SDK .NET ou leur IDE. La version de -`Microsoft.CodeAnalysis.*` contre laquelle l'analyzer est *compilé* devient donc -le Roslyn **minimum** capable de le charger : +Une mise à jour ordinaire de la dépendance Roslyn peut relever silencieusement ce minimum. La CI moderne et l'IDE du mainteneur ne révèlent pas la régression puisqu'ils satisfont déjà le nouveau plancher. Le dépôt a déjà subi cette dérive lorsque l'analyseur a fini par exiger Roslyn 5.6. -* si l'analyzer référence un Roslyn **plus récent** que l'hôte, l'hôte refuse de - le charger et émet **`CS8032`** (et l'analyzer ne fait silencieusement rien) ; -* si l'analyzer lève une exception lors du chargement, l'hôte émet **`AD0001`**. - -Une montée de version de routine de la dépendance `Microsoft.CodeAnalysis.*` -élève donc silencieusement le SDK/IDE minimum que chaque consommateur doit -posséder. Cette régression précise s'est produite une fois : l'analyzer a dérivé -jusqu'à exiger Roslyn 5.6. Dependabot propose de telles montées de version -automatiquement, comme n'importe quelle autre mise à jour de dépendance. - -Le contrat de chargement est invisible sur les chaînes d'outils modernes — la CI -sur le dernier SDK, ainsi que l'IDE du mainteneur lui-même, satisfont l'un comme -l'autre n'importe quel floor — de sorte qu'il peut régresser sans aucun signal -rouge. - -L'hôte le plus ancien que FirstClassErrors prend en charge est le **SDK .NET -8.0.100 / Visual Studio 2022 17.8**, dont le compilateur est **Roslyn 4.8**. - -`CS8032` n'est émis que lorsqu'un chargement est *tenté* : un analyzer qui n'est -jamais raccordé à une compilation laisse la build au vert. Et l'analyzer atteint -les consommateurs via un chemin de packaging (`analyzers/dotnet/cs/`) qui peut se -casser indépendamment des références propres de l'analyzer. - -`Microsoft.CodeAnalysis.Analyzers` (5.6.0) est un analyzer d'authoring à la -compilation (`PrivateAssets="all"`), et non une référence à l'exécution de -l'assembly livré, de sorte qu'il n'affecte pas le contrat de chargement. +Le plus ancien hôte d'analyseur pris en charge est le SDK .NET 8.0.100 / Visual Studio 2022 17.8, qui embarque Roslyn 4.8. Une garantie complète de compatibilité doit couvrir à la fois les références de l'analyseur et l'artefact empaqueté réellement chargé par les consommateurs. ## Décision -Le floor Roslyn de l'analyzer est fixé à **4.8.0** — le Roslyn de l'hôte pris en -charge le plus ancien — déclaré une seule fois comme `RoslynFloorVersion` dans -`Directory.Build.props` et appliqué par quatre garde-fous indépendants. +Le plancher Roslyn de l'analyseur est fixé à **4.8.0**, la version de Roslyn du plus ancien hôte pris en charge, et protégé par des garde-fous indépendants lors du build, des tests, de l'empaquetage et de la gestion des dépendances. ## Justification -La valeur du floor est dictée par le contexte : 4.8.0 est exactement le Roslyn -livré avec l'hôte le plus ancien que nous prétendons prendre en charge (SDK .NET -8.0.100 / VS 2022 17.8). Un floor plus bas n'apporte rien ; un floor plus haut -abandonne silencieusement des hôtes pris en charge. - -Le contrat nécessite plus qu'un simple épinglage de version, car il régresse sans -aucun signal rouge sur les chaînes d'outils modernes. Un épinglage seul -*ressemble* encore à de la maintenance de routine, et une référence trop récente -peut se glisser de trois manières distinctes qu'aucune vérification unique -n'attrape : la *version de référence* peut dériver, l'analyzer livré peut échouer -à se *charger* sur un hôte ancien, et il peut échouer à être *packagé* là où les -consommateurs le cherchent. La décision superpose donc une **défense en -profondeur** — une source unique de vérité et quatre garde-fous, chacun fermant -une brèche que les autres ne peuvent fermer : - -* le floor est déclaré **une seule fois**, comme `RoslynFloorVersion` dans - `Directory.Build.props`, de sorte que l'épinglage, le test et le job CI suivent - une valeur unique et ne peuvent jamais diverger ; -* **l'épinglage** compile l'analyzer contre exactement ce Roslyn, fixant le floor - à sa source ; -* **le test unitaire** (`RoslynFloorTests`) relit le floor depuis les métadonnées - de l'assembly et échoue, rapidement et in-process, si un quelconque assembly - `Microsoft.CodeAnalysis*` référencé le dépasse — attrapant la dérive de - *référence* ; -* **le job CI de floor-check** empaquette la bibliothèque et reconstruit - l'exemple contre l'analyzer empaqueté sous le SDK du floor, prouvant que - l'**artefact livré** à la fois se **charge** et est **packagé** correctement sur - le **compilateur pris en charge le plus ancien** — les deux brèches que le test - unitaire ne peut atteindre ; -* **l'ignore Dependabot** empêche une PR automatisée de jamais proposer la montée - de version, de sorte que relever le floor reste un acte conscient plutôt qu'une - mise à jour entérinée sans examen. - -Deux des garde-fous échouent **bruyamment** (le test unitaire et le job CI) ; -l'épinglage et l'ignore Dependabot fonctionnent silencieusement par construction. -Ensemble, ils satisfont l'exigence posée par le contexte : un garde-fou qui -échoue sur l'hôte le **plus ancien**, sur l'**artefact exact que nous livrons**, -avant qu'un consommateur ne voie jamais `CS8032`. Le compromis accepté est -l'entretien d'un job CI délibérément complexe et d'un projet `tools/floor-check/` -hors-solution ; les mécanismes de ce job — la séparation en deux SDK, la preuve -de chargement et les pièges NuGet qu'il ferme — sont documentés dans la -[référence du workflow `analyzers`](../workflows/analyzers.fr.md), et l'épinglage -et les métadonnées résident dans `FirstClassErrors.Analyzers.csproj`. +Le plancher découle directement de la promesse de compatibilité : une version supérieure exclurait silencieusement un hôte pris en charge, tandis qu'une version inférieure n'ajouterait aucun environnement supporté. -## Alternatives envisagées +Un simple épinglage de version ne suffit pas, car la dérive des références, le chargement de l'analyseur et son emplacement dans le package sont des modes de panne distincts. Des garde-fous indépendants apportent une défense en profondeur et vérifient l'artefact réellement livré sur le plus ancien compilateur pris en charge, pas seulement dans un environnement de développement moderne. -### Suivre le Roslyn courant et laisser passer les montées de version de dépendances (statu quo) +Le coût de maintenance supplémentaire est justifié, car les échecs de chargement de l'analyseur restent autrement invisibles au mainteneur et n'apparaissent chez les consommateurs qu'après publication. -Envisagée parce que c'est le comportement par défaut, sans effort : les montées -de version de Roslyn arrivent comme des mises à jour de dépendances de routine, -et rien dans la chaîne d'outils ne s'y oppose. +La réalisation technique actuelle et la procédure de relèvement du plancher sont documentées dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#plancher-de-compatibilité-de-lanalyseur) et la [référence du workflow `analyzers`](../workflows/analyzers.fr.md). -Rejetée parce que chaque montée de version élève silencieusement le SDK/IDE -minimum que chaque consommateur doit posséder — la dérive vers Roslyn 5.6 s'est -produite exactement de cette manière, sans aucun signal rouge nulle part. +## Alternatives envisagées -### Épingler la version de Roslyn, sans garde-fous supplémentaires +### Suivre la version courante de Roslyn -Envisagée comme le correctif minimal : un épinglage d'une ligne arrête la dérive. +Envisagé car il s'agit du chemin normal de maintenance des dépendances. Rejeté parce que chaque mise à jour relève silencieusement la version minimale du SDK et de l'IDE exigée des consommateurs. -Rejetée parce qu'un simple épinglage *ressemble* encore à de la maintenance de -routine — Dependabot continue de proposer la montée de version, et en accepter -une ne fait rien échouer : aucun test ne relit le floor, et aucune build ne -s'exécute sur l'hôte le plus ancien. La régression reviendrait avec la prochaine -mise à jour bien intentionnée. +### Épingler la version sans garde-fous supplémentaires -### S'appuyer sur le seul test unitaire (pas de job CI de floor-check) +Envisagé comme correction minimale. Rejeté parce que cela ne prouve ni que l'analyseur empaqueté se charge sur l'hôte plancher, ni qu'une future mise à jour bien intentionnée ne réintroduira pas la régression. -Envisagée parce que le test est rapide, in-process, et s'exécute dans le -`dotnet test` ordinaire. +### S'appuyer uniquement sur un test des références d'assembly -Rejetée parce qu'elle n'attrape que la dérive de version de *référence*. Elle ne -peut pas prouver que l'artefact livré se **charge** réellement sur l'hôte pris en -charge le plus ancien, ni que l'analyzer est réellement **packagé** à -`analyzers/dotnet/cs/` — deux choses qui peuvent se casser alors que chaque -version de référence reste au floor. +Envisagé car il est rapide et déterministe. Rejeté parce qu'il ne peut prouver ni que l'analyseur est empaqueté à l'emplacement attendu, ni que l'artefact livré se charge sur le plus ancien compilateur. ## Conséquences ### Positives -* Le contrat de chargement ne peut pas régresser silencieusement : une référence - Roslyn trop récente fait échouer le test unitaire (rapide) **et** le job de - floor-check (authentique), et un chemin de packaging cassé fait échouer le job - de floor-check. -* Le job de floor-check teste l'*artefact livré* sur l'*hôte pris en charge le - plus ancien*, et non un substitut. -* Le floor est une décision d'une seule ligne, auto-documentée - (`RoslynFloorVersion`). +* Le compilateur minimal de l'analyseur ne peut plus dériver au gré de la maintenance courante des dépendances. +* La promesse de compatibilité est vérifiée sur l'artefact empaqueté et sur le plus ancien hôte pris en charge. +* Un relèvement du plancher reste une décision architecturale explicite. ### Négatives -* Deux garde-fous supplémentaires à maintenir au vert, et un projet - `tools/floor-check/` hors-solution avec une configuration NuGet - intentionnellement complexe (documentée dans la [référence du workflow - `analyzers`](../workflows/analyzers.fr.md)). -* Le job de floor-check télécharge le SDK 8.0.100 à chaque exécution (~quelques - secondes). -* Relever le floor est un acte délibéré et en plusieurs étapes (à dessein). +* Plusieurs garde-fous complémentaires doivent être maintenus. +* Relever le plancher exige des modifications coordonnées du build, des tests, de l'empaquetage et de la documentation. ### Risques -* La configuration NuGet du floor-check paraît sur-conçue à un lecteur qui ne - connaît pas les pièges qu'elle ferme ; une future « simplification » en - réintroduirait un comme un bug silencieux. Atténuation : chaque subtilité est - consignée dans la [référence du workflow `analyzers`](../workflows/analyzers.fr.md) - et dans les commentaires YAML du workflow lui-même. -* Lorsque le floor est relevé, le SDK du floor épinglé dans `analyzers.yml` et - dans `tools/floor-check/global.json` doit être déplacé à la main ; les oublier - laisse le job valider l'ancien floor. Atténuation : la procédure de relèvement - ci-dessous les liste explicitement. +* Un futur mainteneur pourrait supprimer un garde-fou dont l'utilité n'est pas évidente. Mesure : les mécanismes sont documentés dans les références d'implémentation et de workflow. +* Un garde-fou pourrait continuer à valider un ancien plancher après un changement volontaire. Mesure : tout changement de plancher doit remplacer cet ADR et suivre la procédure documentée. ## Actions de suivi -* Aucune dans l'immédiat : les quatre garde-fous (épinglage, `RoslynFloorTests`, - le job de floor-check `analyzers.yml`, l'ignore Dependabot) ont été livrés avec - cette décision. -* Si le floor est un jour relevé : - 1. Monter `` dans `Directory.Build.props`. - 2. Mettre à jour le SDK du floor dans `analyzers.yml` (`dotnet-version` et le - `tools/floor-check/global.json` imbriqué) vers le SDK dont le Roslyn - correspond au nouveau floor. - 3. Mettre à jour la note d'exigence de compilateur du README / - `doc/handwritten/for-users/README.fr.md`. - 4. Remplacer cet ADR (nouveau floor, nouveau SDK/IDE minimum). - - L'épinglage, le test unitaire et l'ignore Dependabot ne nécessitent aucun - changement — ils suivent tous `$(RoslynFloorVersion)` ou les identifiants de - package. +* Aucune action immédiate. +* Remplacer cet ADR lorsque le plancher Roslyn pris en charge change. ## Références -* Façonné par #69 (verrouillage initial) et #74 / #75 / #77 (durcissement du - floor-check). -* [ADR-0002](0002-floor-the-tooling-runtime.fr.md) — le floor du runtime - d'outillage, le pendant à l'exécution de cette décision à la compilation. -* [référence du workflow `analyzers`](../workflows/analyzers.fr.md) — le job de - floor-check, structurellement. +* [Référence d'implémentation des ADR — Plancher de compatibilité de l'analyseur](../specifications/adr-implementation-reference.fr.md#plancher-de-compatibilité-de-lanalyseur) +* [Référence du workflow `analyzers`](../workflows/analyzers.fr.md) +* [ADR-0002](0002-floor-the-tooling-runtime.fr.md) — la décision équivalente pour le runtime. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From d11214d46a572f3ef7067859bdd520fd034c409b Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:46:14 +0200 Subject: [PATCH 08/33] docs: separate the tooling floor decision from its implementation --- .../adr/0002-floor-the-tooling-runtime.md | 193 +++--------------- 1 file changed, 31 insertions(+), 162 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md index 24ac82df..27b0d0d2 100644 --- a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md +++ b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md @@ -8,196 +8,65 @@ ## Context -FirstClassErrors ships two very different kinds of artifact: - -* the **library** (`FirstClassErrors`, `FirstClassErrors.Testing`) targets - **`netstandard2.0`**. A netstandard library is consumed by *any* runtime that - implements the standard — .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5–10+, - Mono/Unity — so the "runs almost everywhere" question is already answered, - once, by the TFM, and needs nothing here. -* the **tooling** (`FirstClassErrors.Cli` — the `fce` .NET tool — plus - `FirstClassErrors.GenDoc` and `FirstClassErrors.GenDoc.Worker`, which it - loads in-process and spawns as a child) is a **runnable framework-dependent - app**. Its TFM is a **hard minimum**: a framework-dependent app can never run - on a runtime **older** than its TFM, and **roll-forward only ever goes up, - never down**. - -The tooling TFM therefore decides *which consumers can run `fce` at all*. It -was `net10.0`, which meant a shop whose newest installed runtime is .NET 8 -could reference the library but **could not run the documentation generator** — -even though the library it documents is `netstandard2.0`. - -There is a second, subtler constraint. The worker loads the **target** assembly -via `Assembly.LoadFrom` (see `FirstClassErrors.GenDoc.Worker/Program.cs`). That -target can be built for any runtime the consumer chose, so the worker -**process** must run on a runtime `>=` the target's. This is a *roll-forward* -problem, not a *TFM-count* problem, and the two are easy to conflate. - -Roll-forward policies behave as follows: the default `Minor` policy never -crosses a major; `Major` rolls up to the next major only when the requested -major is *absent*; `LatestMajor` always binds the **highest installed** major. - -.NET 8 is the oldest .NET still in Microsoft support (its EOL date is -2026-11-10), and it is the floor the analyzer already states: -[ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) pins Roslyn 4.8, the -compiler of the .NET 8.0.100 SDK. - -CI builds on the latest released .NET SDK (currently .NET 10), and GitHub -runners carry several runtimes side by side. +FirstClassErrors ships both broadly consumable libraries and runnable tooling. The libraries target `netstandard2.0`; the command-line tool, documentation generator, and worker are framework-dependent applications whose target framework is a hard minimum runtime. + +The tooling previously targeted the latest .NET runtime. That prevented consumers on an older supported LTS from running `fce`, even when their application could consume the libraries. + +The worker also loads consumer assemblies. Its process must therefore be able to run on a runtime compatible with the target assembly it inspects. This is a runtime-selection concern rather than a reason to publish one binary per .NET release. + +At the time of the decision, .NET 8 was the oldest supported LTS and matched the product's analyzer-host floor. The library's separate .NET Framework support floor is defined by [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md), which refines the incidental statement previously carried here. ## Decision -The tooling (`FirstClassErrors.Cli`, `FirstClassErrors.GenDoc`, -`FirstClassErrors.GenDoc.Worker`) single-targets **`net8.0`** — the oldest -.NET still in Microsoft support — and covers every newer runtime with -roll-forward, not a target matrix. +The tooling (`FirstClassErrors.Cli`, `FirstClassErrors.GenDoc`, and `FirstClassErrors.GenDoc.Worker`) single-targets **`net8.0`**, the oldest supported .NET LTS at the time of this decision, and supports newer runtimes through roll-forward rather than a target-framework matrix. ## Rationale -The floor makes the support story one sentence: *FirstClassErrors supports -.NET 8 and up for its tooling and its analyzer; the library itself is -`netstandard2.0` and runs down to .NET Framework 4.6.1.* The tooling floor and -the analyzer floor (ADR-0001) state the **same** minimum, so the product -states **one** support number. - -Roll-forward covers every newer runtime, tuned per process: - -| Project | `RollForward` | Why | -|---|---|---| -| `FirstClassErrors.Cli` (`fce`) | `Major` | The front-end only needs to *run*. `Major` rolls the net8 build up to the next major when .NET 8 is absent, so a machine that has only .NET 10 runs it (rolls 8→10). Without it, the default `Minor` policy never crosses a major and `fce` would fail to start on the common "newer .NET, no .NET 8" machine. | -| `FirstClassErrors.GenDoc.Worker` | `LatestMajor` | The worker must **out-rank the target it loads**. `Major` only rolls up when the requested major is *absent*, so on a machine carrying **both** .NET 8 and .NET 10 a net8 worker would bind to 8 and fail to load a net10 target. `LatestMajor` always binds the **highest installed** major, so the worker can document a target built for any runtime present. | -| `FirstClassErrors.GenDoc` | — | Loaded in-process by `fce`; the runtime is chosen by the CLI's runtimeconfig, so a library sets no policy. | - -`latest` stays on the three projects so the net8 -floor bounds only the **BCL surface and target runtime**, not the C# the -source may use (the `netstandard2.0` projects already do exactly this). - -This design also avoids a per-release treadmill: a new .NET release (net11, -net12, …) requires **no rebuild, no code change, no re-release** — roll-forward -runs the existing `net8.0` binaries on it — and a runtime *above* the floor -reaching EOL requires nothing, because we do not target it. Only the floor LTS -itself reaching EOL calls for a bump, one line per project, on a roughly -biennial cadence (see Follow-up Actions). - -The decision is safe to hold because the floor is enforceable on both axes it -can regress on, and each axis has a guard: - -* **API surface drifts at build time.** Because the projects *target* `net8.0`, - every CI build (on the .NET 10 SDK) compiles them against the net8 reference - pack, so a `net10`-only API cannot slip in silently — it breaks the ordinary - build, with no dedicated job needed. This is why the tooling floor is cheaper - to guard than the analyzer's Roslyn floor, which is invisible on a modern CI - and needs `tools/floor-check` (ADR-0001). -* **Runtime execution regresses at run time.** The `floor` job in `ci.yml` - runs the shipped net8 tooling on the .NET 8 runtime itself, proving that both - the CLI and the worker actually start and document a real net8 target there — - the guarantee the build cannot give. The one surface it cannot cover, - roll-forward onto a **not-yet-released** major, is watched ahead of time by - the weekly `canary.yml`, which runs the same tooling on the next .NET preview - and warns the maintainer before that major ships. - -Both jobs' mechanics — the roll-forward overrides that pin execution to the -intended runtime, and why `Usage` is multi-targeted to give them a target — are -documented in the [`ci` workflow reference](../workflows/ci.en.md); the -per-project `RollForward` settings live in the three tooling csprojs. +A single floor build gives every consumer on the supported range access to the tooling without creating a per-release matrix. It keeps the compatibility statement aligned with the analyzer host while avoiding rebuilds that add no functional value. -## Alternatives Considered - -### Keep the tooling on `net10.0` (status quo) +Roll-forward is the correct mechanism because the tooling needs to execute on newer installed runtimes, and the worker must select a runtime capable of loading the target assembly. Publishing several target frameworks would not remove that worker constraint and would create ongoing release churn. -Considered because it was the existing state: targeting the latest runtime is -the path of least resistance and needs no roll-forward tuning. +The floor remains enforceable on two independent axes: compilation prevents accidental use of APIs above the target framework, while dedicated runtime checks exercise the shipped tooling at the floor and on upcoming runtimes. -Rejected because the TFM is a hard minimum for a framework-dependent app: a -shop whose newest installed runtime is .NET 8 could reference the -`netstandard2.0` library but could not run the documentation generator that -documents it. +The exact runtime policies, CI jobs, project settings, and maintenance procedure are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#tooling-runtime-floor) and the [`ci` workflow reference](../workflows/ci.en.md). -### Multi-target the tooling (`net8.0;net10.0`) +## Alternatives Considered -Considered as the conventional way to serve several runtimes at once. +### Keep the tooling on the latest runtime -Rejected because: +Considered because it is the simplest project configuration. Rejected because the target framework is a hard minimum and would exclude consumers on an older supported LTS. -* roll-forward already lets a single `net8.0` build run on 8 / 9 / 10 / 11+, - so a second TFM buys reach we already have; -* a documentation generator has no need for `net10`-only BCL APIs; -* a matrix puts the tooling on a per-release "add at the top, drop at the - bottom" cadence, and **re-introduces the worker trap**: the low build in the - matrix is exactly the one that cannot load a higher-TFM target. +### Multi-target the tooling -One floor build + the two roll-forward settings is strictly simpler and has -the same reach. +Considered as the conventional compatibility strategy. Rejected because one floor build already reaches newer runtimes through roll-forward, while a matrix adds release maintenance and does not solve the worker's need to load higher-targeted assemblies. ## Consequences ### Positive -* Any consumer on **.NET 8 or newer** can run `fce`, not just those on the - latest runtime. -* **One** shipped tooling artifact; no per-release TFM matrix to maintain. -* The tooling floor and the analyzer floor state the **same** minimum (.NET 8), - so the support story is a single sentence. -* Verified end to end: a `net8.0` `fce` documents a **`net10`** target assembly - on a machine that has **only** the .NET 10 runtime — `fce` rolls 8→10 - (`Major`) and the worker binds the highest major (`LatestMajor`) to load the - net10 target. -* Guarded in CI across the whole range: `build-test` runs the suite on the - latest released .NET (10); the `floor` job runs the shipped tooling on the - .NET 8 runtime; and `canary.yml` runs it on the next .NET preview (see - Rationale, and the [`ci` workflow reference](../workflows/ci.en.md)). -* No code churn from .NET version movement; at most a one-line TFM bump about - once every two years. +* Consumers on the oldest supported LTS or any newer runtime can run the tooling. +* The repository ships one tooling artifact rather than a per-release target matrix. +* Runtime compatibility is verified at the floor and monitored ahead of new .NET releases. ### Negative -* `fce` cannot run on a machine whose newest runtime predates .NET 8 (e.g. an - EOL .NET 6/7, or .NET-Framework-only). Accepted: those consumers still - **use** the `netstandard2.0` library in their app; running a dev/CI *tool* - on a currently-supported runtime is a reasonable prerequisite (a modern .NET - SDK is already present wherever modern .NET is built). +* The tooling cannot run on runtimes older than the selected LTS floor. +* Runtime-selection policies and dedicated compatibility checks must remain maintained. ### Risks -* `LatestMajor` on the worker will, on a box that has a **preview** of the - next major installed, bind that preview. This is only a risk for machines - that opt into previews, and `canary.yml` is exactly the early warning that - this binding still works before that major ships. -* A roll-forward regression against a not-yet-released major is caught by the - weekly canary, not by a pull-request gate — by design, since a preview may - be unpublished or unstable. +* A future runtime could change roll-forward behavior or break the tooling. Mitigation: exercise the current floor in CI and the next runtime through the canary workflow. +* The support statement could become inconsistent when the floor LTS reaches end of support. Mitigation: supersede this ADR and update the analyzer and tooling support documentation together. ## Follow-up Actions -* **When the floor LTS reaches EOL** (.NET 8 → 2026-11-10; hygiene rather than - function — roll-forward keeps the net8 build working on newer runtimes after - EOL — on a roughly biennial cadence): - 1. Change `` from `net8.0` to the new floor in the three - tooling csprojs: `FirstClassErrors.Cli`, `FirstClassErrors.GenDoc`, - `FirstClassErrors.GenDoc.Worker`. Leave the `RollForward` settings as - they are. - 2. Bump the new floor in the `Usage` sample's `` (so the - CI floor job still has a target on the new floor) and in the `floor` job - of `ci.yml` (`dotnet-version` `8.0.x` → the new floor's runtime). - 3. Update the runtime note in `FirstClassErrors.Cli/README.nuget.md`. - 4. Supersede this ADR (new floor, new minimum runtime). - 5. Optionally drop the `latest` overrides if the - new floor's default C# is already the version you want. - - Keeping this in step with the analyzer floor (ADR-0001) keeps the product's - single ".NET N and up" support statement true. -* **When the canary's preview major reaches GA:** `canary.yml` pins the - preview major it targets (`dotnet-version: 11.0.x`, quality `preview`); bump - it to the next one (`12.0.x`, …) so the canary keeps looking one release - ahead. Nothing breaks if you forget: `build-test` picks up the newly - released major as "latest", and the canary simply stops finding a - newer-than-build-SDK preview and ends its runs neutral until bumped. +* Supersede this ADR when the tooling runtime floor changes. +* Keep the canary pointed at the next .NET release. ## References -* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) — the analyzer's Roslyn - floor, the build-time sibling of this run-time decision. -* [`ci` workflow reference](../workflows/ci.en.md) — the `floor` job, - structurally. -* `FirstClassErrors.GenDoc.Worker/Program.cs` — the `Assembly.LoadFrom` call - behind the worker's roll-forward constraint. +* [ADR implementation reference — Tooling runtime floor](../specifications/adr-implementation-reference.md#tooling-runtime-floor) +* [`ci` workflow reference](../workflows/ci.en.md) +* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) — the analyzer-host counterpart. +* [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) — refines the library's .NET Framework floor; it replaces the incidental 4.6.1 statement formerly present in this ADR. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From 47a0f155190ee129cccdd89fec71f980b98af13c Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:46:44 +0200 Subject: [PATCH 09/33] docs: translate ADR-0002's editorial rewrite to French --- .../adr/0002-floor-the-tooling-runtime.fr.md | 213 +++--------------- 1 file changed, 32 insertions(+), 181 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md index 9ab27fd2..5e4a720f 100644 --- a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md +++ b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md @@ -1,4 +1,4 @@ -# ADR-0002 | Fixer le floor du runtime de l'outillage à la plus ancienne LTS supportée +# ADR-0002 | Fixer le plancher du runtime de l'outillage à la plus ancienne LTS prise en charge 🌍 🇬🇧 [English](0002-floor-the-tooling-runtime.md) · 🇫🇷 Français (ce fichier) @@ -8,214 +8,65 @@ ## Contexte -FirstClassErrors livre deux types d'artefacts très différents : - -* la **bibliothèque** (`FirstClassErrors`, `FirstClassErrors.Testing`) cible - **`netstandard2.0`**. Une bibliothèque netstandard est consommée par - *n'importe quel* runtime qui implémente le standard — .NET Framework 4.6.1+, - .NET Core 2.0+, .NET 5–10+, Mono/Unity — de sorte que la question - « fonctionne presque partout » est déjà résolue, une seule fois, par le TFM, - et ne nécessite rien ici. -* l'**outillage** (`FirstClassErrors.Cli` — l'outil .NET `fce` — plus - `FirstClassErrors.GenDoc` et `FirstClassErrors.GenDoc.Worker`, qu'il charge - in-process et lance comme processus enfant) est une **application exécutable - dépendante du framework**. Son TFM est un **minimum strict** : une application - dépendante du framework ne peut jamais s'exécuter sur un runtime **plus - ancien** que son TFM, et le **roll-forward ne va jamais que vers le haut, - jamais vers le bas**. - -Le TFM de l'outillage décide donc *quels consommateurs peuvent exécuter `fce` -tout court*. Il était à `net10.0`, ce qui signifiait qu'un atelier dont le -runtime installé le plus récent est .NET 8 pouvait référencer la bibliothèque -mais **ne pouvait pas exécuter le générateur de documentation** — alors même que -la bibliothèque qu'il documente est `netstandard2.0`. - -Il existe une seconde contrainte, plus subtile. Le worker charge l'assembly -**cible** via `Assembly.LoadFrom` (voir -`FirstClassErrors.GenDoc.Worker/Program.cs`). Cette cible peut être compilée -pour n'importe quel runtime choisi par le consommateur, de sorte que le -**processus** worker doit s'exécuter sur un runtime `>=` à celui de la cible. -C'est un problème de *roll-forward*, pas un problème de *nombre de TFM*, et les -deux sont faciles à confondre. - -Les politiques de roll-forward se comportent comme suit : la politique `Minor` -par défaut ne franchit jamais une version majeure ; `Major` monte à la version -majeure suivante uniquement lorsque la majeure demandée est *absente* ; -`LatestMajor` se lie toujours à la **plus haute** majeure **installée**. - -.NET 8 est le plus ancien .NET encore supporté par Microsoft (sa date d'EOL est -le 2026-11-10), et c'est le floor que l'analyzer énonce déjà : -[ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) épingle Roslyn 4.8, le -compilateur du SDK .NET 8.0.100. - -La CI compile sur le dernier SDK .NET publié (actuellement .NET 10), et les -runners GitHub embarquent plusieurs runtimes côte à côte. +FirstClassErrors livre à la fois des bibliothèques largement consommables et des outils exécutables. Les bibliothèques ciblent `netstandard2.0` ; l'outil en ligne de commande, le générateur de documentation et le worker sont des applications dépendantes d'un framework dont le TFM constitue un runtime minimum strict. + +L'outillage ciblait auparavant la dernière version de .NET. Cela empêchait les consommateurs utilisant une LTS plus ancienne mais encore prise en charge d'exécuter `fce`, même lorsque leur application pouvait consommer les bibliothèques. + +Le worker charge également les assemblies des consommateurs. Son processus doit donc pouvoir s'exécuter sur un runtime compatible avec l'assembly cible qu'il inspecte. Il s'agit d'une question de sélection du runtime, pas d'une raison de publier un binaire par version de .NET. + +Au moment de la décision, .NET 8 était la plus ancienne LTS prise en charge et correspondait au plancher de l'hôte de l'analyseur. Le plancher distinct de prise en charge de .NET Framework par la bibliothèque est défini par l'[ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.fr.md), qui raffine la mention incidente auparavant présente ici. ## Décision -L'outillage (`FirstClassErrors.Cli`, `FirstClassErrors.GenDoc`, -`FirstClassErrors.GenDoc.Worker`) cible uniquement **`net8.0`** — le plus ancien -.NET encore supporté par Microsoft — et couvre tous les runtimes plus récents -avec le roll-forward, et non une matrice de cibles. +L'outillage (`FirstClassErrors.Cli`, `FirstClassErrors.GenDoc` et `FirstClassErrors.GenDoc.Worker`) cible uniquement **`net8.0`**, la plus ancienne LTS .NET prise en charge au moment de cette décision, et prend en charge les runtimes plus récents par roll-forward plutôt que par une matrice de frameworks cibles. ## Justification -Le floor réduit l'histoire du support à une seule phrase : *FirstClassErrors -supporte .NET 8 et au-delà pour son outillage et son analyzer ; la bibliothèque -elle-même est `netstandard2.0` et descend jusqu'à .NET Framework 4.6.1.* Le -floor de l'outillage et le floor de l'analyzer (ADR-0001) énoncent le **même** -minimum, de sorte que le produit énonce **un seul** chiffre de support. - -Le roll-forward couvre tous les runtimes plus récents, ajusté par processus : - -| Projet | `RollForward` | Pourquoi | -|---|---|---| -| `FirstClassErrors.Cli` (`fce`) | `Major` | Le front-end a seulement besoin de *s'exécuter*. `Major` monte le build net8 à la majeure suivante lorsque .NET 8 est absent, de sorte qu'une machine qui n'a que .NET 10 l'exécute (monte 8→10). Sans lui, la politique `Minor` par défaut ne franchit jamais une majeure et `fce` échouerait à démarrer sur la machine courante « .NET plus récent, pas de .NET 8 ». | -| `FirstClassErrors.GenDoc.Worker` | `LatestMajor` | Le worker doit **surclasser la cible qu'il charge**. `Major` ne monte que lorsque la majeure demandée est *absente*, de sorte que sur une machine qui embarque **à la fois** .NET 8 et .NET 10, un worker net8 se lierait à 8 et échouerait à charger une cible net10. `LatestMajor` se lie toujours à la **plus haute** majeure **installée**, de sorte que le worker peut documenter une cible compilée pour n'importe quel runtime présent. | -| `FirstClassErrors.GenDoc` | — | Chargé in-process par `fce` ; le runtime est choisi par le runtimeconfig de la CLI, de sorte qu'une bibliothèque ne fixe aucune politique. | - -`latest` reste sur les trois projets afin que le floor -net8 ne borne que la **surface BCL et le runtime cible**, et non le C# que la -source peut utiliser (les projets `netstandard2.0` font déjà exactement cela). - -Ce design évite aussi un cycle répétitif à chaque version : une nouvelle version -.NET (net11, net12, …) ne requiert **aucun rebuild, aucun changement de code, -aucune re-publication** — le roll-forward exécute dessus les binaires `net8.0` -existants — et un runtime *au-dessus* du floor qui atteint son EOL ne requiert -rien, parce que nous ne le ciblons pas. Seul le floor LTS lui-même atteignant -son EOL appelle un bump, une ligne par projet, selon une cadence à peu près -biennale (voir Actions de suivi). - -La décision est sûre à maintenir parce que le floor peut être imposé sur les -deux axes sur lesquels il peut régresser, et chaque axe dispose d'un garde-fou : - -* **La surface d'API dérive au moment du build.** Parce que les projets - *ciblent* `net8.0`, chaque build de CI (sur le SDK .NET 10) les compile contre - le reference pack net8, de sorte qu'une API propre à `net10` ne peut pas se - glisser silencieusement — elle casse le build ordinaire, sans qu'un job dédié - soit nécessaire. C'est pourquoi le floor de l'outillage est moins coûteux à - garder que le floor Roslyn de l'analyzer, qui est invisible sur une CI moderne - et nécessite `tools/floor-check` (ADR-0001). -* **L'exécution runtime régresse au moment de l'exécution.** Le job `floor` dans - `ci.yml` exécute l'outillage net8 livré sur le runtime .NET 8 lui-même, - prouvant que la CLI et le worker démarrent et documentent effectivement une - vraie cible net8 à cet endroit — la garantie que le build ne peut pas donner. - La seule surface qu'il ne peut pas couvrir, le roll-forward vers une majeure - **pas encore publiée**, est surveillée à l'avance par le `canary.yml` - hebdomadaire, qui exécute le même outillage sur la prochaine preview .NET et - avertit le mainteneur avant que cette majeure ne sorte. - -Les mécaniques des deux jobs — les overrides de roll-forward qui épinglent -l'exécution au runtime voulu, et pourquoi `Usage` est multi-ciblé pour leur -fournir une cible — sont documentées dans la -[référence du workflow `ci`](../workflows/ci.fr.md) ; les réglages `RollForward` -par projet vivent dans les trois csprojs de l'outillage. +Un build unique sur le plancher permet à tous les consommateurs de la plage supportée d'utiliser l'outillage sans créer une matrice à maintenir à chaque version. Il aligne la déclaration de compatibilité sur l'hôte de l'analyseur tout en évitant des reconstructions sans valeur fonctionnelle. -## Alternatives envisagées - -### Garder l'outillage sur `net10.0` (statu quo) +Le roll-forward est le bon mécanisme, car l'outillage doit s'exécuter sur les runtimes plus récents installés et le worker doit sélectionner un runtime capable de charger l'assembly cible. Publier plusieurs frameworks cibles ne supprimerait pas cette contrainte et créerait une maintenance continue des releases. -Envisagé parce que c'était l'état existant : cibler le runtime le plus récent -est la voie de moindre résistance et ne nécessite aucun ajustement de -roll-forward. +Le plancher reste vérifiable selon deux axes indépendants : la compilation empêche l'utilisation accidentelle d'API supérieures au framework cible, tandis que des vérifications d'exécution dédiées exercent l'outillage livré sur le plancher et sur les runtimes à venir. -Rejeté parce que le TFM est un minimum strict pour une application dépendante du -framework : un atelier dont le runtime installé le plus récent est .NET 8 -pourrait référencer la bibliothèque `netstandard2.0` mais ne pourrait pas -exécuter le générateur de documentation qui la documente. +Les politiques exactes de runtime, les jobs de CI, les réglages de projets et la procédure de maintenance sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#plancher-dexécution-des-outils) et la [référence du workflow `ci`](../workflows/ci.fr.md). -### Multi-cibler l'outillage (`net8.0;net10.0`) +## Alternatives envisagées -Envisagé comme la façon conventionnelle de servir plusieurs runtimes à la fois. +### Conserver l'outillage sur le dernier runtime -Rejeté parce que : +Envisagé car il s'agit de la configuration de projet la plus simple. Rejeté parce que le framework cible constitue un minimum strict et exclurait les consommateurs utilisant une LTS plus ancienne mais encore prise en charge. -* le roll-forward permet déjà à un seul build `net8.0` de s'exécuter sur - 8 / 9 / 10 / 11+, de sorte qu'un second TFM achète une portée que nous avons - déjà ; -* un générateur de documentation n'a aucun besoin d'API BCL propres à `net10` ; -* une matrice met l'outillage sur une cadence « ajouter en haut, retirer en - bas » à chaque version, et **réintroduit le piège du worker** : le build bas - de la matrice est précisément celui qui ne peut pas charger une cible au TFM - plus élevé. +### Multi-cibler l'outillage -Un seul build floor + les deux réglages de roll-forward est strictement plus -simple et a la même portée. +Envisagé comme stratégie classique de compatibilité. Rejeté parce qu'un build sur le plancher atteint déjà les runtimes plus récents par roll-forward, tandis qu'une matrice ajoute une maintenance de release et ne résout pas le besoin du worker de charger des assemblies ciblant une version supérieure. ## Conséquences ### Positives -* Tout consommateur sur **.NET 8 ou plus récent** peut exécuter `fce`, pas - seulement ceux sur le runtime le plus récent. -* **Un seul** artefact d'outillage livré ; aucune matrice de TFM par version à - maintenir. -* Le floor de l'outillage et le floor de l'analyzer énoncent le **même** minimum - (.NET 8), de sorte que l'histoire du support tient en une seule phrase. -* Vérifié de bout en bout : un `fce` `net8.0` documente un assembly cible - **`net10`** sur une machine qui n'a **que** le runtime .NET 10 — `fce` monte - 8→10 (`Major`) et le worker se lie à la plus haute majeure (`LatestMajor`) - pour charger la cible net10. -* Gardé en CI sur toute la plage : `build-test` exécute la suite sur le dernier - .NET publié (10) ; le job `floor` exécute l'outillage livré sur le runtime - .NET 8 ; et `canary.yml` l'exécute sur la prochaine preview .NET (voir - Justification, et la [référence du workflow `ci`](../workflows/ci.fr.md)). -* Aucun remaniement de code dû au mouvement des versions .NET ; tout au plus un - bump de TFM d'une ligne environ une fois tous les deux ans. +* Les consommateurs utilisant la plus ancienne LTS prise en charge ou un runtime plus récent peuvent exécuter l'outillage. +* Le dépôt livre un seul artefact d'outillage plutôt qu'une matrice par version. +* La compatibilité d'exécution est vérifiée sur le plancher et surveillée avant les nouvelles versions de .NET. ### Négatives -* `fce` ne peut pas s'exécuter sur une machine dont le runtime le plus récent - est antérieur à .NET 8 (par exemple un .NET 6/7 en EOL, ou uniquement - .NET Framework). Accepté : ces consommateurs **utilisent** toujours la - bibliothèque `netstandard2.0` dans leur application ; exécuter un *outil* de - dev/CI sur un runtime actuellement supporté est un prérequis raisonnable (un - SDK .NET moderne est déjà présent partout où l'on compile du .NET moderne). +* L'outillage ne peut pas s'exécuter sur des runtimes antérieurs au plancher LTS choisi. +* Les politiques de sélection du runtime et les vérifications de compatibilité dédiées doivent être maintenues. ### Risques -* `LatestMajor` sur le worker va, sur une machine où une **preview** de la - prochaine majeure est installée, se lier à cette preview. Ce n'est un risque - que pour les machines qui optent pour les previews, et `canary.yml` est - précisément l'alerte précoce que cette liaison fonctionne toujours avant que - cette majeure ne sorte. -* Une régression de roll-forward contre une majeure pas encore publiée est - attrapée par le canary hebdomadaire, pas par une barrière de pull request — - par conception, puisqu'une preview peut être non publiée ou instable. +* Un futur runtime pourrait modifier le roll-forward ou casser l'outillage. Mesure : exercer le plancher actuel en CI et le prochain runtime via le workflow canary. +* La déclaration de support pourrait devenir incohérente lorsque la LTS plancher arrive en fin de support. Mesure : remplacer cet ADR et mettre à jour ensemble la documentation de support de l'analyseur et de l'outillage. ## Actions de suivi -* **Quand le floor LTS atteint son EOL** (.NET 8 → 2026-11-10 ; hygiène plutôt - que fonction — le roll-forward maintient le build net8 fonctionnel sur les - runtimes plus récents après l'EOL — selon une cadence à peu près biennale) : - 1. Changer `` de `net8.0` vers le nouveau floor dans les trois - csprojs de l'outillage : `FirstClassErrors.Cli`, `FirstClassErrors.GenDoc`, - `FirstClassErrors.GenDoc.Worker`. Laisser les réglages `RollForward` tels - quels. - 2. Monter le nouveau floor dans le `` de l'exemple `Usage` - (afin que le job floor de la CI ait toujours une cible sur le nouveau floor) - et dans le job `floor` de `ci.yml` (`dotnet-version` `8.0.x` → le runtime du - nouveau floor). - 3. Mettre à jour la note de runtime dans `FirstClassErrors.Cli/README.nuget.md`. - 4. Remplacer cet ADR (nouveau floor, nouveau runtime minimum). - 5. Optionnellement, retirer les overrides `latest` - si le C# par défaut du nouveau floor est déjà la version que vous voulez. - - Garder ceci aligné avec le floor de l'analyzer (ADR-0001) maintient vrai - l'énoncé de support unique « .NET N et au-delà » du produit. -* **Quand la majeure preview du canary atteint la GA :** `canary.yml` épingle la - majeure preview qu'il cible (`dotnet-version: 11.0.x`, qualité `preview`) ; - montez-la à la suivante (`12.0.x`, …) pour que le canary continue de regarder - une version en avant. Rien ne casse si vous oubliez : `build-test` récupère la - majeure nouvellement publiée comme « latest », et le canary cesse simplement de - trouver une preview plus récente que le SDK de build et termine ses exécutions - en neutre jusqu'au bump. +* Remplacer cet ADR lorsque le plancher du runtime de l'outillage change. +* Maintenir le canary sur la prochaine version de .NET. ## Références -* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) — le floor Roslyn de - l'analyzer, le pendant build-time de cette décision run-time. -* [référence du workflow `ci`](../workflows/ci.fr.md) — le job `floor`, - structurellement. -* `FirstClassErrors.GenDoc.Worker/Program.cs` — l'appel `Assembly.LoadFrom` - derrière la contrainte de roll-forward du worker. +* [Référence d'implémentation des ADR — Plancher d'exécution des outils](../specifications/adr-implementation-reference.fr.md#plancher-dexécution-des-outils) +* [Référence du workflow `ci`](../workflows/ci.fr.md) +* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) — la décision correspondante pour l'hôte de l'analyseur. +* [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.fr.md) — raffine le plancher .NET Framework de la bibliothèque et remplace la mention incidente de 4.6.1 auparavant présente dans cet ADR. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From fc86f3f6b59218a05b680d13e62d59b2ba5c595c Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:47:08 +0200 Subject: [PATCH 10/33] docs: clarify the ADR review decision and its enforcement boundary --- ...every-pull-request-against-the-adr-base.md | 125 +++++------------- 1 file changed, 33 insertions(+), 92 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md index 72232225..da4989f1 100644 --- a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md +++ b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md @@ -8,129 +8,70 @@ ## Context -The repository records significant decisions as ADRs under `doc/handwritten/for-maintainers/adr/`, but -nothing confronts a change with that base at the moment the change is made. A pull -request is where new decisions enter the codebase: it can embark a decision that is -never recorded, replace a decision an existing ADR holds without saying so, or -contradict an accepted ADR unnoticed. - -Hard, mechanical invariants are already guarded elsewhere: the value-object `class` -rule, the analyzer's Roslyn floor (ADR-0001), and the tooling runtime floor -(ADR-0002) are enforced by unit tests and CI jobs that fail deterministically. What -no test expresses is the softer question a reader of a diff must still ask: is there -an architectural decision here, and does it fit what has already been decided? - -The repository's work is largely produced through Claude Code coding sessions, which -load `CLAUDE.md` (and, when directed, `AGENTS.md`) as instructions and hold, -in-session, the full diff, the ADR base, and the reasoning that produced the change. -It is also open to contributors who do not use Claude Code. A GitHub Actions workflow -can run a model on manual dispatch, as the `changelog` workflow already does. Such -a model call has a per-request cost, and the model is referenced by a floating alias, -so its verdict is not reproducible. - -The maintainer (`Reefact`) is the sole authority who merges a pull request and who -accepts an ADR; no agent merges, and an ADR is immutable once accepted (a decision is -revisited by a superseding ADR). +Pull requests are where new architectural decisions enter the repository. A change can introduce an unrecorded decision, replace an existing one without acknowledgement, or conflict with an accepted ADR. -## Decision - -Every pull request is checked against the ADR base as an advisory, non-blocking -recommendation — automatically within a Claude Code coding session and on manual -dispatch for contributors without Claude Code — with an agent drafting any ADR as -`Proposed` and the maintainer alone accepting, superseding, or deprecating it. +Mechanical invariants are already enforced by tests and CI. The remaining question — whether a diff contains or conflicts with an architectural decision — requires judgement and context rather than a deterministic rule. -## Rationale +The maintainer is the sole authority who merges pull requests and changes ADR statuses. Agents may analyse changes and draft proposed ADRs, but they do not accept, supersede, deprecate, or merge them. -The check belongs at the pull request because that is where decisions enter the -codebase; asking "should this be recorded?" while the context is fresh is what the -ADR base needs and does not yet get. +The repository supports both agent-assisted work and contributions that do not use an in-session agent. A model-based check is advisory and non-reproducible, so it must not become an autonomous merge gate. -It is advisory, never blocking, because the decisions it surfaces are matters of -judgement, not conditions a machine settles: the hard invariants that *can* be -settled mechanically are already gated by tests and CI, and gating a merge on a -model's opinion would contradict the rule that the maintainer alone merges. +## Decision -The automatic path runs inside a Claude Code session because the agent there is the -best-placed checker — it already holds the diff, the ADR base, and the reason the -change was made, so the check adds little to work the session is already doing, with -more context than any separate call could carry. +Every pull request is subject to an advisory review against the accepted ADR base, performed in-session when an agent owns the change or explicitly invoked by a contributor otherwise, with agents limited to drafting `Proposed` ADRs and the maintainer retaining sole decision authority. -The manual workflow exists so a contributor without Claude Code is still covered; -keeping it manual — like the sibling `changelog` workflow — gives that coverage -without turning a non-reproducible LLM verdict into an autonomous gate on every pull -request. +## Rationale -An agent drafts and proposes; it never sets an ADR's status. This keeps the maintainer -as the decision authority, consistent with "no agent merges" and with ADR immutability. +The review belongs at pull-request time because that is when the implementation context is freshest and when a decision can still be recorded or challenged before merge. -## Alternatives Considered +The review remains advisory because architectural significance is a judgement call. Deterministic invariants should continue to be enforced mechanically, while the maintainer remains responsible for accepting the recommendation and for every status transition. -### An automatic per-pull-request model check in CI +Using the in-session agent when available avoids a lower-context duplicate analysis. An explicit fallback for other contributors preserves accessibility without turning a non-deterministic model verdict into an automatic gate. -Considered because it would cover every pull request — human- and agent-authored, -Claude Code or not — deterministically and without anyone remembering to run it. +The current agent instructions, checklist, and workflow mechanics are documented in `AGENTS.md`, `CLAUDE.md`, the [ADR implementation reference](../specifications/adr-implementation-reference.md#adr-pull-request-check), and the [`adr-check` workflow reference](../workflows/adr-check.en.md). -Rejected because an autonomous model call on every pull request carries a per-request -cost, introduces a non-deterministic check on a near-mandatory surface, and duplicates -— with less context — what a Claude Code session already performs; the coverage it -would add is met instead by the session check plus the manual dispatch. +## Alternatives Considered -### Encode each ADR's invariant in a machine-checkable field +### Run an automatic model check on every pull request -Considered because a crisp, declared invariant would make conflict detection more -reliable than reasoning over prose. +Considered because it would maximize nominal coverage. Rejected because it would add cost, duplicate higher-context in-session analysis, and place a non-deterministic judgement on a near-mandatory CI surface. -Rejected because the hard invariants that lend themselves to mechanical checking are -already enforced by tests and CI, which do it better and deterministically; and adding -a specification field to an ADR contradicts the principle that an ADR is a decision -record, not a specification, eroding the human-readability that is the point of the -format. +### Encode each ADR as a machine-checkable invariant -### Rely on memory, with no check +Considered because deterministic checks are reliable. Rejected because only a subset of architectural decisions can be expressed mechanically; those that can already belong in tests or CI, while the ADR itself must remain a human decision record. -Considered because it is the zero-effort status quo. +### Rely on memory -Rejected because it is exactly the gap the ADR base exists to close: decisions embarked -in a pull request then go unrecorded, silently supersede an earlier one, or contradict -an accepted ADR. +Considered because it requires no process. Rejected because it leaves exactly the gap the ADR corpus is intended to close. ## Consequences ### Positive -* The question "is there a decision to record?" is asked on every pull request, while - the context that produced the change is still fresh. -* Drafting is cheap: the in-session agent already has everything it needs. -* Nothing blocks a merge; the maintainer keeps sole authority over ADR status. -* Contributors without Claude Code have a first-class fallback. +* Architectural significance is considered before merge while the reasoning is still available. +* Agents can draft records cheaply without acquiring decision authority. +* Contributors without an in-session agent have an explicit fallback. +* No model opinion blocks a maintainer from merging. ### Negative -* The in-session check is best-effort: it is guidance the agent follows, not a hard - gate. -* Coverage of a pull request opened without Claude Code depends on someone dispatching - the workflow. -* The advisory verdict is non-deterministic — it uses a floating model alias — so it is - not reproducible. +* Coverage is procedural rather than mechanically guaranteed. +* The review is non-deterministic and may produce false positives or omissions. +* A contributor can forget to invoke the fallback review. ### Risks -* An agent skips the in-session check. Mitigation: the essentials are in `CLAUDE.md` - (reliably loaded), a checklist item sits on every pull request, and the manual - workflow is an independent path. -* False alarms train the team to ignore the check. Mitigation: the prompt is biased - hard toward silence on routine changes. +* The phrase "every pull request" could be interpreted as an automated guarantee. Mitigation: this ADR defines an obligation of process; the current workflow is manually invoked and does not itself prove universal execution. +* Repeated low-value findings could cause the review to be ignored. Mitigation: keep prompts biased toward silence on routine implementation changes. ## Follow-up Actions -* None blocking. If the in-session guidance proves unreliable in practice, add a - narrowly scoped, non-blocking Claude Code hook that runs the same check — not built - pre-emptively. +* Revisit automated enforcement only if procedural coverage proves insufficient, and keep any future model-based check advisory unless a separate decision changes that rule. ## References -* `AGENTS.md` — "Architecture decisions" (the agent procedure). -* `CLAUDE.md` — the inlined per-session essentials. -* [`adr-check` workflow reference](../workflows/adr-check.en.md). -* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md), [ADR-0002](0002-floor-the-tooling-runtime.md) - — examples of the hard invariants this check deliberately leaves to tests and CI. +* `AGENTS.md` — the agent procedure and status authority. +* `CLAUDE.md` — the in-session guidance. +* [ADR implementation reference — ADR pull-request check](../specifications/adr-implementation-reference.md#adr-pull-request-check) +* [`adr-check` workflow reference](../workflows/adr-check.en.md) +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From b79358c638d3e746d2b2efb4e3910dd307c521bb Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:47:34 +0200 Subject: [PATCH 11/33] docs: translate ADR-0004's editorial rewrite to French --- ...ry-pull-request-against-the-adr-base.fr.md | 138 +++++------------- 1 file changed, 33 insertions(+), 105 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md index c834d512..9521804e 100644 --- a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md +++ b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md @@ -8,142 +8,70 @@ ## Contexte -Le dépôt consigne les décisions importantes sous forme d'ADR dans `doc/handwritten/for-maintainers/adr/`, -mais rien ne confronte un changement à cette base au moment où le changement est -réalisé. C'est dans une pull request que les nouvelles décisions entrent dans la base -de code : elle peut embarquer une décision qui n'est jamais consignée, remplacer une -décision que porte un ADR existant sans le dire, ou contredire un ADR accepté sans que -personne ne le remarque. - -Les invariants durs et mécaniques sont déjà gardés ailleurs : la règle `class` des -value objects, le floor Roslyn de l'analyzer (ADR-0001) et le floor du runtime de -l'outillage (ADR-0002) sont garantis par des tests unitaires et des jobs CI qui -échouent de façon déterministe. Ce qu'aucun test n'exprime, c'est la question plus -subtile que le lecteur d'un diff doit malgré tout se poser : y a-t-il ici une décision -d'architecture, et s'accorde-t-elle avec ce qui a déjà été décidé ? - -Le travail sur le dépôt est en grande partie produit au cours de sessions de codage -Claude Code, qui chargent `CLAUDE.md` (et, lorsqu'on le leur demande, `AGENTS.md`) -comme instructions et détiennent, au sein de la session, l'intégralité du diff, la base -d'ADR et le raisonnement qui a produit le changement. Il est aussi ouvert à des -contributeurs qui n'utilisent pas Claude Code. Un workflow GitHub Actions peut exécuter -un modèle sur déclenchement manuel, comme le fait déjà le workflow `changelog`. Un tel -appel de modèle a un coût par requête, et le modèle est référencé par un alias flottant, -de sorte que son verdict n'est pas reproductible. - -Le mainteneur (`Reefact`) est la seule autorité qui merge une pull request et qui -accepte un ADR ; aucun agent ne merge, et un ADR est immuable une fois accepté (une -décision est réexaminée au moyen d'un ADR de remplacement). +Les pull requests sont le point d'entrée des nouvelles décisions architecturales dans le dépôt. Une modification peut introduire une décision non enregistrée, remplacer une décision existante sans le signaler ou contredire un ADR accepté. -## Décision - -Chaque pull request est contrôlée au regard de la base d'ADR sous la forme d'une -recommandation consultative et non bloquante — automatiquement au sein d'une session de -codage Claude Code et sur déclenchement manuel pour les contributeurs sans Claude Code -— un agent rédigeant tout ADR comme `Proposé` et le mainteneur étant seul à l'accepter, -le remplacer ou le déprécier. +Les invariants mécaniques sont déjà protégés par les tests et la CI. La question restante — savoir si un diff contient ou contredit une décision architecturale — exige du jugement et du contexte plutôt qu'une règle déterministe. -## Justification +Le mainteneur est la seule autorité qui fusionne les pull requests et modifie les statuts des ADR. Les agents peuvent analyser les changements et rédiger des ADR proposés, mais ils ne les acceptent, ne les remplacent, ne les déprécient ni ne les fusionnent. -Le contrôle a sa place au niveau de la pull request parce que c'est là que les décisions -entrent dans la base de code ; se demander « faut-il consigner ceci ? » pendant que le -contexte est encore frais, c'est ce dont la base d'ADR a besoin et qu'elle n'obtient pas -encore. +Le dépôt accueille à la fois des travaux assistés par agent et des contributions sans agent en session. Une vérification fondée sur un modèle est consultative et non reproductible ; elle ne doit donc pas devenir un gate autonome de fusion. -Il est consultatif, jamais bloquant, parce que les décisions qu'il fait émerger relèvent -du jugement, et non de conditions qu'une machine tranche : les invariants durs qui -*peuvent* être tranchés mécaniquement sont déjà verrouillés par les tests et la CI, et -conditionner un merge à l'opinion d'un modèle contredirait la règle selon laquelle le -mainteneur seul merge. +## Décision -La voie automatique s'exécute au sein d'une session Claude Code parce que l'agent qui -s'y trouve est le mieux placé pour effectuer le contrôle — il détient déjà le diff, la -base d'ADR et la raison pour laquelle le changement a été fait, de sorte que le contrôle -ajoute peu au travail que la session accomplit déjà, avec plus de contexte que n'en -pourrait porter aucun appel séparé. +Chaque pull request fait l'objet d'une revue consultative au regard de la base d'ADR acceptés, réalisée en session lorsqu'un agent porte la modification ou invoquée explicitement par le contributeur dans les autres cas, les agents étant limités à la rédaction d'ADR `Proposé` et le mainteneur conservant seul l'autorité de décision. -Le workflow manuel existe pour qu'un contributeur sans Claude Code reste couvert ; le -garder manuel — comme le workflow jumeau `changelog` — apporte cette couverture sans -transformer un verdict de LLM non reproductible en une barrière autonome sur chaque pull -request. +## Justification -Un agent rédige et propose ; il ne fixe jamais le statut d'un ADR. Cela maintient le -mainteneur comme autorité de décision, en cohérence avec « aucun agent ne merge » et -avec l'immuabilité des ADR. +La revue doit intervenir au moment de la pull request, lorsque le contexte de l'implémentation est encore disponible et qu'une décision peut encore être enregistrée ou contestée avant fusion. -## Alternatives envisagées +Elle reste consultative, car la portée architecturale relève du jugement. Les invariants déterministes doivent continuer à être imposés mécaniquement, tandis que le mainteneur reste responsable de l'acceptation de la recommandation et de toute transition de statut. -### Un contrôle automatique par modèle sur chaque pull request dans la CI +Utiliser l'agent déjà présent dans la session évite une seconde analyse moins contextualisée. Un mécanisme explicite pour les autres contributeurs préserve l'accessibilité sans transformer l'avis non déterministe d'un modèle en gate automatique. -Envisagé parce qu'il couvrirait chaque pull request — rédigée par un humain ou par un -agent, avec Claude Code ou non — de façon déterministe et sans que personne ait à penser -à le lancer. +Les instructions d'agent, la checklist et les mécanismes de workflow actuels sont documentés dans `AGENTS.md`, `CLAUDE.md`, la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#vérification-adr-des-pull-requests) et la [référence du workflow `adr-check`](../workflows/adr-check.fr.md). -Rejeté parce qu'un appel de modèle autonome sur chaque pull request entraîne un coût par -requête, introduit un contrôle non déterministe sur une surface quasi obligatoire, et -duplique — avec moins de contexte — ce qu'une session Claude Code accomplit déjà ; la -couverture qu'il apporterait est assurée à la place par le contrôle en session, complété -par le déclenchement manuel. +## Alternatives envisagées -### Encoder l'invariant de chaque ADR dans un champ vérifiable par machine +### Exécuter automatiquement une vérification par modèle sur chaque pull request -Envisagé parce qu'un invariant net et déclaré rendrait la détection de conflits plus -fiable qu'un raisonnement sur de la prose. +Envisagé car cela maximiserait la couverture apparente. Rejeté parce que cela ajouterait un coût, dupliquerait une analyse en session mieux contextualisée et placerait un jugement non déterministe sur une surface de CI presque obligatoire. -Rejeté parce que les invariants durs qui se prêtent à une vérification mécanique sont -déjà garantis par les tests et la CI, qui le font mieux et de façon déterministe ; et -ajouter un champ de spécification à un ADR contredit le principe selon lequel un ADR est -un enregistrement de décision, non une spécification, ce qui éroderait la lisibilité par -un humain qui fait tout l'intérêt du format. +### Encoder chaque ADR sous forme d'invariant vérifiable mécaniquement -### S'en remettre à la mémoire, sans aucun contrôle +Envisagé car les vérifications déterministes sont fiables. Rejeté parce qu'une partie seulement des décisions architecturales peut être exprimée mécaniquement ; celles qui le peuvent appartiennent déjà aux tests ou à la CI, tandis que l'ADR doit rester un relevé de décision humain. -Envisagé parce que c'est le statu quo sans effort. +### S'appuyer sur la mémoire -Rejeté parce que c'est précisément la faille que la base d'ADR existe pour combler : des -décisions embarquées dans une pull request restent alors non consignées, en remplacent -silencieusement une précédente, ou contredisent un ADR accepté. +Envisagé car cela ne nécessite aucun processus. Rejeté parce que cela laisse précisément ouverte la faille que le corpus d'ADR doit combler. ## Conséquences ### Positives -* La question « y a-t-il une décision à consigner ? » est posée sur chaque pull request, - pendant que le contexte qui a produit le changement est encore frais. -* La rédaction est peu coûteuse : l'agent en session dispose déjà de tout ce dont il a - besoin. -* Rien ne bloque un merge ; le mainteneur conserve l'autorité exclusive sur le statut des - ADR. -* Les contributeurs sans Claude Code disposent d'un recours de première classe. +* La portée architecturale est examinée avant fusion tant que le raisonnement est encore disponible. +* Les agents peuvent rédiger les enregistrements à faible coût sans acquérir l'autorité de décision. +* Les contributeurs sans agent en session disposent d'un mécanisme explicite. +* Aucun avis de modèle n'empêche le mainteneur de fusionner. ### Négatives -* Le contrôle en session est fait au mieux : c'est une consigne que l'agent suit, non une - barrière dure. -* La couverture d'une pull request ouverte sans Claude Code dépend du fait que quelqu'un - déclenche le workflow. -* Le verdict consultatif est non déterministe — il utilise un alias de modèle flottant — - et n'est donc pas reproductible. +* La couverture relève du processus et n'est pas garantie mécaniquement. +* La revue est non déterministe et peut produire des faux positifs ou des omissions. +* Un contributeur peut oublier d'invoquer la revue de secours. ### Risques -* Un agent saute le contrôle en session. Atténuation : l'essentiel figure dans `CLAUDE.md` - (chargé de façon fiable), un élément de checklist est présent sur chaque pull request, - et le workflow manuel constitue une voie indépendante. -* Les fausses alertes habituent l'équipe à ignorer le contrôle. Atténuation : le prompt - est fortement orienté vers le silence sur les changements de routine. +* L'expression « chaque pull request » pourrait être comprise comme une garantie automatisée. Mesure : cet ADR définit une obligation de processus ; le workflow actuel est déclenché manuellement et ne prouve pas à lui seul une exécution universelle. +* Des résultats répétés sans valeur pourraient conduire à ignorer la revue. Mesure : conserver des prompts fortement orientés vers le silence pour les changements d'implémentation ordinaires. ## Actions de suivi -* Aucune qui soit bloquante. Si la consigne en session s'avère peu fiable en pratique, - ajouter un hook Claude Code au périmètre étroit et non bloquant qui exécute le même - contrôle — à ne pas construire de manière préventive. +* Ne réexaminer une automatisation plus forte que si la couverture procédurale se révèle insuffisante, et conserver tout futur contrôle par modèle comme consultatif sauf décision distincte contraire. ## Références -* `AGENTS.md` — « Architecture decisions » (la procédure de l'agent). -* `CLAUDE.md` — l'essentiel par session, inséré en ligne. -* [référence du workflow `adr-check`](../workflows/adr-check.fr.md). -* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md), [ADR-0002](0002-floor-the-tooling-runtime.fr.md) - — des exemples des invariants durs que ce contrôle laisse délibérément aux tests et à - la CI. +* `AGENTS.md` — la procédure des agents et l'autorité sur les statuts. +* `CLAUDE.md` — les instructions en session. +* [Référence d'implémentation des ADR — Vérification ADR des pull requests](../specifications/adr-implementation-reference.fr.md#vérification-adr-des-pull-requests) +* [Référence du workflow `adr-check`](../workflows/adr-check.fr.md) +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From f1c858e01a0de0fd25991f88e434136c8a939e40 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:48:30 +0200 Subject: [PATCH 12/33] docs: accept and simplify the nullable value-type binder ADR --- ...s-through-a-struct-constrained-overload.md | 129 +++++------------- 1 file changed, 35 insertions(+), 94 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md index c911ce54..be58670c 100644 --- a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md +++ b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md @@ -2,133 +2,74 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md) -**Status:** Proposed -**Date:** 2026-07-16 +**Status:** Accepted +**Date:** 2026-07-19 **Decision Makers:** Reefact ## Context -* The request binder selects each DTO property with `SimpleProperty(r => r.X)` or - `ListOfSimpleProperties(r => r.X)`, then a converter binds it — a value-object - factory `Func>`, typically a method group such as - `EmailAddress.Parse`. -* The original selector is generic over `TArgument`, with an unconstrained - `Expression>` parameter. -* When the DTO property is a nullable value type (`int?`), the unconstrained selector - infers `TArgument = Nullable`, because for an unconstrained type parameter the - `?` is a no-op annotation on value types. The converter stage then expects - `Func, Outcome>`, so a method group over the underlying type - (`int -> Outcome`) does not match and the call fails to compile with **CS0411**. -* A non-nullable value-type property is already rejected at bind time (review finding - #4, shipped in #141): such a property must be declared nullable so a missing - argument is distinguishable from a legitimately-sent default. -* C# does not treat two methods that differ only by a `class` versus `struct` - constraint as distinct signatures — a constraint is not part of the signature - (CS0111). -* Under a `where TArgument : struct` constraint, `TArgument?` denotes the constructed - type `Nullable`. That is a different parameter type from the unconstrained - selector's bare `TArgument`, and — being a constructed type — is structurally more - specific for overload resolution. -* A `Nullable` list element can independently be `null`; a converter over - the non-nullable underlying type cannot represent that element, and the reference - list converter dereferences elements without unwrapping a `Nullable`. -* The library is pre-release, unpublished on NuGet with no external consumers. Additive - overloads settled before the v1 freeze cannot shift inference at consumer call sites - that do not yet exist; the same overloads added after consumers write value-type - bindings could change resolution at those sites. +The request binder lets a DTO property be selected and then converted by a value-object factory, commonly supplied as a method group. + +For nullable value-type properties, the original unconstrained generic selector inferred `Nullable` as the converter input. A factory over the underlying value type therefore failed to bind as a method group even though the DTO correctly used a nullable property to distinguish absence from a legitimate default value. + +C# cannot overload methods solely by changing generic constraints, but a struct-constrained selector can expose `Nullable` as a distinct parameter shape while keeping the converter on the underlying non-nullable type. + +Lists of nullable value types also require explicit null-element handling before conversion. ## Decision -The request binder binds a nullable value-type DTO property through a dedicated -`where TArgument : struct` selector overload whose selector carries -`Nullable` and whose converter runs over the underlying non-nullable type. +The request binder binds nullable value-type DTO properties and list elements through dedicated `where TArgument : struct` selector paths whose converters operate on the underlying non-nullable value type. ## Rationale -* The struct-constrained overload's `Nullable` parameter is a genuinely - different — and structurally more specific — type than the unconstrained overload's - bare `TArgument`, so the two coexist without CS0111 and the value-type overload wins - for a nullable-value-type property, while reference and string properties keep - resolving to the unconstrained overload. The CS0411 failure is removed at the - selector rather than pushed onto the consumer as an adapter lambda. -* Surfacing the underlying non-nullable type (`int`, not `int?`) lets a value-object - factory bind as a method group exactly as it does for a reference property, keeping - one fluent ergonomic across both property kinds: the property stays declared nullable - so absence remains observable, and the underlying type is what the converter - meaningfully operates on. -* A dedicated list converter is required rather than a reuse, because a `Nullable` - element needs its own null handling — a `null` element is a missing argument recorded - under its indexed path — and an unwrap before conversion, neither of which the - reference converter performs. -* Deciding before the v1 freeze settles the API shape while it is still free: the - overloads are additive now, with no call sites to disturb, whereas deferring them - past the freeze would make the same addition a source-breaking change to consumers' - value-type bindings. +The dedicated path restores the same method-group ergonomics that reference-type properties already have while preserving the semantic distinction between a missing argument and a supplied default value. -## Alternatives Considered +The overload belongs at the selector boundary because that is where C# type inference otherwise chooses `Nullable` and produces an opaque compile-time failure for consumers. + +Nullable list elements cannot safely reuse the reference-element implementation because they require their own absence handling and unwrapping before conversion. -### Require consumers to pass an adapter lambda +Settling the additive API before the first stable release avoids introducing a later source-compatibility hazard through overload-resolution changes. -Considered because it needs no new API: `AsRequired(v => PositiveInt.From(v))` binds -where the bare method group does not. +Exact overload signatures, converter types, null-element behavior, and examples are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) and the Request Binder user documentation. -Rejected because it silently degrades the method-group ergonomics for the common -nullable-value-type case, surfaces a CS0411 compile error with no obvious cause, and -duplicates at every call site the unwrap the binder can perform once. +## Alternatives Considered -### A single selector unifying reference and value types +### Require adapter lambdas at each call site -Considered because one overload is the smallest surface. +Considered because it requires no new public API. Rejected because it exposes an unintuitive inference failure to consumers and duplicates binder-owned unwrapping logic throughout application code. -Rejected because C# cannot express it: an unconstrained `TArgument?` cannot both infer -the underlying type for a value-type property and stay a reference for a reference-type -property, and a constraint cannot be varied within one method. +### Use one selector for both reference and value types -### Reuse the reference list converter for value-type lists +Considered because it minimizes the surface. Rejected because C# cannot infer the underlying value type from an unconstrained nullable annotation while preserving reference-type behavior. -Considered because the binding logic is otherwise identical. +### Reuse the reference-list converter -Rejected because a `Nullable` element and a reference element need different null -handling, and the value-type path must unwrap each present element before conversion; -folding both into one converter would obscure that a `null` element is a recorded -missing argument. +Considered because the conversion flow is otherwise similar. Rejected because nullable value-type elements require distinct missing-element handling and unwrapping. ## Consequences ### Positive -* A nullable-value-type DTO property (`int?`, `bool?`, ...) and lists of them bind via - a method group over the underlying type, with the same ergonomics as reference - properties. -* The fix is additive and settled before the v1 freeze, so it never becomes a - source-breaking change to a consumer's existing value-type binding. -* Reference and string properties are unaffected: they keep resolving to the original - overload. +* Nullable value-type properties bind with the same method-group ergonomics as reference properties. +* Absence remains observable while converters receive the meaningful underlying value. +* The public shape is settled before the stable API freeze. ### Negative -* Two more public selector overloads and one more public converter type to document and - maintain, doubling the selector surface of `SimpleProperty` and - `ListOfSimpleProperties`. -* The mechanism — a `struct` constraint turning `TArgument?` into a more-specific - `Nullable` — is subtle; a maintainer unfamiliar with the overload- - resolution rule may not immediately see why the two selectors coexist. +* The binder exposes additional selector and converter surface. +* The overload-resolution mechanism is subtle and requires regression tests. ### Risks -* A future third selector shape could interact with the two overloads in ways that - reintroduce ambiguity; mitigated by this ADR and by regression tests that pin - reference and string resolution to the original overload. +* Future selector shapes could reintroduce ambiguity. Mitigation: keep targeted compile-time regression coverage for reference, string, scalar value, and list cases. -### Follow-up Actions +## Follow-up Actions -* Keep the RequestBinder guide (EN and French) in sync with the value-type binding - ergonomics. +* Keep the bilingual Request Binder documentation aligned with the accepted behavior. ## References -* ADR-0007 — name the binder terminals New and Create, the sibling public-API decision - on the same binder. -* Pull requests #126 and #141 — the request binder feature and the non-nullable - value-type guard this decision complements. -* Issue #144 — the CS0411 finding this decision resolves. +* [ADR implementation reference — Request Binder implementation contracts](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) +* [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) +* Issue #144 and pull requests #126 and #141. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From ed51ef07ffac9d4e28eaedeb147e35aeed5d7c18 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:48:53 +0200 Subject: [PATCH 13/33] docs: translate ADR-0008's editorial rewrite to French --- ...hrough-a-struct-constrained-overload.fr.md | 135 +++++------------- 1 file changed, 35 insertions(+), 100 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md index e65b7cee..84f5fcc1 100644 --- a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md +++ b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md @@ -2,139 +2,74 @@ 🌍 🇬🇧 [English](0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md) · 🇫🇷 Français (ce fichier) -**Statut :** Proposé -**Date :** 2026-07-16 +**Statut :** Accepté +**Date :** 2026-07-19 **Décideurs :** Reefact ## Contexte -* Le request binder sélectionne chaque propriété du DTO avec `SimpleProperty(r => r.X)` ou - `ListOfSimpleProperties(r => r.X)`, puis un convertisseur la lie — une fabrique de value - object `Func>`, typiquement un groupe de méthodes tel que - `EmailAddress.Parse`. -* Le sélecteur d'origine est générique sur `TArgument`, avec un paramètre - `Expression>` non contraint. -* Lorsque la propriété du DTO est un type valeur nullable (`int?`), le sélecteur non - contraint infère `TArgument = Nullable`, car pour un paramètre de type non contraint - le `?` est une annotation sans effet sur les types valeur. L'étape de conversion attend - alors `Func, Outcome>`, si bien qu'un groupe de méthodes sur le type - sous-jacent (`int -> Outcome`) ne correspond pas et l'appel échoue à compiler avec - **CS0411**. -* Une propriété de type valeur non nullable est déjà rejetée au moment du binding (constat - de revue n°4, livré dans #141) : une telle propriété doit être déclarée nullable pour - qu'un argument manquant se distingue d'une valeur par défaut légitimement envoyée. -* C# ne traite pas deux méthodes qui ne diffèrent que par une contrainte `class` versus - `struct` comme des signatures distinctes — une contrainte ne fait pas partie de la - signature (CS0111). -* Sous une contrainte `where TArgument : struct`, `TArgument?` désigne le type construit - `Nullable`. C'est un type de paramètre différent du `TArgument` nu du sélecteur - non contraint et — étant un type construit — il est structurellement plus spécifique pour - la résolution de surcharge. -* Un élément de liste `Nullable` peut indépendamment être `null` ; un - convertisseur sur le type sous-jacent non nullable ne peut pas représenter cet élément, et - le convertisseur de liste de référence déréférence les éléments sans déballer un - `Nullable`. -* La bibliothèque est en pré-version, non publiée sur NuGet et sans consommateurs externes. Des - surcharges additives figées avant le gel de la v1 ne peuvent pas déplacer l'inférence sur - des sites d'appel de consommateurs qui n'existent pas encore ; les mêmes surcharges - ajoutées après que des consommateurs ont écrit des bindings de type valeur pourraient - changer la résolution sur ces sites. +Le Request Binder permet de sélectionner une propriété de DTO puis de la convertir au moyen d'une factory de value object, souvent fournie sous forme de groupe de méthodes. + +Pour les propriétés de type valeur nullable, le sélecteur générique non contraint d'origine inférait `Nullable` comme entrée du convertisseur. Une factory opérant sur le type valeur sous-jacent ne pouvait donc pas être liée comme groupe de méthodes, alors même que le DTO utilisait correctement une propriété nullable pour distinguer l'absence d'une valeur par défaut effectivement fournie. + +C# ne permet pas de surcharger des méthodes en ne changeant que les contraintes génériques, mais un sélecteur contraint à `struct` peut exposer `Nullable` comme forme de paramètre distincte tout en conservant un convertisseur sur le type sous-jacent non nullable. + +Les listes de types valeur nullables exigent également une gestion explicite des éléments `null` avant conversion. ## Décision -Le request binder lie une propriété de DTO de type valeur nullable au travers d'une surcharge -de sélecteur dédiée `where TArgument : struct` dont le sélecteur porte `Nullable` -et dont le convertisseur opère sur le type sous-jacent non nullable. +Le Request Binder lie les propriétés de DTO et les éléments de liste de type valeur nullable au moyen de chemins de sélection dédiés `where TArgument : struct`, dont les convertisseurs opèrent sur le type valeur sous-jacent non nullable. ## Justification -* Le paramètre `Nullable` de la surcharge contrainte à `struct` est un type - véritablement différent — et structurellement plus spécifique — que le `TArgument` nu de la - surcharge non contrainte, si bien que les deux coexistent sans CS0111 et que la surcharge - de type valeur l'emporte pour une propriété de type valeur nullable, tandis que les - propriétés de référence et de type `string` continuent de se résoudre vers la surcharge non - contrainte. L'échec CS0411 est supprimé au niveau du sélecteur plutôt que reporté sur le - consommateur sous la forme d'une lambda d'adaptation. -* Faire remonter le type sous-jacent non nullable (`int`, et non `int?`) permet à une fabrique - de value object de lier sous forme de groupe de méthodes exactement comme elle le fait pour - une propriété de référence, gardant une même ergonomie fluide pour les deux sortes de - propriétés : la propriété reste déclarée nullable pour que l'absence demeure observable, et - le type sous-jacent est ce sur quoi le convertisseur opère concrètement. -* Un convertisseur de liste dédié est requis plutôt qu'une réutilisation, parce qu'un élément - `Nullable` a besoin de sa propre gestion du `null` — un élément `null` est un argument - manquant enregistré sous son chemin indexé — et d'un déballage avant conversion, ni l'un ni - l'autre n'étant réalisés par le convertisseur de référence. -* Décider avant le gel de la v1 fige la forme de l'API tant qu'elle est encore libre : les - surcharges sont additives maintenant, sans aucun site d'appel à perturber, alors que les - différer au-delà du gel ferait de la même addition un changement cassant à la source pour - les bindings de type valeur des consommateurs. +Le chemin dédié rétablit la même ergonomie de groupe de méthodes que pour les propriétés de type référence, tout en préservant la distinction sémantique entre un argument absent et une valeur par défaut fournie. -## Alternatives envisagées +La surcharge doit se situer à la frontière du sélecteur, car c'est là que l'inférence de types de C# choisit sinon `Nullable` et produit pour le consommateur une erreur de compilation peu explicite. + +Les éléments de liste de type valeur nullable ne peuvent pas réutiliser sans risque l'implémentation destinée aux références, puisqu'ils exigent une gestion propre de l'absence et un déballage avant conversion. -### Exiger des consommateurs qu'ils passent une lambda d'adaptation +Stabiliser cette API additive avant la première version stable évite d'introduire plus tard un risque de compatibilité source lié à la résolution des surcharges. -Envisagée parce qu'elle ne nécessite aucune nouvelle API : `AsRequired(v => PositiveInt.From(v))` -lie là où le groupe de méthodes nu ne le fait pas. +Les signatures exactes, les types de convertisseurs, le comportement des éléments `null` et les exemples sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) et la documentation utilisateur du Request Binder. -Rejetée parce qu'elle dégrade silencieusement l'ergonomie du groupe de méthodes pour le cas -courant du type valeur nullable, fait surgir une erreur de compilation CS0411 sans cause -évidente, et duplique à chaque site d'appel le déballage que le binder peut effectuer une -seule fois. +## Alternatives envisagées -### Un sélecteur unique unifiant types référence et types valeur +### Exiger une lambda adaptatrice à chaque appel -Envisagée parce qu'une seule surcharge est la surface la plus réduite. +Envisagé car cela ne nécessite aucune nouvelle API publique. Rejeté parce que cela expose au consommateur une erreur d'inférence peu intuitive et duplique dans le code applicatif une logique de déballage qui appartient au binder. -Rejetée parce que C# ne peut pas l'exprimer : un `TArgument?` non contraint ne peut pas à la -fois inférer le type sous-jacent pour une propriété de type valeur et rester une référence -pour une propriété de type référence, et une contrainte ne peut pas varier au sein d'une même -méthode. +### Utiliser un sélecteur unique pour les types référence et valeur -### Réutiliser le convertisseur de liste de référence pour les listes de types valeur +Envisagé pour minimiser la surface. Rejeté parce que C# ne peut pas inférer le type valeur sous-jacent depuis une annotation nullable non contrainte tout en conservant le comportement des types référence. -Envisagée parce que la logique de binding est par ailleurs identique. +### Réutiliser le convertisseur de liste destiné aux références -Rejetée parce qu'un élément `Nullable` et un élément de référence nécessitent une gestion -du `null` différente, et que le chemin de type valeur doit déballer chaque élément présent -avant conversion ; fondre les deux dans un seul convertisseur masquerait qu'un élément `null` -est un argument manquant enregistré. +Envisagé car le flux de conversion est autrement similaire. Rejeté parce que les éléments de type valeur nullable exigent une gestion distincte des éléments manquants et un déballage. ## Conséquences ### Positives -* Une propriété de DTO de type valeur nullable (`int?`, `bool?`, ...) et les listes de - celles-ci se lient via un groupe de méthodes sur le type sous-jacent, avec la même ergonomie - que les propriétés de référence. -* Le correctif est additif et figé avant le gel de la v1, si bien qu'il ne devient jamais un - changement cassant à la source pour le binding de type valeur existant d'un consommateur. -* Les propriétés de référence et de type `string` ne sont pas affectées : elles continuent de - se résoudre vers la surcharge d'origine. +* Les propriétés de type valeur nullable se lient avec la même ergonomie de groupe de méthodes que les propriétés de type référence. +* L'absence reste observable tandis que les convertisseurs reçoivent la valeur sous-jacente pertinente. +* La forme publique est stabilisée avant le gel de l'API stable. ### Négatives -* Deux surcharges de sélecteur publiques supplémentaires et un type de convertisseur public de - plus à documenter et à maintenir, doublant la surface de sélecteur de `SimpleProperty` et - `ListOfSimpleProperties`. -* Le mécanisme — une contrainte `struct` transformant `TArgument?` en un `Nullable` - plus spécifique — est subtil ; un mainteneur peu familier de la règle de résolution de - surcharge peut ne pas voir immédiatement pourquoi les deux sélecteurs coexistent. +* Le binder expose des sélecteurs et convertisseurs supplémentaires. +* Le mécanisme de résolution des surcharges est subtil et nécessite des tests de régression. ### Risques -* Une future troisième forme de sélecteur pourrait interagir avec les deux surcharges de - manière à réintroduire de l'ambiguïté ; atténué par cette ADR et par des tests de régression - qui épinglent la résolution des types référence et `string` sur la surcharge d'origine. +* De futures formes de sélecteurs pourraient réintroduire une ambiguïté. Mesure : conserver une couverture de compilation ciblée pour les références, les chaînes, les valeurs scalaires et les listes. -### Actions de suivi +## Actions de suivi -* Garder le guide RequestBinder (EN et français) synchronisé avec l'ergonomie de binding de - type valeur. +* Maintenir la documentation bilingue du Request Binder alignée sur le comportement accepté. ## Références -* ADR-0007 — nommer les terminaux du binder New et Create, la décision d'API publique sœur sur - le même binder. -* Pull requests #126 et #141 — la fonctionnalité de request binder et le garde-fou de type - valeur non nullable que cette décision complète. -* Issue #144 — le constat CS0411 que cette décision résout. +* [Référence d'implémentation des ADR — Contrats d'implémentation du Request Binder](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) +* [ADR-0007](0007-name-the-binder-terminals-new-and-create.fr.md) +* Issue #144 et pull requests #126 et #141. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 8f637c1c1d64639f4b365c1536f017926727a86c Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:49:12 +0200 Subject: [PATCH 14/33] docs: separate the GenDoc catalog decision from release mechanics --- ...s-error-catalog-as-a-versioned-contract.md | 121 +++++------------- 1 file changed, 33 insertions(+), 88 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md index 0a3626ba..b988b112 100644 --- a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md +++ b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md @@ -8,123 +8,68 @@ ## Context -ADR-0009 established that `FirstClassErrors.GenDoc` documents its own failures as -first-class errors, giving each a stable `GENDOC_`-prefixed code and a structured -context. Those codes and context keys are emitted to callers at runtime and are -the identities external consumers — CI pipelines, integrators, support — match on. - -GenDoc has no NuGet package of its own: it ships bundled inside the `fce` tool, -released on the `cli` train (`tools/packaging/pack.sh`). A change to GenDoc's own -error catalog — a code renamed or removed, a context key dropped or retyped — -is therefore a change to what the `cli` package emits, indistinguishable, from the -outside, from any other compatibility change of that package. - -The library already ships the mechanics to treat a catalog as a versioned -contract: `fce catalog update` records a baseline snapshot, `fce catalog diff` -compares against it, and the comparison classifies each change as Breaking, -Compatible, or Informational (a removed code or a removed/retyped context key is -Breaking). Until now these were offered to consumers documenting their own errors, -but never applied to GenDoc's own catalog: nothing recorded GenDoc's baseline, and -nothing checked a change to it against the version the `cli` train was about to -publish. The two trains follow semantic versioning, and the repository already -enforces Conventional Commits, but neither mechanism connects a breaking change of -the *error catalog* to the *version number* that ships it. +GenDoc exposes stable first-class error codes and typed context that external consumers can match in CI, integrations, and support tooling. + +GenDoc ships as part of the `fce` command-line package rather than on an independent release train. Removing or changing one of its documented codes or context contracts is therefore a compatibility change in the `cli` package. + +The repository already knows how to snapshot and classify catalog changes, but the tool's own error catalog was not tied to the semantic version published by the release process. ## Decision -A breaking change to GenDoc's own error catalog, measured by `fce catalog diff` -against a committed baseline, requires a major version bump of the `cli` train, -enforced at release time. +A breaking change to GenDoc's own error catalog requires a major version bump of the `cli` release train and is enforced when that release is published. ## Rationale -* **The failure surface is a published contract, so it must be versioned like - one.** ADR-0009 made GenDoc's codes stable identities that consumers depend on; - a stable identity that can silently disappear under a compatible-looking version - bump is not actually stable. Semantic versioning is the promise the `cli` - package already makes, and a removed or renamed code is exactly the kind of - break that promise exists to signal. -* **Enforce at the release, not at the pull request.** A breaking change to an - error catalog is not wrong in itself — an intentional one is precisely what a - major version is for. Only shipping it *silently*, under a version that promises - compatibility, is the failure. Gating pull requests would fight normal - incremental development; gating the release targets the single point where the - compatibility promise is actually made, and leaves day-to-day work unblocked. -* **Compare against the last release, not a moving target.** The baseline advances - only when a `cli` release publishes. Between releases it stays fixed, so the - diff always answers "what changed since the last thing actually shipped" — - which is the question the version number must answer — regardless of how many - pull requests landed in between. -* **Reuse the existing contract mechanics, add no new judgement.** `fce catalog - diff`'s Breaking classification is already defined and tested; this decision - wires it to the release version rather than inventing a second notion of what - "breaking" means for the tool's own errors. +The catalog is a published contract because consumers depend on stable error identities and context. A breaking catalog change must therefore be signalled by the same semantic-versioning promise as any other externally observable break. + +Release time is the correct enforcement point. A breaking change can be legitimate during development; the failure is shipping it under a version that promises compatibility. + +The comparison must remain anchored to the last shipped catalog rather than a moving development snapshot so that the version number answers what changed since the previous release. + +Reusing the existing catalog-diff classification avoids creating a second, competing definition of compatibility. + +The exact baseline location, release workflow, update commands, and recovery procedure are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#gendoc-catalog-compatibility), the workflow reference, and the catalog versioning documentation. ## Alternatives Considered -### Leave it to Conventional Commits and reviewer discipline +### Rely on Conventional Commits and review discipline -Considered because the repository already requires a `!`/`BREAKING CHANGE:` -marker on breaking commits, checked in CI. Rejected because that marker is -authored by hand from the commit's intent, while a catalog break can be an -unintended side effect (a refactor that drops a context key); nothing tied the -marker to a mechanical measurement of the catalog, so a silent break could still -ship under a minor bump. +Considered because the repository already records intended breaking changes. Rejected because an accidental catalog break can occur without the commit author recognizing it, while the generated catalog provides a mechanical measurement. -### Gate the pull request instead of the release +### Gate every pull request -Considered because it surfaces the break earliest. Rejected because a breaking -change is legitimate mid-development as long as the eventual release carries the -major bump; blocking it per pull request would penalize normal iteration and -force premature version decisions, while the release gate catches the same break -at the only moment it actually matters. +Considered because it would surface breaks earlier. Rejected because a breaking catalog change is valid during development as long as the eventual release carries the correct major version. -### Publish GenDoc as its own package on its own train +### Publish GenDoc on an independent release train -Considered because a dedicated train would let the catalog version independently. -Rejected as disproportionate: GenDoc has no standalone consumer (it runs only -inside `fce`), and ADR-0002's tooling model deliberately keeps it bundled; a new -train would add release machinery for no consumer benefit. +Considered because it would give the catalog its own version. Rejected because GenDoc has no standalone consumer and is intentionally shipped inside `fce`; the additional release machinery would provide no corresponding user benefit. ## Consequences ### Positive -* A breaking change to GenDoc's own errors cannot ship under a non-major `cli` - version: the release fails until the version or the change is reconciled. -* The catalog gains a committed baseline and a per-pull-request diff report, so - the pending compatibility impact is visible during review. -* The living documentation the CI regenerates is backed by an explicit, - release-anchored contract rather than a best-effort snapshot. +* A breaking GenDoc catalog change cannot ship under a compatible-looking `cli` version. +* Reviewers can see pending catalog compatibility impact before release. +* The generated catalog becomes an explicit release contract rather than a best-effort snapshot. ### Negative -* Releasing the `cli` train now depends on a committed baseline and a diff step; - a maintainer must understand that accepting a breaking change means bumping the - major version (or reverting), not overriding the gate. -* The baseline is refreshed by a direct push to `main` after a successful - release — one automated write outside the normal pull-request flow, scoped to - that moment. +* The `cli` release now depends on a valid catalog baseline and compatibility check. +* Maintainers must understand that accepting a catalog break requires a major version bump or reversal of the change. ### Risks -* A stale or hand-edited baseline could mis-measure a change. Mitigation: the - baseline is only ever written by `fce catalog update`, run by the release after - a real publish, so it always reflects the last shipped catalog. +* A stale or incorrectly advanced baseline could misclassify a release. Mitigation: keep baseline updates inside the controlled release procedure and document recovery when publication and baseline advancement diverge. ## Follow-up Actions -* None beyond the workflow wiring itself (`gendoc-docs.yml` and the `release.yml` - gate); the mechanism lives in the workflows and the `fce catalog` commands the - reference documentation already covers. +* Maintain the release procedure and recovery path in the workflow reference rather than this ADR. ## References -* ADR-0009 — GenDoc modeling its own failures as first-class errors, the codes - this contract versions. -* ADR-0002 — the tooling runtime model that keeps GenDoc bundled in the `cli` - train rather than shipping standalone. -* Issue [#167](https://github.com/Reefact/first-class-errors/issues/167) — the - request this decision answers. +* [ADR implementation reference — GenDoc catalog compatibility](../specifications/adr-implementation-reference.md#gendoc-catalog-compatibility) * [Catalog Versioning Reference](../../for-users/CatalogVersioningReference.en.md) - — the `fce catalog update`/`diff` mechanics reused here. +* [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.md) +* [ADR-0002](0002-floor-the-tooling-runtime.md) +* Issue #167. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From a537e38220c350e5890aabff285b4a26522d0b8c Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:49:35 +0200 Subject: [PATCH 15/33] docs: translate ADR-0010's editorial rewrite to French --- ...rror-catalog-as-a-versioned-contract.fr.md | 133 +++++------------- 1 file changed, 34 insertions(+), 99 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md index 04e36615..82084ab7 100644 --- a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md +++ b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md @@ -8,133 +8,68 @@ ## Contexte -L'ADR-0009 a établi que `FirstClassErrors.GenDoc` documente ses propres échecs -comme des erreurs de première classe, en donnant à chacun un code stable préfixé -`GENDOC_` et un contexte structuré. Ces codes et ces clés de contexte sont émis -aux appelants à l'exécution et constituent les identités sur lesquelles -s'appuient les consommateurs externes — pipelines CI, intégrateurs, support. - -GenDoc n'a pas de package NuGet propre : il est embarqué dans l'outil `fce`, -publié sur le train `cli` (`tools/packaging/pack.sh`). Un changement du catalogue -d'erreurs propre à GenDoc — un code renommé ou supprimé, une clé de contexte -retirée ou dont le type change — est donc un changement de ce qu'émet le package -`cli`, indiscernable de l'extérieur de tout autre changement de compatibilité de -ce package. - -La bibliothèque livre déjà les mécanismes pour traiter un catalogue comme un -contrat versionné : `fce catalog update` enregistre une baseline, `fce catalog -diff` la compare, et la comparaison classe chaque changement en Cassant, -Compatible ou Informationnel (un code supprimé ou une clé de contexte -supprimée/retypée est Cassant). Jusqu'ici ces commandes étaient offertes aux -consommateurs documentant leurs propres erreurs, mais jamais appliquées au -catalogue de GenDoc lui-même : rien n'enregistrait la baseline de GenDoc, et rien -ne vérifiait un changement au regard de la version que le train `cli` s'apprêtait -à publier. Les deux trains suivent le versionnage sémantique, et le dépôt impose -déjà les Conventional Commits, mais aucun de ces mécanismes ne relie un changement -cassant du *catalogue d'erreurs* au *numéro de version* qui le publie. +GenDoc expose des codes d'erreur first-class stables et un contexte typé que les consommateurs externes peuvent utiliser dans la CI, les intégrations et les outils de support. + +GenDoc est livré dans le package en ligne de commande `fce` plutôt que sur un train de release indépendant. Supprimer ou modifier l'un de ses codes documentés ou de ses contrats de contexte constitue donc une modification de compatibilité du package `cli`. + +Le dépôt sait déjà prendre un instantané d'un catalogue et classifier ses changements, mais le propre catalogue d'erreurs de l'outil n'était pas relié à la version sémantique publiée par le processus de release. ## Décision -Un changement cassant du catalogue d'erreurs propre à GenDoc, mesuré par `fce -catalog diff` contre une baseline committée, exige un incrément de version majeure -du train `cli`, imposé au moment de la publication. +Une modification cassante du propre catalogue d'erreurs de GenDoc exige une version majeure du train de release `cli` et est contrôlée lors de la publication de cette release. ## Justification -* **La surface d'échec est un contrat publié : elle doit être versionnée comme - tel.** L'ADR-0009 a fait des codes de GenDoc des identités stables dont - dépendent les consommateurs ; une identité stable qui peut disparaître en - silence sous un incrément de version d'apparence compatible n'est pas réellement - stable. Le versionnage sémantique est la promesse que le package `cli` fait - déjà, et un code supprimé ou renommé est exactement le genre de rupture que - cette promesse existe pour signaler. -* **Imposer au release, pas à la pull request.** Un changement cassant du - catalogue n'est pas fautif en soi — un changement délibéré est précisément ce à - quoi sert une version majeure. Seule sa publication *silencieuse*, sous une - version qui promet la compatibilité, est le problème. Bloquer les pull requests - entraverait le développement incrémental normal ; imposer au release cible le - seul point où la promesse de compatibilité est réellement faite, et laisse le - travail quotidien libre. -* **Comparer au dernier release, pas à une cible mouvante.** La baseline n'avance - que lorsqu'un release `cli` publie. Entre deux releases elle reste fixe, de - sorte que le diff répond toujours à « qu'est-ce qui a changé depuis la dernière - chose réellement publiée » — la question à laquelle le numéro de version doit - répondre — quel que soit le nombre de pull requests intercalées. -* **Réutiliser les mécanismes de contrat existants, sans nouveau jugement.** La - classification Cassant de `fce catalog diff` est déjà définie et testée ; cette - décision la relie à la version du release plutôt que d'inventer une seconde - notion de ce qu'être « cassant » signifie pour les erreurs propres à l'outil. +Le catalogue est un contrat publié, car les consommateurs dépendent d'identités d'erreur et de contextes stables. Une rupture du catalogue doit donc être signalée par la même promesse de versionnement sémantique que toute autre rupture observable. + +Le moment de la release est le bon point de contrôle. Une rupture peut être légitime pendant le développement ; l'erreur consiste à la publier sous une version qui promet la compatibilité. + +La comparaison doit rester ancrée sur le dernier catalogue effectivement livré plutôt que sur un instantané de développement mobile, afin que le numéro de version réponde à la question de ce qui a changé depuis la release précédente. + +Réutiliser la classification existante du diff de catalogue évite de créer une seconde définition concurrente de la compatibilité. + +L'emplacement exact de la baseline, le workflow de release, les commandes de mise à jour et la procédure de reprise sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#compatibilité-du-catalogue-gendoc), la référence des workflows et la documentation du versionnement des catalogues. ## Alternatives envisagées -### S'en remettre aux Conventional Commits et à la vigilance des relecteurs +### S'appuyer sur les Conventional Commits et la discipline de revue -Envisagée parce que le dépôt exige déjà un marqueur `!`/`BREAKING CHANGE:` sur les -commits cassants, vérifié en CI. Rejetée parce que ce marqueur est rédigé à la -main d'après l'intention du commit, alors qu'une rupture du catalogue peut être un -effet de bord non voulu (un refactor qui supprime une clé de contexte) ; rien ne -reliait le marqueur à une mesure mécanique du catalogue, de sorte qu'une rupture -silencieuse pouvait encore être publiée sous un incrément mineur. +Envisagé car le dépôt enregistre déjà les ruptures intentionnelles. Rejeté parce qu'une rupture accidentelle du catalogue peut survenir sans être identifiée par l'auteur du commit, tandis que le catalogue généré fournit une mesure mécanique. -### Contrôler la pull request plutôt que le release +### Bloquer chaque pull request -Envisagée parce qu'elle fait apparaître la rupture au plus tôt. Rejetée parce -qu'un changement cassant est légitime en cours de développement tant que le -release final porte l'incrément majeur ; le bloquer par pull request pénaliserait -l'itération normale et forcerait des décisions de version prématurées, alors que -le contrôle au release attrape la même rupture au seul moment où elle compte. +Envisagé pour remonter les ruptures plus tôt. Rejeté parce qu'une rupture du catalogue est valide pendant le développement dès lors que la release finale porte la bonne version majeure. -### Publier GenDoc comme son propre package sur son propre train +### Publier GenDoc sur un train de release indépendant -Envisagée parce qu'un train dédié permettrait de versionner le catalogue -indépendamment. Rejetée comme disproportionnée : GenDoc n'a pas de consommateur -autonome (il ne tourne qu'à l'intérieur de `fce`), et le modèle d'outillage de -l'ADR-0002 le garde délibérément embarqué ; un nouveau train ajouterait une -machinerie de release sans bénéfice pour aucun consommateur. +Envisagé pour donner au catalogue sa propre version. Rejeté parce que GenDoc n'a pas de consommateur autonome et est volontairement livré dans `fce` ; la machinerie supplémentaire de release n'apporterait pas de bénéfice utilisateur correspondant. ## Conséquences ### Positives -* Un changement cassant des erreurs propres à GenDoc ne peut plus être publié sous - une version `cli` non majeure : le release échoue jusqu'à ce que la version ou - le changement soit réconcilié. -* Le catalogue gagne une baseline committée et un rapport de diff par pull - request, de sorte que l'impact de compatibilité en attente est visible à la - relecture. -* La documentation vivante que la CI régénère s'appuie sur un contrat explicite, - ancré au release, plutôt que sur un instantané au mieux. +* Une rupture du catalogue GenDoc ne peut pas être livrée sous une version `cli` qui semble compatible. +* Les relecteurs peuvent voir l'impact de compatibilité en attente avant la release. +* Le catalogue généré devient un contrat explicite de release plutôt qu'un instantané au mieux. ### Négatives -* Publier le train `cli` dépend désormais d'une baseline committée et d'une étape - de diff ; un mainteneur doit comprendre qu'accepter un changement cassant - signifie incrémenter la version majeure (ou revenir en arrière), pas contourner - le contrôle. -* La baseline est rafraîchie par un push direct sur `main` après un release - réussi — une écriture automatisée hors du flux normal de pull request, limitée à - ce moment précis. +* La release `cli` dépend désormais d'une baseline de catalogue valide et d'un contrôle de compatibilité. +* Les mainteneurs doivent comprendre qu'accepter une rupture exige une version majeure ou l'abandon de la modification. ### Risques -* Une baseline périmée ou éditée à la main pourrait mal mesurer un changement. - Mitigation : la baseline n'est jamais écrite que par `fce catalog update`, - exécuté par le release après une publication réelle, de sorte qu'elle reflète - toujours le dernier catalogue publié. +* Une baseline obsolète ou avancée incorrectement pourrait mal classifier une release. Mesure : conserver les mises à jour de baseline dans la procédure contrôlée de release et documenter la reprise lorsque la publication et l'avancement de la baseline divergent. ## Actions de suivi -* Aucune au-delà du câblage des workflows eux-mêmes (`gendoc-docs.yml` et le - contrôle dans `release.yml`) ; le mécanisme vit dans les workflows et les - commandes `fce catalog` que la documentation de référence couvre déjà. +* Maintenir la procédure de release et son chemin de reprise dans la référence du workflow plutôt que dans cet ADR. ## Références -* ADR-0009 — GenDoc modélisant ses propres échecs comme des erreurs de première - classe, les codes que ce contrat versionne. -* ADR-0002 — le modèle de runtime de l'outillage qui garde GenDoc embarqué dans le - train `cli` plutôt que publié séparément. -* Issue [#167](https://github.com/Reefact/first-class-errors/issues/167) — la - demande à laquelle cette décision répond. -* [Référence du versionnage de catalogue](../../for-users/CatalogVersioningReference.fr.md) - — les mécanismes `fce catalog update`/`diff` réutilisés ici. +* [Référence d'implémentation des ADR — Compatibilité du catalogue GenDoc](../specifications/adr-implementation-reference.fr.md#compatibilité-du-catalogue-gendoc) +* [Référence de versionnement des catalogues](../../for-users/CatalogVersioningReference.fr.md) +* [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.fr.md) +* [ADR-0002](0002-floor-the-tooling-runtime.fr.md) +* Issue #167. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 5ed4c6cc464b222145b69bb0798770f9b649689a Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:49:56 +0200 Subject: [PATCH 16/33] docs: accept and simplify the standalone Dummies package ADR --- ...11-host-dummies-as-a-standalone-package.md | 127 +++++------------- 1 file changed, 33 insertions(+), 94 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md index 7275576a..9e9e2645 100644 --- a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md +++ b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md @@ -2,135 +2,74 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0011-host-dummies-as-a-standalone-package.fr.md) -**Status:** Proposed -**Date:** 2026-07-17 +**Status:** Accepted +**Date:** 2026-07-19 **Decision Makers:** Reefact ## Context -`FirstClassErrors.Testing` supplies arbitrary test values through an error-aware -`Any` facade backed by a single seedable source (ADR-0006). That ADR listed, as a -follow-up, extracting the generic value engine into a standalone, error-agnostic -utility, and the engine was deliberately kept internally separable to that end. +The generic arbitrary-value engine anticipated by ADR-0006 serves domain-driven testing in general rather than error handling specifically. -A new library, `Dummies`, now provides a fluent DSL of typed, constraint-carrying -generators (`IAny`) for arbitrary yet valid test values. Its constraints -express the invariants a value must satisfy — a value object's format, a contract -precondition — which targets domain-driven tests in general, not error handling: -the library carries no knowledge of FirstClassErrors, targets `netstandard2.0`, -and has zero dependencies. Its intended audience extends beyond FirstClassErrors -users. +A standalone library named `Dummies` now provides typed, constraint-carrying generators without knowledge of FirstClassErrors. Its intended audience extends beyond consumers of this repository's main package. -Two facts constrain where and under what name it ships: +A package identity is costly to rename after adoption, while this repository already provides the CI, packaging, release, SBOM, SourceLink, and governance infrastructure needed to ship a package safely. -* A NuGet package ID is effectively permanent: renaming after adoption means - publishing a new package and forcing a consumer migration. -* This repository already carries the shipping apparatus a published package - needs — CI with a zero-warning ratchet, SBOM embedding, SourceLink, tag-driven - release trains selected by an explicit project list, commit conventions, and - this ADR base. A separate repository would have to duplicate all of it. - -The library's API is expected to evolve fastest in its first iterations, while -its most likely early consumers (this repository's own test projects, and -possibly `FirstClassErrors.Testing` later) live here. +The library's API is expected to evolve quickly during its first iterations, and its earliest consumers are colocated in this repository. ## Decision -The `Dummies` library ships as its own NuGet package, named `Dummies`, hosted in -this repository as a standalone project that references no FirstClassErrors -project — a boundary guarded by an architecture test. +`Dummies` ships as an independent NuGet package named `Dummies`, hosted in this repository as a standalone project that must not reference any FirstClassErrors project. ## Rationale -* **The name must not narrow the audience.** The library is a generic - test-value generator; a `FirstClassErrors.Testing.*` name would describe it as - error-handling tooling, cap its audience to FirstClassErrors users, and imply - a dependency that does not exist. Because a package ID is permanent, this had - to be decided before first publication, not after. -* **The identity lives in the package boundary, not the repository boundary.** - A standalone package ID, its own namespace, and a zero-reference rule deliver - the independent identity; hosting the sources here reuses the existing - shipping apparatus and keeps iteration friction low precisely while the API - churns most. -* **The boundary is enforced, not hoped for.** An architecture test fails any - build in which `Dummies` gains a FirstClassErrors reference, so the standalone - promise cannot erode silently, and a later extraction to its own repository - stays a mechanical operation. -* **It realizes ADR-0006's follow-up as intended.** The standalone, - error-agnostic utility that ADR anticipated now exists as a first-class - package rather than an internal engine. +The package name reflects the library's actual scope and avoids implying an error-handling dependency that does not exist. + +Repository colocation reuses mature delivery infrastructure and keeps iteration inexpensive while the package boundary, namespace, and dependency rule preserve a distinct product identity. + +The no-reference rule makes the independence enforceable and keeps a later repository extraction mechanical rather than architectural. + +The current release-train and architecture-test mechanics are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#dummies-generation-contracts) and the repository's packaging documentation. ## Alternatives Considered -### Name it `FirstClassErrors.Testing.Dummies` +### Name it as part of FirstClassErrors.Testing -Considered because the library was conceived while splitting the generic value -engine out of `FirstClassErrors.Testing`, and a family name inherits that -package's audience. Rejected because the name misdescribes the content (the -library is not about errors), caps the audience the library is built for, and -suggests a coupling the code deliberately forbids. +Considered because the engine originated near that package. Rejected because the name would narrow the audience, misdescribe the library, and imply a dependency the architecture forbids. -### Create a separate repository now +### Create a separate repository immediately -Considered because a standalone product in its own repository is the cleanest -long-term identity. Rejected for now because it duplicates the entire shipping -apparatus for no identity gain the package boundary does not already deliver, -and it adds cross-repository friction at the moment the API evolves fastest. -The extraction stays cheap as long as the no-reference boundary holds; the -triggers for revisiting are listed as follow-ups. +Considered because it gives the strongest organizational separation. Rejected because the package boundary already provides identity while a new repository would duplicate delivery infrastructure during the period of fastest API evolution. -### Extend the `Any` facade of `FirstClassErrors.Testing` in place +### Extend the existing FirstClassErrors.Testing facade -Considered because that facade exists and is shipped. Rejected because it welds -the generic engine to the error-specific surface — the opposite of the -standalone ambition — and because growing a full constraint DSL inside a -test-support package for errors would misplace its center of gravity. -`FirstClassErrors.Testing` keeps its own facade unchanged. +Considered because it already ships. Rejected because it would couple a generic generation DSL to an error-specific package and prevent the intended independent audience. ## Consequences ### Positive -* The library carries an identity and an audience of its own, independent of - FirstClassErrors, from its first release. -* No shipping infrastructure is duplicated; the package benefits from the - repository's existing CI, packaging hardening, and conventions. -* The no-reference boundary is machine-checked, and extraction to a dedicated - repository remains a low-cost, mechanical option. +* Dummies has an independent package identity and audience from its first release. +* Delivery infrastructure is reused rather than duplicated. +* The dependency boundary is machine-checkable and future extraction remains inexpensive. ### Negative -* One more published package to maintain from this repository: its own release - train, documentation, and versioning cadence. -* The repository's name does not advertise the package; discoverability rests - on the package itself and its documentation. -* The commit-scope list grows by one (`dummies`), and contributors must know - that one project in this repository is deliberately not part of the - FirstClassErrors dependency graph. +* The repository maintains an additional package, release train, and documentation set. +* Contributors must understand that this project is deliberately outside the FirstClassErrors dependency graph. ### Risks -* **Boundary erosion** — a convenient shortcut adds a FirstClassErrors - reference. Mitigated by the architecture test and by this ADR recording the - rule. -* **Cadence conflict** — Dummies' release rhythm may start fighting the - repository's release trains. That pressure is an extraction trigger, not a - reason to couple the package tighter. +* The boundary could erode through a convenient project reference. Mitigation: enforce the rule with architecture tests. +* The package's release cadence could diverge from the repository. Mitigation: treat recurring cadence conflict, independent contributors, or a separate issue flow as extraction triggers. ## Follow-up Actions -* Give `Dummies` its own release train in the packaging tooling before its - first publication; until then, no release publishes it. -* Extract to a dedicated repository (keeping the package ID) when a trigger - fires: external contributors arrive, the release cadence diverges, or the - package develops an issue flow of its own. -* Write the user documentation (English and French) once the V1 surface - stabilizes. -* Decide separately whether `FirstClassErrors.Testing` later re-bases its - internal value engine on `Dummies`; nothing in this decision requires it. +* Revisit repository extraction when the package develops independent governance or release pressure. +* Decide separately whether FirstClassErrors.Testing should consume Dummies internally. ## References -* ADR-0006 — Supply arbitrary test values from a single seedable source (the - follow-up this decision realizes). -* The architecture test guarding the boundary, in `Dummies.UnitTests`. +* [ADR implementation reference — Dummies generation contracts](../specifications/adr-implementation-reference.md#dummies-generation-contracts) +* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) +* Architecture tests in `Dummies.UnitTests`. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From 0cbf79e9f975fefb3f981f78fce4f8ac1ede539f Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:50:19 +0200 Subject: [PATCH 17/33] docs: translate ADR-0011's editorial rewrite to French --- ...host-dummies-as-a-standalone-package.fr.md | 154 +++++------------- 1 file changed, 44 insertions(+), 110 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md index 1b6f352f..b43d4899 100644 --- a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md +++ b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md @@ -2,140 +2,74 @@ 🌍 🇬🇧 [English](0011-host-dummies-as-a-standalone-package.md) · 🇫🇷 Français (ce fichier) -**Statut :** Proposé -**Date :** 2026-07-17 +**Statut :** Accepté +**Date :** 2026-07-19 **Décideurs :** Reefact ## Contexte -`FirstClassErrors.Testing` fournit des valeurs de test arbitraires via une façade -`Any` orientée erreurs, adossée à une source unique à graine (ADR-0006). Cet ADR -listait, en action de suivi, l'extraction du moteur générique de valeurs vers un -utilitaire autonome et agnostique des erreurs, et le moteur avait été gardé -séparable en interne à cette fin. - -Une nouvelle bibliothèque, `Dummies`, fournit désormais une DSL fluide de -générateurs typés porteurs de contraintes (`IAny`) pour des valeurs de test -arbitraires mais valides. Ses contraintes expriment les invariants qu'une valeur -doit satisfaire — le format d'un value object, une précondition de contrat — ce -qui vise les tests orientés domaine en général, pas la gestion d'erreurs : la -bibliothèque ne connaît rien de FirstClassErrors, cible `netstandard2.0` et n'a -aucune dépendance. Son audience visée dépasse les utilisateurs de -FirstClassErrors. - -Deux faits contraignent où et sous quel nom elle est publiée : - -* Un identifiant de package NuGet est de fait permanent : le renommer après - adoption signifie publier un nouveau package et imposer une migration aux - consommateurs. -* Ce dépôt porte déjà l'appareillage de publication qu'un package publié - requiert — CI avec cliquet zéro warning, SBOM embarqué, SourceLink, trains de - release pilotés par tag et sélectionnés par liste explicite de projets, - conventions de commit, et cette base d'ADR. Un dépôt séparé devrait tout - dupliquer. - -L'API de la bibliothèque évoluera le plus vite dans ses premières itérations, -alors que ses premiers consommateurs probables (les projets de test de ce dépôt, -et peut-être `FirstClassErrors.Testing` plus tard) vivent ici. +Le moteur générique de valeurs arbitraires anticipé par l'ADR-0006 sert les tests orientés domaine en général, pas spécifiquement la gestion des erreurs. + +Une bibliothèque autonome nommée `Dummies` fournit désormais des générateurs typés portant leurs contraintes, sans connaissance de FirstClassErrors. Son public visé dépasse les consommateurs du package principal de ce dépôt. + +L'identité d'un package est coûteuse à renommer après adoption, tandis que ce dépôt fournit déjà la CI, l'empaquetage, les releases, le SBOM, SourceLink et la gouvernance nécessaires à une publication sûre. + +L'API de la bibliothèque est appelée à évoluer rapidement pendant ses premières itérations et ses premiers consommateurs sont présents dans ce dépôt. ## Décision -La bibliothèque `Dummies` est publiée comme package NuGet propre, nommé -`Dummies`, hébergé dans ce dépôt comme projet autonome ne référençant aucun -projet FirstClassErrors — frontière gardée par un test d'architecture. +`Dummies` est livré comme package NuGet indépendant nommé `Dummies`, hébergé dans ce dépôt sous la forme d'un projet autonome qui ne doit référencer aucun projet FirstClassErrors. ## Justification -* **Le nom ne doit pas restreindre l'audience.** La bibliothèque est un - générateur générique de valeurs de test ; un nom `FirstClassErrors.Testing.*` - la décrirait comme un outillage de gestion d'erreurs, plafonnerait son - audience aux utilisateurs de FirstClassErrors et suggérerait une dépendance - qui n'existe pas. Un identifiant de package étant permanent, ce choix devait - être tranché avant la première publication, pas après. -* **L'identité tient à la frontière du package, pas à celle du dépôt.** Un - identifiant autonome, son propre namespace et une règle de zéro référence - livrent l'identité indépendante ; héberger les sources ici réutilise - l'appareillage existant et garde la friction d'itération basse précisément - quand l'API bouge le plus. -* **La frontière est vérifiée, pas espérée.** Un test d'architecture fait - échouer tout build où `Dummies` gagnerait une référence FirstClassErrors : la - promesse d'autonomie ne peut pas s'éroder silencieusement, et une extraction - ultérieure vers son propre dépôt reste une opération mécanique. -* **La décision réalise le suivi d'ADR-0006 tel qu'anticipé.** L'utilitaire - autonome et agnostique des erreurs que cet ADR envisageait existe désormais - comme package de premier rang plutôt que comme moteur interne. - -## Alternatives considérées - -### Le nommer `FirstClassErrors.Testing.Dummies` - -Considéré parce que la bibliothèque est née en scindant le moteur générique de -`FirstClassErrors.Testing`, et qu'un nom de famille hérite de l'audience de ce -package. Rejeté parce que le nom décrit mal le contenu (la bibliothèque ne -parle pas d'erreurs), plafonne l'audience visée et suggère un couplage que le -code interdit délibérément. - -### Créer un dépôt séparé dès maintenant - -Considéré parce qu'un produit autonome dans son propre dépôt est l'identité la -plus propre à long terme. Rejeté pour l'instant parce que cela duplique tout -l'appareillage de publication sans gain d'identité que la frontière du package -ne livre déjà, et ajoute une friction inter-dépôts au moment où l'API évolue le -plus vite. L'extraction reste peu coûteuse tant que la frontière de zéro -référence tient ; les déclencheurs de réexamen sont listés en actions de suivi. - -### Étendre la façade `Any` de `FirstClassErrors.Testing` sur place - -Considéré parce que cette façade existe et est publiée. Rejeté parce que cela -soude le moteur générique à la surface spécifique aux erreurs — l'inverse de -l'ambition d'autonomie — et que faire grandir une DSL de contraintes complète -dans un package de support de test dédié aux erreurs en déplacerait le centre -de gravité. `FirstClassErrors.Testing` garde sa façade inchangée. +Le nom du package reflète la portée réelle de la bibliothèque et évite de suggérer une dépendance à la gestion d'erreurs qui n'existe pas. + +La colocalisation dans le dépôt réutilise une infrastructure de livraison mature et maintient un coût d'itération faible, tandis que la frontière du package, le namespace et la règle de dépendance préservent une identité produit distincte. + +La règle d'absence de référence rend l'indépendance vérifiable et conserve une future extraction vers un dépôt distinct comme opération mécanique plutôt qu'architecturale. + +Les mécanismes actuels du train de release et des tests d'architecture sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) et la documentation d'empaquetage du dépôt. + +## Alternatives envisagées + +### Le nommer comme une extension de FirstClassErrors.Testing + +Envisagé parce que le moteur a été conçu à proximité de ce package. Rejeté parce que ce nom limiterait le public, décrirait mal la bibliothèque et suggérerait une dépendance interdite par l'architecture. + +### Créer immédiatement un dépôt séparé + +Envisagé car cela donne la séparation organisationnelle la plus forte. Rejeté parce que la frontière du package fournit déjà l'identité, tandis qu'un nouveau dépôt dupliquerait l'infrastructure de livraison pendant la période où l'API évolue le plus vite. + +### Étendre la façade existante de FirstClassErrors.Testing + +Envisagé parce qu'elle est déjà publiée. Rejeté parce que cela couplerait un DSL générique de génération à un package spécifique aux erreurs et empêcherait le public indépendant recherché. ## Conséquences ### Positives -* La bibliothèque porte une identité et une audience propres, indépendantes de - FirstClassErrors, dès sa première release. -* Aucune infrastructure de publication n'est dupliquée ; le package bénéficie - de la CI, du durcissement de packaging et des conventions existants. -* La frontière de zéro référence est vérifiée par la machine, et l'extraction - vers un dépôt dédié reste une option mécanique et peu coûteuse. +* Dummies possède une identité de package et un public indépendants dès sa première release. +* L'infrastructure de livraison est réutilisée plutôt que dupliquée. +* La frontière de dépendance est vérifiable et une future extraction reste peu coûteuse. ### Négatives -* Un package publié de plus à maintenir depuis ce dépôt : son propre train de - release, sa documentation, sa cadence de versions. -* Le nom du dépôt ne met pas le package en avant ; sa découvrabilité repose sur - le package lui-même et sa documentation. -* La liste des scopes de commit grandit d'un élément (`dummies`), et les - contributeurs doivent savoir qu'un projet de ce dépôt ne fait délibérément - pas partie du graphe de dépendances FirstClassErrors. +* Le dépôt maintient un package, un train de release et une documentation supplémentaires. +* Les contributeurs doivent comprendre que ce projet est volontairement extérieur au graphe de dépendances FirstClassErrors. ### Risques -* **Érosion de la frontière** — un raccourci commode ajoute une référence - FirstClassErrors. Atténué par le test d'architecture et par cet ADR qui - consigne la règle. -* **Conflit de cadence** — le rythme de release de Dummies peut finir par se - heurter aux trains du dépôt. Cette pression est un déclencheur d'extraction, - pas une raison de coupler le package davantage. +* La frontière pourrait s'éroder par l'ajout opportuniste d'une référence de projet. Mesure : imposer la règle par des tests d'architecture. +* Le rythme de release du package pourrait diverger de celui du dépôt. Mesure : considérer les conflits récurrents de cadence, l'arrivée de contributeurs indépendants ou un flux d'issues propre comme déclencheurs d'extraction. ## Actions de suivi -* Donner à `Dummies` son propre train de release dans l'outillage de packaging - avant sa première publication ; d'ici là, aucune release ne le publie. -* Extraire vers un dépôt dédié (en conservant l'identifiant du package) quand - un déclencheur se présente : arrivée de contributeurs externes, cadence de - release divergente, ou flux d'issues propre au package. -* Écrire la documentation utilisateur (anglais et français) une fois la surface - V1 stabilisée. -* Décider séparément si `FirstClassErrors.Testing` rebase plus tard son moteur - interne de valeurs sur `Dummies` ; rien dans cette décision ne l'impose. +* Réexaminer l'extraction vers un dépôt séparé lorsque le package développe sa propre gouvernance ou une pression de release indépendante. +* Décider séparément si FirstClassErrors.Testing doit consommer Dummies en interne. ## Références -* ADR-0006 — Fournir les valeurs de test arbitraires depuis une source unique à - graine (le suivi que cette décision réalise). -* Le test d'architecture gardant la frontière, dans `Dummies.UnitTests`. +* [Référence d'implémentation des ADR — Contrats de génération de Dummies](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) +* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) +* Tests d'architecture dans `Dummies.UnitTests`. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 9cb5480ad2415b54bd31307631bb9e84b01c3998 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:50:39 +0200 Subject: [PATCH 18/33] docs: accept and simplify the distinct-collection ADR Rebased onto main after PR #199 merged a substantive rewrite of this ADR's decision content; folds that content into the editorial trim so neither PR's work is lost. --- ...ctions-by-cardinality-else-bounded-draw.md | 143 +++++------------- .../adr-implementation-reference.md | 2 +- 2 files changed, 37 insertions(+), 108 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md index f28c63ea..e03a75b5 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md @@ -8,141 +8,70 @@ ## Context -`Dummies` carries a core contract: a constraint expresses what a value must -satisfy, contradictory constraints fail at the moment they are declared with a -`ConflictingAnyConstraintException` naming both sides, and a value is built to -satisfy its constraints in a single pass — never generated then filtered, and -never behind a retry loop. - -The collection increment adds distinct collections: `SetOf`, `ListOf(...).Distinct()` -and the like, and a dictionary's keys. A distinct collection of *N* elements is -satisfiable only if *N* distinct values can be assembled from its **effective -domain**: the element generator's own domain, widened by any values pinned with -`Containing(...)` that fall outside it — each such value is one the generator itself -could never draw — and by the opaque draws of `ContainingAny(...)`. The element -generator's cardinality alone therefore bounds only the elements that must come -*from* it, not the whole request. - -Element generators fall into two groups. Some draw from a domain the library can -count **cheaply**: a boolean's two values, an enum's declared members, a narrow -integer or time range, a restricted character pool, an explicit allow-list -(`OneOf`), or a scalar pinned to a single value (`Zero`, `Between(x, x)`). Others -draw from a domain that is effectively unbounded, or countable only in principle -at a cost the library declines to pay: unconstrained integers, strings and -identifiers, a floating-point **range** (finite in representable values, but -counting them is type-specific bit-arithmetic disproportionate to the dummy use -case — so a decimal or floating-point generator is gated only through an -allow-list or a pin, never a wider range), and — decisively — any foreign -`IAny` implementation or any derived generator (`As`, `Combine`), which carries -no domain information at all. `IAny` is a public interface, so the library -cannot assume every generator can report its cardinality. - -A custom equality comparer can only merge distinct values into fewer equivalence -classes; it can never manufacture new ones. +Dummies treats contradictory constraints as arrangement errors and avoids hidden unbounded retry loops. + +A distinct collection of `N` elements is satisfiable only when at least `N` distinct values can be assembled from its effective domain: the element generator's own domain, widened by any values pinned outside it and by opaque externally-supplied values the generator itself could never draw. The generator's own cardinality therefore bounds only the elements that must come from it, not the whole request. + +Some generators expose a domain the library can count cheaply — a small fixed set, or a value pinned to one member of it. Others cannot honestly report their domain size, either because counting it is disproportionately expensive (a floating-point range, for example) or because it is genuinely unbounded or unknowable, including foreign `IAny` implementations and composed generators. + +A custom equality comparer can reduce the number of effective equivalence classes even when the generator's nominal domain is larger. ## Decision -A distinct collection rejects, at declaration time, any element count that -exceeds the element generator's advertised cardinality, and otherwise builds its -elements by a bounded deduplicating draw that fails at generation, with a -replayable seed, if the element domain proves too small. +A distinct collection rejects a requested count immediately when it exceeds a known effective element-domain cardinality, and otherwise uses a bounded deduplicating draw that fails explicitly and reproducibly when enough distinct values cannot be obtained. ## Rationale -* **Fail eagerly wherever the domain is knowable.** The declaration-time conflict - is the library's signature: a count that exceeds a countable element domain is - a contradiction in the test's `Arrange`, and it must read as one, named on both - sides, exactly like every scalar conflict — not surface later as a puzzling - runtime failure. -* **A bounded draw is the only honest option where it is not.** Because an - arbitrary generator's cardinality is generally unknowable, the only universal - way to obtain *N* distinct values is to draw and deduplicate. Keeping that draw - bounded honours the no-retry-loop principle; on exhaustion it reports the real - shortfall as an `AnyGenerationException` naming the seed — the same failure - channel a factory rejection already uses — rather than looping. -* **The eager check counts only what the generator must supply, and stays sound - under a comparer.** It compares the element generator's cardinality against the - count *reduced by* the values pinned outside its domain and by each opaque - `ContainingAny(...)` draw — so a fixed value the generator cannot produce widens - the request instead of conflicting with it, and an unprovable overlap defers to - the bounded draw rather than becoming a false conflict. Because a comparer only - merges values, the advertised cardinality remains a valid *upper* bound on the - generator's own contribution, so the check never rejects a request that was - actually satisfiable; a comparer that collapses the effective domain below the - requested count is caught by the bounded draw instead. -* **One principle, applied where its information exists.** Splitting the failure - between declaration time (when the domain is countable) and generation time - (when it is not) is not a dilution of the eager-conflict principle but its - faithful extension to the only place where the information needed to be eager is - absent. +When the domain size is known, the contradiction is certain and belongs at declaration time with the rest of Dummies' constraint validation. + +Counting only the generator's own cardinality would eagerly reject requests that are actually satisfiable once already-accounted-for values are considered; the eager check therefore compares against the domain size net of the values already pinned or opaquely supplied outside it, so it stays sound: it never rejects a request that was truly satisfiable, and a comparer that collapses the effective domain below the requested count is still caught by the bounded draw. + +When the domain size is unknown, drawing and deduplicating is the only general strategy available. Bounding the work preserves termination and turns an impossible or practically unreachable request into a diagnosable generation failure rather than a hang. + +The cardinality capability remains optional so public and foreign generators are not forced to provide information they cannot know. A comparer-induced reduction is then handled by the generation-time bound. + +The exact hint interface, collection state, draw budget, exception payload, and seed propagation are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#dummies-generation-contracts) and the Dummies user documentation. ## Alternatives Considered -### Always fail at generation, dropping the eager cardinality check +### Always fail at generation -Considered because a single failure channel is simpler to explain and to -implement. Rejected because it discards the library's signature diagnostic -precisely where it is cheap and certain — a set of three booleans, an enum asked -for more members than it declares — turning an obvious `Arrange` contradiction -into a runtime surprise. +Considered because one failure point is simpler. Rejected because it discards an exact declaration-time diagnosis for generators whose domain size is known. -### Make cardinality part of `IAny`, so every request is decided eagerly +### Require every generator to expose cardinality -Considered because a mandatory cardinality on every generator would let every -distinct request fail or pass at declaration time. Rejected because `IAny` is -a public contract with foreign implementations and with derived generators -(`As`, `Combine`) that cannot honestly report a bound; the guarantee would be -unenforceable and frequently wrong, and it would burden every implementer with a -value most of them cannot supply. +Considered because it would make every request decidable up front. Rejected because many valid generators cannot provide a trustworthy bound and the public interface supports foreign implementations. -### Draw without a bound until *N* distinct values appear +### Draw without a bound -Considered because an unbounded draw always terminates when the request is -satisfiable. Rejected because it never terminates when the request is *not* -satisfiable, which is exactly the case this decision must diagnose: it would turn -an impossible request into a hang instead of an error, breaking the library's -bounded-work principle. +Considered because a satisfiable request would eventually complete. Rejected because an unsatisfiable request could loop forever. ## Consequences ### Positive -* The signature declaration-time diagnostic now reaches distinct collections - wherever the element domain is countable. -* Requests over unknown or comparer-reduced domains still fail safely and - reproducibly, with a seed to replay, never as a hang. -* The cardinality capability is internal and opt-in, so the public `IAny` - contract is unchanged and foreign generators keep working unmodified. +* Known contradictions fail early and clearly. +* Unknown domains still fail safely, reproducibly, and without hanging. +* Foreign generators remain compatible without implementing cardinality metadata. ### Negative -* Failure timing is not uniform: the same logical contradiction surfaces at - declaration for a known-small domain and at generation for an unknowable one, - which a user must understand. -* The bounded draw runs to a chosen budget; a request pushed pathologically close - to an unknown domain's true size could in principle fail although it was - satisfiable — astronomically unlikely for the dummy-sized collections the - library targets. +* Failure timing differs between known and unknown domains. +* A bounded draw can fail for a theoretically satisfiable but heavily biased generator. ### Risks -* **Overstated hint** — a generator could advertise a cardinality larger than the - distinct values it truly yields, so the eager check misses a real conflict. - Mitigated because the hint is defined as an upper bound and the bounded draw - catches any residual shortfall at generation. -* **Budget mis-tuning** — too small a draw budget would yield spurious generation - failures. Mitigated by scaling the budget to a known cardinality and keeping a - generous floor for unknown domains. +* A generator may advertise an inaccurate upper bound. Mitigation: the bounded draw remains the final safety net. +* A poorly tuned budget may cause spurious failures. Mitigation: keep the budget documented, test representative biased generators, and revise it based on evidence rather than describing failure as impossible. ## Follow-up Actions -* Document the two failure channels in the user documentation once the collection - surface stabilizes. -* Revisit the draw budget if real usage ever surfaces a spurious exhaustion. +* Document both failure channels and the replay seed in the Dummies guide. +* Revisit the budget if real usage reveals false exhaustion. ## References -* ADR-0011 — Host Dummies as a standalone package in this repository. -* The distinct-collection engine and its unified cardinality-and-membership capability - (one interface, so a finite generator cannot drift out of the eager perimeter), in the - `Dummies` project (`CollectionState`, `ICardinalityHint`). +* [ADR implementation reference — Dummies generation contracts](../specifications/adr-implementation-reference.md#dummies-generation-contracts) +* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) +* `CollectionState` and `ICardinalityHint` in the `Dummies` project. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md index d4dcf33d..e55285cd 100644 --- a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md +++ b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md @@ -61,7 +61,7 @@ Related decisions: [ADR-0006](../adr/0006-supply-arbitrary-test-values-from-a-se Dummies is shipped as a standalone package with no dependency on the FirstClassErrors runtime package. Generation is unseeded by default; reproducible generation is selected explicitly and exposes the seed needed to replay failures. -Distinct collection generation first uses a cardinality hint when the source can provide one. When cardinality is unknown, generation uses a bounded draw and fails explicitly rather than looping forever. The bound is a safety mechanism, not a proof that every foreign or biased generator will succeed whenever enough distinct values theoretically exist. +Distinct collection generation first compares the requested count against the element generator's cardinality hint, when `ICardinalityHint` can provide one, net of any values pinned outside that domain via `Containing(...)` and any opaque draws requested via `ContainingAny(...)` — both widen what the generator itself must still supply rather than counting against it. A floating-point or decimal range is not treated as cheaply countable, since enumerating its representable values is type-specific bit-arithmetic disproportionate to the dummy use case, so such a generator only participates in the eager check when pinned to an explicit allow-list or a single value (`OneOf`, `Zero`, `Between(x, x)`), never through a wider range. When cardinality is unknown, generation uses a bounded draw and fails explicitly rather than looping forever. The bound is a safety mechanism, not a proof that every foreign or biased generator will succeed whenever enough distinct values theoretically exist. `CollectionState` and `ICardinalityHint` unify cardinality and membership behind one interface, so a generator with a finite domain cannot drift out of the eager perimeter through a comparer. `Any.Combine` provides overloads up to arity eight. Higher arities are intentionally outside the supported convenience surface and should use composition or a domain-specific factory. From e518db16707bcf536b8f436557bac08bdfd224ff Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:51:01 +0200 Subject: [PATCH 19/33] docs: translate ADR-0013's editorial rewrite to French --- ...ons-by-cardinality-else-bounded-draw.fr.md | 172 +++++------------- .../adr-implementation-reference.fr.md | 2 +- 2 files changed, 45 insertions(+), 129 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md index 1bee6278..e1cb07e0 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md @@ -8,154 +8,70 @@ ## Contexte -`Dummies` porte un contrat fondateur : une contrainte exprime ce qu'une valeur -doit satisfaire, des contraintes contradictoires échouent au moment où elles sont -déclarées via une `ConflictingAnyConstraintException` nommant les deux côtés, et -une valeur est construite pour satisfaire ses contraintes en une seule passe — -jamais générée puis filtrée, et jamais derrière une boucle de réessai. - -L'incrément « collections » ajoute les collections distinctes : `SetOf`, -`ListOf(...).Distinct()` et consorts, ainsi que les clés d'un dictionnaire. Une -collection distincte de *N* éléments n'est satisfaisable que si *N* valeurs -distinctes peuvent être assemblées depuis son **domaine effectif** : le domaine -propre du générateur d'éléments, élargi par les valeurs fixées avec `Containing(...)` -qui en sortent — chacune est une valeur que le générateur lui-même ne pourrait -jamais tirer — et par les tirages opaques de `ContainingAny(...)`. La cardinalité du -seul générateur d'éléments ne borne donc que les éléments qui doivent venir *de -lui*, non la demande entière. - -Les générateurs d'éléments se répartissent en deux groupes. Certains tirent d'un -domaine que la bibliothèque sait compter **à bas coût** : les deux valeurs d'un -booléen, les membres déclarés d'une énumération, un intervalle entier ou temporel -étroit, un pool de caractères restreint, une liste blanche explicite (`OneOf`), ou -un scalaire épinglé sur une seule valeur (`Zero`, `Between(x, x)`). D'autres tirent -d'un domaine effectivement non borné, ou dénombrable seulement en principe à un -coût que la bibliothèque refuse de payer : entiers non contraints, chaînes et -identifiants, une **plage** flottante (finie en valeurs représentables, mais les -compter relève d'une arithmétique de bits spécifique au type, disproportionnée pour -l'usage « dummy » — un générateur décimal ou flottant n'est donc contrôlé que via -une liste blanche ou un pin, jamais une plage plus large), et — de façon décisive — -toute implémentation étrangère de `IAny` ou tout générateur dérivé (`As`, -`Combine`), qui ne porte aucune information de domaine. `IAny` est une interface -publique : la bibliothèque ne peut donc pas supposer que tout générateur sache -rapporter sa cardinalité. - -Un comparateur d'égalité personnalisé ne peut que fusionner des valeurs distinctes -en un nombre moindre de classes d'équivalence ; il ne peut jamais en créer de -nouvelles. +Dummies traite les contraintes contradictoires comme des erreurs d'arrangement et évite les boucles de nouvelles tentatives cachées et non bornées. + +Une collection distincte de `N` éléments n'est satisfaisable que si au moins `N` valeurs distinctes peuvent être assemblées depuis son domaine effectif : le domaine propre du générateur d'éléments, élargi par les valeurs fixées en dehors de celui-ci et par les valeurs opaques fournies de l'extérieur que le générateur lui-même ne pourrait jamais tirer. La cardinalité propre du générateur ne borne donc que les éléments qui doivent venir de lui, non la demande entière. + +Certains générateurs exposent un domaine que la bibliothèque sait compter à bas coût — un petit ensemble fixe, ou une valeur fixée sur l'un de ses membres. D'autres ne peuvent pas annoncer honnêtement la taille de leur domaine, soit parce que la compter est disproportionnément coûteux (une plage flottante, par exemple), soit parce qu'il est véritablement non borné ou inconnaissable, notamment les implémentations externes de `IAny` et les générateurs composés. + +Un comparateur d'égalité personnalisé peut réduire le nombre de classes d'équivalence effectives même lorsque le domaine nominal du générateur est plus grand. ## Décision -Une collection distincte rejette, au moment de la déclaration, tout nombre -d'éléments qui dépasse la cardinalité annoncée par le générateur d'éléments, et -construit sinon ses éléments par un tirage dédupliquant borné qui échoue à la -génération, avec une graine rejouable, si le domaine des éléments se révèle trop -petit. +Une collection distincte rejette immédiatement un nombre demandé supérieur à une cardinalité effective connue du domaine des éléments, et utilise sinon un tirage dédupliqué borné qui échoue explicitement et de manière reproductible lorsqu'il n'est pas possible d'obtenir assez de valeurs distinctes. ## Justification -* **Échouer tôt partout où le domaine est connaissable.** Le conflit à la - déclaration est la signature de la bibliothèque : un nombre qui dépasse un - domaine d'éléments dénombrable est une contradiction dans l'`Arrange` du test, et - il doit se lire comme telle, nommée des deux côtés, exactement comme tout conflit - scalaire — non pas surgir plus tard sous forme d'un échec d'exécution - déroutant. -* **Un tirage borné est la seule option honnête là où il ne l'est pas.** Comme la - cardinalité d'un générateur arbitraire est généralement inconnaissable, la seule - façon universelle d'obtenir *N* valeurs distinctes est de tirer et dédupliquer. - Garder ce tirage borné respecte le principe « pas de boucle de réessai » ; à - épuisement, il rapporte le manque réel via une `AnyGenerationException` nommant - la graine — le canal d'échec qu'utilise déjà un rejet de fabrique — plutôt que de - boucler. -* **Le contrôle anticipé ne compte que ce que le générateur doit fournir, et reste - correct sous un comparateur.** Il compare la cardinalité du générateur d'éléments - au nombre demandé *diminué* des valeurs fixées hors de son domaine et de chaque - tirage opaque de `ContainingAny(...)` — ainsi une valeur fixe que le générateur ne - peut produire élargit la demande au lieu d'entrer en conflit avec elle, et un - recouvrement improuvable est renvoyé au tirage borné plutôt que de devenir un faux - conflit. Puisqu'un comparateur ne fait que fusionner des valeurs, la cardinalité - annoncée reste une borne *supérieure* valide sur la contribution propre du - générateur : le contrôle ne rejette donc jamais une demande qui était en réalité - satisfaisable ; un comparateur qui réduit le domaine effectif sous le nombre - demandé est rattrapé par le tirage borné. -* **Un seul principe, appliqué là où son information existe.** Répartir l'échec - entre la déclaration (quand le domaine est dénombrable) et la génération (quand - il ne l'est pas) n'est pas un affaiblissement du principe de conflit anticipé - mais son extension fidèle au seul endroit où l'information nécessaire pour être - anticipé fait défaut. - -## Alternatives considérées - -### Toujours échouer à la génération, en abandonnant le contrôle anticipé de cardinalité - -Considérée parce qu'un canal d'échec unique est plus simple à expliquer et à -implémenter. Rejetée parce qu'elle jette le diagnostic signature de la -bibliothèque précisément là où il est peu coûteux et certain — un ensemble de trois -booléens, une énumération à qui l'on demande plus de membres qu'elle n'en déclare — -transformant une contradiction évidente de l'`Arrange` en une surprise à -l'exécution. - -### Faire de la cardinalité une partie de `IAny`, pour trancher chaque demande tôt - -Considérée parce qu'une cardinalité obligatoire sur chaque générateur permettrait -de trancher toute demande distincte à la déclaration. Rejetée parce que `IAny` -est un contrat public, avec des implémentations étrangères et des générateurs -dérivés (`As`, `Combine`) incapables de rapporter honnêtement une borne ; la -garantie serait inapplicable et souvent fausse, et elle imposerait à chaque -implémenteur une valeur que la plupart ne peuvent pas fournir. - -### Tirer sans borne jusqu'à ce que *N* valeurs distinctes apparaissent - -Considérée parce qu'un tirage non borné se termine toujours quand la demande est -satisfaisable. Rejetée parce qu'il ne se termine jamais quand la demande ne l'est -*pas*, ce qui est exactement le cas que cette décision doit diagnostiquer : elle -transformerait une demande impossible en blocage au lieu d'une erreur, brisant le -principe de travail borné de la bibliothèque. +Lorsque la taille du domaine est connue, la contradiction est certaine et doit être signalée au moment de la déclaration, comme les autres validations de contraintes de Dummies. + +Ne compter que la cardinalité propre du générateur rejetterait par anticipation des demandes en réalité satisfaisables une fois prises en compte les valeurs déjà couvertes ; le contrôle anticipé compare donc à la taille du domaine diminuée des valeurs déjà fixées ou fournies de façon opaque en dehors de lui, ce qui le garde correct : il ne rejette jamais une demande réellement satisfaisable, et un comparateur qui réduit le domaine effectif sous le nombre demandé reste rattrapé par le tirage borné. + +Lorsque la taille du domaine est inconnue, tirer puis dédupliquer est la seule stratégie générale disponible. Borner le travail garantit la terminaison et transforme une demande impossible ou pratiquement inaccessible en échec de génération diagnostiquable plutôt qu'en blocage. + +La capacité de cardinalité reste optionnelle afin de ne pas imposer aux générateurs publics ou externes une information qu'ils ne peuvent pas connaître. Une réduction induite par le comparateur est alors prise en charge par la borne à la génération. + +L'interface d'indication exacte, l'état de collection, le budget de tirage, le contenu de l'exception et la propagation de la seed sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) et la documentation utilisateur de Dummies. + +## Alternatives envisagées + +### Toujours échouer à la génération + +Envisagé car un point d'échec unique est plus simple. Rejeté parce que cela supprime un diagnostic exact au moment de la déclaration pour les générateurs dont la taille du domaine est connue. + +### Exiger une cardinalité de chaque générateur + +Envisagé pour rendre chaque demande décidable immédiatement. Rejeté parce que de nombreux générateurs valides ne peuvent pas fournir une borne fiable et que l'interface publique accepte des implémentations externes. + +### Tirer sans borne + +Envisagé car une demande satisfaisable finirait par aboutir. Rejeté parce qu'une demande insatisfaisable pourrait boucler indéfiniment. ## Conséquences ### Positives -* Le diagnostic signature à la déclaration atteint désormais les collections - distinctes partout où le domaine des éléments est dénombrable. -* Les demandes sur des domaines inconnus ou réduits par un comparateur échouent - toujours de façon sûre et reproductible, avec une graine à rejouer, jamais en - blocage. -* La capacité de cardinalité est interne et optionnelle : le contrat public - `IAny` reste inchangé et les générateurs étrangers continuent de fonctionner - tels quels. +* Les contradictions connues échouent tôt et clairement. +* Les domaines inconnus échouent tout de même de manière sûre, reproductible et sans blocage. +* Les générateurs externes restent compatibles sans implémenter de métadonnées de cardinalité. ### Négatives -* Le moment de l'échec n'est pas uniforme : la même contradiction logique surgit à - la déclaration pour un domaine connu-petit et à la génération pour un domaine - inconnaissable, ce que l'utilisateur doit comprendre. -* Le tirage borné s'exécute jusqu'à un budget choisi ; une demande poussée - pathologiquement près de la taille réelle d'un domaine inconnu pourrait en - principe échouer bien qu'elle fût satisfaisable — astronomiquement improbable pour - les collections de taille « dummy » que vise la bibliothèque. +* Le moment de l'échec diffère entre domaines connus et inconnus. +* Un tirage borné peut échouer pour un générateur théoriquement satisfaisable mais fortement biaisé. ### Risques -* **Indice surestimé** — un générateur pourrait annoncer une cardinalité plus - grande que les valeurs distinctes qu'il produit réellement, de sorte que le - contrôle anticipé manque un vrai conflit. Atténué parce que l'indice est défini - comme une borne supérieure et que le tirage borné rattrape tout manque résiduel à - la génération. -* **Mauvais réglage du budget** — un budget de tirage trop petit produirait des - échecs de génération fallacieux. Atténué en dimensionnant le budget sur une - cardinalité connue et en gardant un plancher généreux pour les domaines inconnus. +* Un générateur peut annoncer une borne supérieure inexacte. Mesure : le tirage borné reste le filet de sécurité final. +* Un budget mal calibré peut provoquer des échecs indus. Mesure : documenter le budget, tester des générateurs biaisés représentatifs et le réviser sur la base de faits plutôt que de présenter l'échec comme impossible. ## Actions de suivi -* Documenter les deux canaux d'échec dans la documentation utilisateur une fois la - surface « collections » stabilisée. -* Réexaminer le budget de tirage si un usage réel fait un jour apparaître un - épuisement fallacieux. +* Documenter les deux canaux d'échec et la seed de rejeu dans le guide Dummies. +* Réexaminer le budget si l'usage réel révèle des épuisements indus. ## Références -* ADR-0011 — Héberger Dummies comme un paquet autonome dans ce dépôt. -* Le moteur de collection distincte et sa capacité unifiée de cardinalité et - d'appartenance (une seule interface, pour qu'un générateur fini ne puisse pas sortir - du périmètre anticipé), dans le projet `Dummies` (`CollectionState`, `ICardinalityHint`). +* [Référence d'implémentation des ADR — Contrats de génération de Dummies](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) +* [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) +* `CollectionState` et `ICardinalityHint` dans le projet `Dummies`. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md index 16f3c568..6a604859 100644 --- a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md +++ b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md @@ -61,7 +61,7 @@ Décisions liées : [ADR-0006](../adr/0006-supply-arbitrary-test-values-from-a-s Dummies est livré comme package autonome sans dépendance sur le package d'exécution FirstClassErrors. La génération n'est pas seedée par défaut ; la génération reproductible est choisie explicitement et expose la seed nécessaire pour rejouer les échecs. -La génération de collections distinctes utilise d'abord une indication de cardinalité lorsque la source sait la fournir. Lorsque la cardinalité est inconnue, elle effectue un nombre borné de tirages et échoue explicitement plutôt que de boucler indéfiniment. Cette borne est un mécanisme de sûreté, pas une preuve que tout générateur externe ou biaisé réussira dès lors qu'un nombre suffisant de valeurs distinctes existe théoriquement. +La génération de collections distinctes compare d'abord le nombre demandé à l'indication de cardinalité du générateur d'éléments, lorsque `ICardinalityHint` sait en fournir une, diminuée des valeurs fixées en dehors de ce domaine via `Containing(...)` et des tirages opaques demandés via `ContainingAny(...)` — les deux élargissent ce que le générateur doit encore fournir lui-même plutôt que de compter contre lui. Une plage flottante ou décimale n'est pas considérée comme dénombrable à bas coût, car énumérer ses valeurs représentables relève d'une arithmétique de bits spécifique au type, disproportionnée pour l'usage « dummy » ; un tel générateur ne participe donc au contrôle anticipé que s'il est fixé sur une liste blanche explicite ou une valeur unique (`OneOf`, `Zero`, `Between(x, x)`), jamais via une plage plus large. Lorsque la cardinalité est inconnue, elle effectue un nombre borné de tirages et échoue explicitement plutôt que de boucler indéfiniment. Cette borne est un mécanisme de sûreté, pas une preuve que tout générateur externe ou biaisé réussira dès lors qu'un nombre suffisant de valeurs distinctes existe théoriquement. `CollectionState` et `ICardinalityHint` unifient la cardinalité et l'appartenance derrière une seule interface, afin qu'un générateur à domaine fini ne puisse pas sortir du périmètre anticipé via un comparateur. `Any.Combine` fournit des surcharges jusqu'à l'arité huit. Les arités supérieures sont volontairement exclues de cette surface de confort et doivent utiliser la composition ou une factory spécifique au domaine. From cab24413646af790406ee4b5e06afefbd631a52f Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:51:25 +0200 Subject: [PATCH 20/33] docs: accept and simplify the Any.Combine arity ADR --- .../0015-cap-any-combine-at-arity-eight.md | 111 ++++++------------ 1 file changed, 35 insertions(+), 76 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md index 366b8e59..b47a0cc2 100644 --- a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md +++ b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md @@ -2,116 +2,75 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0015-cap-any-combine-at-arity-eight.fr.md) -**Status:** Proposed -**Date:** 2026-07-18 +**Status:** Accepted +**Date:** 2026-07-19 **Decision Makers:** Reefact ## Context -`Dummies` composes constrained generators into larger objects through -`Any.Combine(parts..., compose)`: the parts are generated, then handed to a -caller-supplied constructor lambda, so a value object or aggregate is assembled -without reflection and the domain's own constructor stays the single gatekeeper. -Building large objects easily is an explicit goal of the library. - -C# has no heterogeneous variadic generics: passing *N* differently-typed parts in -one call requires a distinct overload per arity, each with *N*+1 generic type -parameters and *N*+1 value parameters. Until now `Combine` existed only for two -and three parts; composing more forced nesting (`Combine(Combine(a, b, …), c, …)`) -or routing through tuples, both of which surface positional `Item1..ItemN` access -or nested lambdas at the call site. - -The repository runs a SonarCloud analysis whose rule S107 flags a method with more -than seven parameters. A `Combine` of seven parts has eight parameters (seven -generators plus the composer) and of eight parts, nine — so the two largest -overloads cross that threshold. The repository's standing practice is to keep the -analysis clean, suppressing a rule inline with a justification where a deliberate -exception is made. - -A constructor that needs many parts is itself often a design signal — a missing -intermediate value object. +Dummies composes differently typed generators into larger objects through `Any.Combine`, preserving constructor-based domain validation without reflection. + +C# has no heterogeneous variadic generics, so each supported arity requires a distinct public overload. Low arities alone force nested composition or positional tuples for larger constructors, while an unlimited surface would create repetitive API and documentation with diminishing value. + +Very wide constructors can also indicate missing intermediate domain concepts. ## Decision -`Any.Combine` offers overloads from two up to eight parts and stops there, -accepting the parameter- and generic-count code smell of the two largest overloads -as a deliberate trade-off for a flat, reflection-free composition call site. +`Any.Combine` provides flat heterogeneous overloads from arity two through arity eight and deliberately stops there. ## Rationale -* **It serves the "build large objects" goal directly.** A flat - `Combine(a, b, c, d, e, (…) => new Thing(…))` with caller-named lambda parameters - reads far better than nested `Combine` calls or tuple `Item1..ItemN` access, and - keeps the composition reflection-free — the whole point of `Combine`. -* **Eight is where the ceiling belongs.** Eight parts cover essentially every - hand-written DDD constructor; beyond that, the object is complex enough that - intermediate value objects are the healthier design, so a ceiling at eight nudges - toward that structure instead of smoothing over arbitrarily wide constructors. -* **The smell is inherent, not accidental.** There is no lower-smell way to pass - *N* differently-typed parts in a single call; the parameter and generic counts - are the irreducible cost of heterogeneous composition. Suppressing S107 with a - justification on the two largest overloads records that trade-off at the code, - and this ADR records why it is acceptable. -* **Stopping short of sixteen keeps the surface bounded.** Matching `Func`'s - sixteen-argument ceiling would add hand-maintained, fully-documented overloads - whose marginal value is low and whose boilerplate cost is real; the demand past - eight does not justify it. +A flat call with named lambda parameters is materially clearer than nested composition or positional tuple access for the object sizes commonly encountered in domain code. + +Eight is a pragmatic convenience ceiling rather than a mathematical property of DDD. It covers the intended large-object use cases while keeping the manually maintained surface bounded and allowing wider constructors to remain a design signal. + +The unavoidable parameter-count warnings on the largest overloads are an explicit local trade-off, not a general relaxation of the repository's code-quality rules. + +Exact signatures, documentation, and analyzer suppressions are implementation details recorded in the [ADR implementation reference](../specifications/adr-implementation-reference.md#dummies-generation-contracts) and the Dummies API reference. ## Alternatives Considered -### Keep only arity two and three; compose more by nesting +### Keep only the smallest overloads -Considered because it adds no new surface. Rejected because nesting forces -positional tuple access (`Item1..ItemN`) or nested lambdas at the call site — -unreadable precisely where the library promises easy large-object construction. +Considered because it minimizes API surface. Rejected because larger compositions become substantially less readable through nested lambdas or positional tuple members. -### A fluent tuple-accumulating builder (`Combine(a).And(b).And(c)…`) +### Use a fluent tuple-accumulating builder -Considered as a way to avoid one overload per arity. Rejected because `.And` would -itself need an overload per source arity, and the accumulated tuple exposes -positional access again — it trades one form of boilerplate for a worse call site. +Considered to avoid one overload per arity. Rejected because it moves the same complexity into the builder and still exposes positional structure at the call site. -### Extend all the way to arity sixteen +### Extend to the maximum arity supported by `Func` -Considered for completeness, matching `Func`. Rejected because nine-to-sixteen-part -constructors are a design smell the library should not smooth over, and each -overload is fully-documented, hand-maintained surface with negligible real demand. +Considered for completeness. Rejected because the maintenance cost and normalization of extremely wide constructors outweigh the marginal convenience. -### A `params` array of same-typed generators +### Accept only homogeneous generators through `params` -Considered for the homogeneous case. Rejected because it only works when every part -shares one type and it loses per-part typing — it does not serve the -heterogeneous-constructor case `Combine` exists for. It remains an orthogonal option -that could be added later without touching this decision. +Considered because it is naturally variadic. Rejected because it does not serve the differently typed constructor parameters for which `Combine` exists. ## Consequences ### Positive -* Large value objects and aggregates compose in one flat, readable, reflection-free - call, with caller-named parameters. -* The ceiling gently steers very wide constructors toward intermediate value - objects. +* Common large objects compose in one readable, reflection-free call. +* The convenience API remains deliberately bounded. +* Extremely wide construction stays visible as a possible design problem. ### Negative -* Five more hand-maintained, fully-documented overloads on the facade. -* The two largest overloads carry an inline S107 suppression — a documented, - localized exception to the parameter-count guideline. +* Several hand-maintained overloads remain part of the public surface. +* The largest overloads require localized analyzer suppressions. +* The ceiling is heuristic and may not fit every domain. ### Risks -* **Ceiling pressure** — a genuine nine-or-more-part need could recur. Mitigated - because that is itself a design signal; the ceiling is revisited only if the need - proves common, and adding higher arities later stays non-breaking. +* A recurring legitimate need above arity eight may appear. Mitigation: higher arities can be added compatibly through a new decision if evidence shows that the current ceiling is too low. ## Follow-up Actions -* If a homogeneous, same-type composition need appears, consider a `params`-based - `Combine` separately — it is orthogonal to this decision. -* Reflect the arity ceiling in the user documentation once the surface stabilizes. +* Keep the supported arity range explicit in the Dummies documentation. +* Consider homogeneous variadic composition separately if a real use case emerges. ## References -* ADR-0011 — Host Dummies as a standalone package in this repository. -* The S107 suppressions on the arity-seven and arity-eight `Combine` overloads. +* [ADR implementation reference — Dummies generation contracts](../specifications/adr-implementation-reference.md#dummies-generation-contracts) +* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From 142f04b6fc3f037e01c023045b6251e26ef8f1ee Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:51:51 +0200 Subject: [PATCH 21/33] docs: translate ADR-0015's editorial rewrite to French --- .../0015-cap-any-combine-at-arity-eight.fr.md | 143 ++++++------------ 1 file changed, 45 insertions(+), 98 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md index 9a4bae0d..8ffbe578 100644 --- a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md +++ b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md @@ -2,128 +2,75 @@ 🌍 🇬🇧 [English](0015-cap-any-combine-at-arity-eight.md) · 🇫🇷 Français (ce fichier) -**Statut :** Proposé -**Date :** 2026-07-18 +**Statut :** Accepté +**Date :** 2026-07-19 **Décideurs :** Reefact ## Contexte -`Dummies` compose des générateurs contraints en objets plus gros via -`Any.Combine(parties..., compose)` : les parties sont générées, puis remises à une -lambda constructeur fournie par l'appelant, de sorte qu'un objet-valeur ou un -agrégat est assemblé sans réflexion et que le constructeur du domaine reste le seul -gardien. Construire facilement de gros objets est un objectif explicite de la -bibliothèque. - -C# n'a pas de générique variadique hétérogène : passer *N* parties de types -différents en un seul appel exige une surcharge distincte par arité, chacune avec -*N*+1 paramètres de type générique et *N*+1 paramètres de valeur. Jusqu'ici, -`Combine` n'existait que pour deux et trois parties ; en composer davantage -imposait l'imbrication (`Combine(Combine(a, b, …), c, …)`) ou le passage par des -tuples, qui exposent tous deux un accès positionnel `Item1..ItemN` ou des lambdas -imbriquées au site d'appel. - -Le dépôt exécute une analyse SonarCloud dont la règle S107 signale une méthode à -plus de sept paramètres. Un `Combine` de sept parties a huit paramètres (sept -générateurs plus le composeur) et de huit parties, neuf — les deux plus grandes -surcharges franchissent donc ce seuil. La pratique établie du dépôt est de garder -l'analyse propre, en supprimant une règle en ligne avec une justification là où une -exception délibérée est faite. - -Un constructeur qui a besoin de beaucoup de parties est lui-même souvent un signal -de conception — un objet-valeur intermédiaire manquant. +Dummies compose des générateurs de types différents en objets plus larges au moyen de `Any.Combine`, en préservant la validation du domaine par les constructeurs sans recourir à la réflexion. + +C# ne dispose pas de génériques variadiques hétérogènes ; chaque arité supportée exige donc une surcharge publique distincte. Des arités trop faibles imposent des compositions imbriquées ou des tuples positionnels pour les constructeurs plus larges, tandis qu'une surface illimitée créerait une API et une documentation répétitives pour une valeur décroissante. + +Des constructeurs très larges peuvent également signaler l'absence de concepts intermédiaires dans le domaine. ## Décision -`Any.Combine` offre des surcharges de deux jusqu'à huit parties et s'arrête là, en -acceptant le *code smell* de nombre de paramètres et de génériques des deux plus -grandes surcharges comme un compromis délibéré en faveur d'un site d'appel de -composition plat et sans réflexion. +`Any.Combine` fournit des surcharges hétérogènes plates de l'arité deux à l'arité huit et s'arrête volontairement à ce seuil. ## Justification -* **Cela sert directement l'objectif « construire de gros objets ».** Un - `Combine(a, b, c, d, e, (…) => new Thing(…))` plat, avec des paramètres de lambda - nommés par l'appelant, se lit bien mieux que des `Combine` imbriqués ou un accès - tuple `Item1..ItemN`, et garde la composition sans réflexion — la raison d'être - même de `Combine`. -* **Huit est le bon endroit pour le plafond.** Huit parties couvrent la quasi- - totalité des constructeurs DDD écrits à la main ; au-delà, l'objet est assez - complexe pour que des objets-valeurs intermédiaires soient la conception plus - saine, donc un plafond à huit pousse vers cette structure plutôt que de lisser des - constructeurs arbitrairement larges. -* **Le smell est inhérent, pas accidentel.** Il n'existe pas de façon moins - « smell » de passer *N* parties de types différents en un seul appel ; le nombre - de paramètres et de génériques est le coût irréductible de la composition - hétérogène. Supprimer S107 avec une justification sur les deux plus grandes - surcharges enregistre ce compromis au niveau du code, et cet ADR enregistre - pourquoi il est acceptable. -* **S'arrêter avant seize garde la surface bornée.** Égaler le plafond de seize - arguments de `Func` ajouterait des surcharges maintenues à la main et entièrement - documentées dont la valeur marginale est faible et dont le coût en boilerplate est - réel ; la demande au-delà de huit ne le justifie pas. - -## Alternatives considérées - -### Ne garder que les arités deux et trois ; composer davantage par imbrication - -Considérée parce qu'elle n'ajoute aucune surface. Rejetée parce que l'imbrication -force un accès tuple positionnel (`Item1..ItemN`) ou des lambdas imbriquées au site -d'appel — illisible précisément là où la bibliothèque promet une construction facile -de gros objets. - -### Un builder fluide accumulant un tuple (`Combine(a).And(b).And(c)…`) - -Considérée comme moyen d'éviter une surcharge par arité. Rejetée parce que `.And` -aurait lui-même besoin d'une surcharge par arité source, et que le tuple accumulé -réexpose l'accès positionnel — elle échange une forme de boilerplate contre un site -d'appel pire. - -### Étendre jusqu'à l'arité seize - -Considérée par souci d'exhaustivité, pour égaler `Func`. Rejetée parce que les -constructeurs de neuf à seize parties sont un *code smell* que la bibliothèque ne -devrait pas lisser, et que chaque surcharge est une surface documentée et maintenue -à la main pour une demande réelle négligeable. - -### Un tableau `params` de générateurs de même type - -Considérée pour le cas homogène. Rejetée parce qu'elle ne fonctionne que si toutes -les parties partagent un même type et qu'elle perd le typage par partie — elle ne -sert pas le cas du constructeur hétérogène pour lequel `Combine` existe. Elle reste -une option orthogonale, ajoutable plus tard sans toucher à cette décision. +Un appel plat avec des paramètres de lambda nommés est nettement plus lisible qu'une composition imbriquée ou l'accès positionnel à un tuple pour les tailles d'objets courantes dans le code métier. + +Huit est un plafond pragmatique de confort, pas une propriété mathématique du DDD. Il couvre les cas visés de construction d'objets larges tout en maintenant une surface manuelle bornée et en laissant les constructeurs encore plus larges jouer leur rôle de signal de conception. + +Les avertissements de nombre de paramètres sur les plus grandes surcharges constituent un compromis local explicite, pas un relâchement général des règles de qualité du dépôt. + +Les signatures exactes, la documentation et les suppressions d'analyseurs sont des détails d'implémentation décrits dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) et la référence d'API de Dummies. + +## Alternatives envisagées + +### Conserver uniquement les plus petites surcharges + +Envisagé pour minimiser la surface d'API. Rejeté parce que les compositions plus larges deviennent nettement moins lisibles avec des lambdas imbriquées ou des membres positionnels de tuples. + +### Utiliser un builder fluent accumulant un tuple + +Envisagé pour éviter une surcharge par arité. Rejeté parce que cela déplace la même complexité dans le builder et continue d'exposer une structure positionnelle au point d'appel. + +### Étendre jusqu'à l'arité maximale de `Func` + +Envisagé pour la complétude. Rejeté parce que le coût de maintenance et la normalisation de constructeurs extrêmement larges dépassent le gain marginal de confort. + +### Accepter uniquement des générateurs homogènes via `params` + +Envisagé car cette forme est naturellement variadique. Rejeté parce qu'elle ne couvre pas les paramètres de constructeur de types différents pour lesquels `Combine` existe. ## Conséquences ### Positives -* Les gros objets-valeurs et agrégats se composent en un seul appel plat, lisible et - sans réflexion, avec des paramètres nommés par l'appelant. -* Le plafond oriente doucement les constructeurs très larges vers des objets-valeurs - intermédiaires. +* Les objets larges courants se composent en un appel lisible et sans réflexion. +* L'API de confort reste volontairement bornée. +* Les constructions extrêmement larges restent visibles comme problème potentiel de conception. ### Négatives -* Cinq surcharges de plus sur la façade, maintenues à la main et entièrement - documentées. -* Les deux plus grandes surcharges portent une suppression S107 en ligne — une - exception documentée et localisée à la règle du nombre de paramètres. +* Plusieurs surcharges maintenues à la main font partie de la surface publique. +* Les plus grandes surcharges exigent des suppressions localisées d'analyseurs. +* Le plafond est heuristique et peut ne pas convenir à tous les domaines. ### Risques -* **Pression sur le plafond** — un besoin réel de neuf parties ou plus pourrait - réapparaître. Atténué parce que c'est en soi un signal de conception ; le plafond - n'est réexaminé que si le besoin se révèle courant, et ajouter des arités - supérieures plus tard reste non-breaking. +* Un besoin légitime récurrent au-delà de l'arité huit peut apparaître. Mesure : des arités supérieures peuvent être ajoutées de manière compatible par une nouvelle décision si des faits montrent que le plafond actuel est trop bas. ## Actions de suivi -* Si un besoin de composition homogène (même type) apparaît, envisager un `Combine` - à base de `params` séparément — c'est orthogonal à cette décision. -* Refléter le plafond d'arité dans la documentation utilisateur une fois la surface - stabilisée. +* Rendre explicite la plage d'arités supportée dans la documentation de Dummies. +* Étudier séparément une composition variadique homogène si un cas réel apparaît. ## Références -* ADR-0011 — Héberger Dummies comme un paquet autonome dans ce dépôt. -* Les suppressions S107 sur les surcharges `Combine` d'arité sept et huit. +* [Référence d'implémentation des ADR — Contrats de génération de Dummies](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) +* [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 055686d232f29617d465a565956e4365d6f8efc2 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:52:12 +0200 Subject: [PATCH 22/33] docs: separate the binder catalog decision from its documentation seams --- ...-binder-errors-in-the-consumers-catalog.md | 119 +++++------------- 1 file changed, 34 insertions(+), 85 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md index f3ced31f..d83f8224 100644 --- a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md +++ b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md @@ -8,118 +8,67 @@ ## Context -* ADR-0018 (which superseded ADR-0016) made the binder's two structural errors configurable - as a `BinderErrorDefinition` — code and public messages together — on - `RequestBinderOptions`. Both ADRs deferred one question to issue #140: how a consumer's - generated catalog surfaces the binder's — possibly overridden — codes without drifting - from what it emits at runtime. -* The documentation generator documents an error from a static factory carrying - `[DocumentedBy]` inside a `[ProvidesErrorsFor]` type: it runs the named documentation - method (the prose) and the example it holds (a live error), reading the code, messages and - context off the built error. It discovers these types by scanning the solution's opted-in - projects; it does not scan referenced packages. -* The binder manufactures its two structural errors itself — their factories are internal — - and already documents its own defaults in its own package catalog. -* When a consumer overrides a definition, it owns the effective code and messages: they live - in the consumer's own assembly (the `BinderErrorDefinition` it injects into the options) - and are runtime configuration, not statically discoverable from the binder's binary. -* The binder's structural-error prose (title, rule, diagnoses) is code-independent: it - describes the meaning of "a required argument was missing" whatever code carries it. -* The library's coded-error model documents an error where it is defined, through - `[ProvidesErrorsFor]` / `[DocumentedBy]`, and forbids referencing an error by a magic - string. -* Exactly one package ships documented, emittable codes today (`FirstClassErrors.RequestBinder`), - and the library is pre-release with no external consumers. +ADR-0018 made the Request Binder's structural errors configurable as consumer-owned definitions containing their effective codes and messages. + +The documentation generator discovers documented errors from opted-in consumer projects, not from runtime configuration inside referenced packages. A package binary can expose only its defaults, while an overriding consumer owns the values it actually emits. + +The meaning of the binder's structural failures is stable and package-owned, but their effective identities and messages may be consumer-owned. + +The repository's catalog model documents errors where they are defined and avoids untyped string links between an error and its documentation. ## Decision -A consumer surfaces its overridden binder structural errors in its own generated catalog by -documenting them in its own `[ProvidesErrorsFor]` type — built from public binder seams that -reuse the binder's code-independent prose and build a faithful example from the consumer's -definition — rather than the generator auto-discovering a referenced package's codes. +A consumer that overrides Request Binder structural-error definitions documents those effective errors in its own generated catalog through compile-safe binder-provided documentation seams rather than through automatic discovery of referenced-package catalogs. ## Rationale -* The consumer owns the effective codes (ADR-0018), so documenting them in its own catalog — - where the coded-error model documents every other owned error — keeps one consistent rule - instead of a special cross-package path. -* Reusing the binder's code-independent prose through public seams means the consumer writes - no description text and constructs no error by hand: the mechanical facts (code, messages, - transience, the argument-path context key, inner-error wiring) come from the binder building - the sample the same way it does at binding time, so the documented entry cannot drift from - what is emitted — closing the exact risk #140 named. -* The generator needs no change: the consumer's catalog is an ordinary - `[ProvidesErrorsFor]` / `[DocumentedBy]` type in an already-scanned project. This avoids the - machinery an auto-discovery step would need — resolving a project's reference closure, a - metadata pre-check to skip non-documenting assemblies, running the worker over third-party - binaries, a documentation-contract-version gate, and a code-collision policy — and its - correctness hazards, since a referenced package is not always emitted on the public surface - and its defaults are wrong for a consumer that overrode them. -* Keeping the link between an error and its description in code, through `nameof` / `typeof`, - leaves it under the compiler — consistent with the library's stance against magic strings — - where a link expressed in build configuration would reintroduce them. -* With one documented-code package and no consumers, the small per-consumer glue is an - acceptable price, and the options surface and documentation seams are settled before any - auto-discovery is committed to. +The consumer owns the effective codes and messages, so its own catalog is the only place that can faithfully describe what the application emits at runtime. -## Alternatives Considered +Binder-provided seams allow the consumer to reuse the stable prose and create representative errors through the same package-owned behavior without copying descriptions or manually reconstructing error shapes. -### Auto-discover a referenced package's documented codes +Keeping the catalog entry in the consumer's ordinary `[ProvidesErrorsFor]` / `[DocumentedBy]` flow avoids a special cross-package discovery mechanism, package-closure analysis, collision policy, and the risk of documenting defaults that the application no longer uses. -Considered because it is zero-boilerplate: a consumer references the package and its codes -appear in the consumer's catalog. +Compile-safe code links preserve the repository's stance against magic strings and remain refactorable by normal tooling. + +The exact public members, analyzer suppressions, and consumer examples are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#documentation-only-public-surfaces) and the Request Binder documentation. + +## Alternatives Considered -Rejected because it is unnecessary and misleading. The only package that ships emittable codes -is `FirstClassErrors.RequestBinder`, whose binary carries only the defaults — so auto-discovery -could never surface a consumer's overrides (these seams do), and for an overriding consumer it -would document the default codes it no longer emits. Scoping it to that one package removes none -of this: the limitation is that overrides live in the consumer's assembly, not the package's. It -would buy only sparing a zero-config consumer the small glue that documents the defaults. +### Auto-discover documented codes from referenced packages -### A build-configuration link binding an error to a description method +Considered because it removes consumer glue for package defaults. Rejected because referenced binaries cannot reveal consumer overrides and could therefore document codes the application does not emit. -Considered because it moves the documentation wiring out of code into project configuration. +### Link documentation through build configuration -Rejected because it would reference members by string in project files — reintroducing the -magic strings the coded-error model exists to eliminate, with no compile-time check, -navigation, or refactoring safety. A compile-safe assembly attribute would be the fallback if -a declarative link is ever wanted. +Considered to keep the wiring outside code. Rejected because member names would become unchecked strings with poor navigation and refactoring safety. ## Consequences ### Positive -* The override half of #140 is closed: a consumer documents exactly what it emits, faithful to - runtime, with the binder's prose and no change to the generator. -* The link between an error and its documentation stays compile-safe and in code, consistent - with the library's magic-string stance. +* A consumer documents exactly the binder errors it emits. +* Stable prose and representative error construction are reused instead of duplicated. +* The generator and its discovery model remain unchanged. +* Documentation links stay compile-safe. ### Negative -* A consumer folds the binder's codes into its catalog with a small per-error glue type (a - `[ProvidesErrorsFor]` type delegating to the public seams); there is no zero-boilerplate - auto-discovery. -* The binder grows four public members (a describe and a sample seam per structural error). - The two sample seams return an error without `[DocumentedBy]` and are suppressed against - FCE009 as deliberate non-catalog helpers. +* Consumers add a small amount of explicit catalog glue. +* The binder exposes a limited public surface whose main purpose is documentation support. ### Risks -* A consumer could call a sample seam to manufacture a structural error outside documentation. - It produces the same shape the binder emits and is injected nowhere, so it is inert — no - different from building any error through the public `PrimaryPortError.Create`. +* Documentation-oriented public members could expand the runtime API unnecessarily. Mitigation: keep the surface minimal, stable, and tied to the catalog contract; reconsider metadata or generator-side alternatives before adding more. +* A consumer could call a sample seam outside documentation. Mitigation: samples are inert error values and do not alter binder behavior. ## Follow-up Actions -* None. Auto-discovery is not needed (see Alternatives): the one package that ships emittable - codes is covered by these seams. +* Review future documentation-only public members against the minimization rule in the implementation reference. ## References -* ADR-0018 — bundle the binder's structural error code and messages in one definition; the - ownership this decision builds on. -* ADR-0016 — make the binder's structural error codes configurable (superseded); it first - deferred this concern to #140. -* Issue #140 — document error codes from referenced FirstClassErrors packages in a consumer's - catalog. -* FCE009 — `ErrorFactoryNotDocumented`, the analyzer the sample seams are suppressed against. +* [ADR implementation reference — Documentation-only public surfaces](../specifications/adr-implementation-reference.md#documentation-only-public-surfaces) +* [ADR-0018](0018-bundle-the-binders-structural-error-code-and-messages.md) +* [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.md) — superseded origin of the deferred question. +* Issue #140 and analyzer FCE009. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From cff4d1ef56a358014d6de257f12ed0852b246c47 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:52:34 +0200 Subject: [PATCH 23/33] docs: translate ADR-0019's editorial rewrite to French --- ...nder-errors-in-the-consumers-catalog.fr.md | 140 +++++------------- 1 file changed, 41 insertions(+), 99 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md index 462f0125..9d11e094 100644 --- a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md +++ b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md @@ -8,125 +8,67 @@ ## Contexte -* L’ADR-0018 (qui a remplacé l’ADR-0016) a rendu les deux erreurs structurelles du binder - configurables comme un `BinderErrorDefinition` — code et messages publics ensemble — sur - `RequestBinderOptions`. Les deux ADR ont différé une question vers l’issue #140 : comment le - catalogue généré d’un consommateur fait apparaître les codes du binder — éventuellement - surchargés — sans dériver de ce qu’il émet au runtime. -* Le générateur de documentation documente une erreur à partir d’une fabrique statique portant - `[DocumentedBy]` dans un type `[ProvidesErrorsFor]` : il exécute la méthode de documentation - nommée (la prose) et l’exemple qu’elle porte (une erreur vivante), en lisant le code, les - messages et le contexte sur l’erreur construite. Il découvre ces types en scannant les - projets opt-in de la solution ; il ne scanne pas les paquets référencés. -* Le binder fabrique lui-même ses deux erreurs structurelles — leurs fabriques sont internes — - et documente déjà ses propres défauts dans le catalogue de son paquet. -* Quand un consommateur surcharge une définition, il possède le code et les messages effectifs : - ils vivent dans son propre assembly (le `BinderErrorDefinition` injecté dans les options) et - sont de la configuration runtime, non découvrable statiquement depuis le binaire du binder. -* La prose des erreurs structurelles du binder (titre, règle, diagnostics) est indépendante du - code : elle décrit le sens de « un argument requis manquait » quel que soit le code qui le - porte. -* Le modèle d’erreurs codées de la bibliothèque documente une erreur là où elle est définie, via - `[ProvidesErrorsFor]` / `[DocumentedBy]`, et interdit de référencer une erreur par une chaîne - magique. -* Un seul paquet livre aujourd’hui des codes documentés et émissibles - (`FirstClassErrors.RequestBinder`), et la bibliothèque est en pré-version sans consommateur - externe. +L'ADR-0018 a rendu configurables les erreurs structurelles du Request Binder sous forme de définitions appartenant au consommateur et contenant les codes et messages effectifs. + +Le générateur de documentation découvre les erreurs documentées dans les projets consommateurs explicitement inclus, pas dans la configuration d'exécution de packages référencés. Le binaire d'un package ne peut exposer que ses valeurs par défaut, alors qu'un consommateur qui les remplace possède les valeurs réellement émises. + +Le sens des erreurs structurelles du binder est stable et appartient au package, mais leurs identités et messages effectifs peuvent appartenir au consommateur. + +Le modèle de catalogue du dépôt documente les erreurs là où elles sont définies et évite les liens non typés par chaînes de caractères entre une erreur et sa documentation. ## Décision -Un consommateur fait apparaître ses erreurs structurelles de binder surchargées dans son propre -catalogue généré en les documentant dans son propre type `[ProvidesErrorsFor]` — bâti à partir -de seams publics du binder qui réutilisent la prose indépendante du code et construisent un -exemple fidèle depuis la définition du consommateur — plutôt que par une découverte automatique -des codes d’un paquet référencé par le générateur. +Un consommateur qui remplace les définitions d'erreurs structurelles du Request Binder documente ces erreurs effectives dans son propre catalogue généré au moyen de points d'extension documentaires du binder vérifiés par le compilateur, plutôt que par découverte automatique des catalogues de packages référencés. ## Justification -* Le consommateur possède les codes effectifs (ADR-0018) : les documenter dans son propre - catalogue — là où le modèle d’erreurs codées documente toute autre erreur possédée — garde une - règle unique plutôt qu’un chemin cross-paquet spécial. -* Réutiliser la prose indépendante du code via des seams publics fait que le consommateur n’écrit - aucun texte de description et ne construit aucune erreur à la main : les faits mécaniques (code, - messages, transience, clé de contexte du chemin d’argument, câblage de l’erreur interne) - viennent du binder qui bâtit l’exemple comme à la liaison, si bien que l’entrée documentée ne - peut pas dériver de ce qui est émis — fermant le risque même que #140 nommait. -* Le générateur n’a pas besoin de changer : le catalogue du consommateur est un type - `[ProvidesErrorsFor]` / `[DocumentedBy]` ordinaire dans un projet déjà scanné. Cela évite la - machinerie qu’exigerait une étape de découverte automatique — résoudre la clôture de références - d’un projet, un pré-check métadonnée pour écarter les assemblys non documentants, exécuter le - worker sur des binaires tiers, une garde de version de contrat de documentation et une - politique de collision de codes — et ses pièges de justesse, puisqu’un paquet référencé n’est - pas toujours émis sur la surface publique et que ses défauts sont faux pour un consommateur qui - les a surchargés. -* Garder le lien entre une erreur et sa description dans le code, via `nameof` / `typeof`, le - laisse sous l’œil du compilateur — cohérent avec la position de la bibliothèque contre les - chaînes magiques — là où un lien exprimé en configuration de build les réintroduirait. -* Avec un seul paquet à codes documentés et aucun consommateur, la petite glue par consommateur - est un prix acceptable, et la surface des options comme les seams de documentation sont arrêtés - avant de s’engager sur une découverte automatique. - -## Alternatives considérées - -### Découvrir automatiquement les codes documentés d’un paquet référencé - -Considérée parce qu’elle est sans boilerplate : un consommateur référence le paquet et ses codes -apparaissent dans son catalogue. - -Rejetée parce qu’elle est inutile et trompeuse. Le seul paquet qui livre des codes émissibles est -`FirstClassErrors.RequestBinder`, dont le binaire ne porte que les défauts — l’auto-découverte ne -pourrait donc jamais faire apparaître les surcharges d’un consommateur (ce que font ces seams), et -pour un consommateur qui surcharge elle documenterait les codes par défaut qu’il n’émet plus. La -restreindre à ce seul paquet n’y change rien : la limite est que les surcharges vivent dans -l’assembly du consommateur, pas dans celui du paquet. Elle n’économiserait qu’une petite glue au -consommateur zéro-config qui documente les défauts. - -### Un lien en configuration de build reliant une erreur à une méthode de description - -Considérée parce qu’elle sort le câblage de documentation du code vers la configuration du projet. - -Rejetée parce qu’elle référencerait des membres par chaîne dans les fichiers projet — -réintroduisant les chaînes magiques que le modèle d’erreurs codées existe pour éliminer, sans -contrôle à la compilation, ni navigation, ni sûreté au renommage. Un attribut d’assembly -compile-safe serait le repli si un lien déclaratif était un jour souhaité. +Le consommateur possède les codes et messages effectifs ; son propre catalogue est donc le seul emplacement capable de décrire fidèlement ce que l'application émet à l'exécution. + +Les points d'extension fournis par le binder permettent au consommateur de réutiliser la prose stable et de créer des erreurs représentatives avec le comportement du package, sans recopier les descriptions ni reconstruire manuellement leur forme. + +Conserver l'entrée de catalogue dans le flux ordinaire `[ProvidesErrorsFor]` / `[DocumentedBy]` du consommateur évite un mécanisme spécial de découverte inter-packages, l'analyse de la fermeture des références, une politique de collisions et le risque de documenter des valeurs par défaut que l'application n'utilise plus. + +Des liens exprimés en code et vérifiés par le compilateur préservent la position du dépôt contre les magic strings et restent refactorables par les outils habituels. + +Les membres publics exacts, les suppressions d'analyseurs et les exemples consommateurs sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#surfaces-publiques-uniquement-destinées-à-la-documentation) et la documentation du Request Binder. + +## Alternatives envisagées + +### Découvrir automatiquement les codes documentés des packages référencés + +Envisagé pour supprimer le glue consommateur pour les valeurs par défaut du package. Rejeté parce que les binaires référencés ne peuvent révéler les surcharges du consommateur et pourraient donc documenter des codes que l'application n'émet pas. + +### Relier la documentation via la configuration de build + +Envisagé pour conserver le câblage hors du code. Rejeté parce que les noms de membres deviendraient des chaînes non vérifiées, peu navigables et fragiles au refactoring. ## Conséquences ### Positives -* La moitié « surcharge » de #140 est fermée : un consommateur documente exactement ce qu’il émet, - fidèle au runtime, avec la prose du binder et sans changer le générateur. -* Le lien entre une erreur et sa documentation reste compile-safe et dans le code, cohérent avec - la position de la bibliothèque sur les chaînes magiques. +* Un consommateur documente exactement les erreurs du binder qu'il émet. +* La prose stable et la construction d'erreurs représentatives sont réutilisées plutôt que dupliquées. +* Le générateur et son modèle de découverte restent inchangés. +* Les liens documentaires restent vérifiés par le compilateur. ### Négatives -* Un consommateur fait entrer les codes du binder dans son catalogue avec un petit type de glue - par erreur (un type `[ProvidesErrorsFor]` déléguant aux seams publics) ; il n’y a pas de - découverte automatique sans boilerplate. -* Le binder gagne quatre membres publics (un seam de description et un d’exemple par erreur - structurelle). Les deux seams d’exemple renvoient une erreur sans `[DocumentedBy]` et sont - supprimés contre FCE009 comme helpers délibérément hors catalogue. +* Les consommateurs ajoutent une petite quantité de glue explicite dans leur catalogue. +* Le binder expose une surface publique limitée dont l'objectif principal est le support documentaire. ### Risques -* Un consommateur pourrait appeler un seam d’exemple pour fabriquer une erreur structurelle hors - documentation. Il produit la même forme que ce que le binder émet et n’est injecté nulle part : - il est donc inerte — pas différent de bâtir n’importe quelle erreur via le `PrimaryPortError.Create` - public. +* Les membres publics destinés à la documentation pourraient étendre inutilement l'API d'exécution. Mesure : conserver une surface minimale, stable et liée au contrat du catalogue ; réexaminer les alternatives par métadonnées ou côté générateur avant tout ajout. +* Un consommateur pourrait appeler un point d'exemple hors documentation. Mesure : les exemples sont des valeurs d'erreur inertes et ne modifient pas le comportement du binder. ## Actions de suivi -* Aucune. L’auto-découverte n’est pas nécessaire (voir Alternatives) : le seul paquet à codes - émissibles est couvert par ces seams. +* Examiner tout futur membre public destiné à la documentation au regard de la règle de minimisation de la référence d'implémentation. ## Références -* ADR-0018 — regrouper le code et les messages d’une erreur structurelle du binder dans une seule - définition ; l’appropriation sur laquelle cette décision s’appuie. -* ADR-0016 — rendre configurables les codes d’erreur structurels du binder (remplacée) ; elle a - d’abord différé ce sujet vers #140. -* Issue #140 — documenter les codes d’erreur des paquets FirstClassErrors référencés dans le - catalogue d’un consommateur. -* FCE009 — `ErrorFactoryNotDocumented`, l’analyseur contre lequel les seams d’exemple sont - supprimés. +* [Référence d'implémentation des ADR — Surfaces publiques uniquement destinées à la documentation](../specifications/adr-implementation-reference.fr.md#surfaces-publiques-uniquement-destinées-à-la-documentation) +* [ADR-0018](0018-bundle-the-binders-structural-error-code-and-messages.fr.md) +* [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.fr.md) — origine remplacée de la question différée. +* Issue #140 et analyseur FCE009. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From f187fa04d738a406338caf4bc4f03262ef230545 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:53:00 +0200 Subject: [PATCH 24/33] docs: accept and simplify the source-agnostic binder ADR --- ...s-peers-through-a-source-agnostic-entry.md | 180 +++++------------- 1 file changed, 44 insertions(+), 136 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md index df437b56..0aa37756 100644 --- a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md +++ b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md @@ -2,180 +2,88 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md) -**Status:** Proposed +**Status:** Accepted **Date:** 2026-07-19 **Decision Makers:** Reefact ## Context -* The binder built a command from a single request DTO. Its entry point was - DTO-first — a binder was *started over* a DTO, and the failure envelope was - declared on the DTO as a second step. There was no seam for an input that does - not live in the DTO. -* Real primary adapters routinely assemble a command from more than the body: a - route identifier, a query parameter, a request header, a claim. A host has - already extracted these individual values; they need the same collect-all, - coded, path-carrying binding the DTO's properties get, into the **same** - envelope — so a bad route segment and a bad body field are reported together. -* The binder is framework-agnostic (HTTP controllers, message consumers, CLIs, - gRPC handlers). Extracting a value from an incoming HTTP request is host - knowledge; the library sees already-extracted values. -* A DTO property's error path is derived by reflection from the C# property name - (through the configured `IArgumentNameProvider`); an out-of-DTO value has no - property to reflect over, so its path must be stated by the caller. -* A DTO property's provenance is uniform and implicit — every property comes from - the one request body. An out-of-DTO value's provenance is not: "route", "query" - and "header" are distinct origins the caller had to state, and which are useful - in diagnostics to tell one failing input from another. -* Naming the command type at the entry point and inferring it everywhere else and - keeping a single binder type cannot all three hold at once: with the command - type fixed on a generic entry, a nested complex binding puts the nested type in - a delegate *parameter* position, where C# method-group inference cannot recover - it — forcing an explicit type argument at every nested call site. -* A complex DTO property is, by construction, a path into a DTO; an out-of-DTO - argument has no DTO to path into. -* Errors carry a typed context; a public accessor key lets a consumer read a - context entry without depending on its internal name. -* The library is pre-release, unpublished on NuGet with no external consumers, so - the entry-point surface can still change without a migration. -* ADR-0007 named the build terminals `New` / `Create`; ADR-0012 fixed a binder's - options at its entry point; ADR-0017 made the application-wide options default - settable. All three describe the entry point in its previous DTO-first shape. +Primary adapters commonly assemble a command from several input origins: a request body, route values, query parameters, headers, claims, or message metadata. + +The original Request Binder was DTO-first and had no natural place for an already-extracted value that did not belong to the DTO. Wrapping every such value in a synthetic DTO would distort its path and provenance. + +All inputs must participate in the same collect-all binding result so failures from the body and from external arguments can be reported together with consistent structural errors and paths. + +A DTO property can derive its name from reflection; an out-of-DTO argument must state its own path and can also carry provenance that distinguishes origins such as route, query, or header. + +Typing the entry point on the eventual command conflicts with preserving method-group inference for nested bindings. The command type is only required when the final object is constructed. ## Decision -The binder is source-agnostic: its untyped entry point declares the failure -envelope up front and attaches inputs as peers — a DTO through a property source, -and individually named out-of-DTO arguments (each stating its provenance) through -an argument source — with the command type named only at the `New` / `Create` -terminal, and with no complex out-of-DTO argument. +The Request Binder uses a source-agnostic untyped entry that declares the failure envelope first, attaches DTO property sources and individually named out-of-DTO argument sources as peers, and names the command type only at the `New` or `Create` terminal. ## Rationale -* Making the entry declare the envelope and attach the DTO and the arguments as - peers is what lets a route/query/header value bind into the *same* envelope, - with the *same* paths and the *same* two structural codes, as a body property — - which is the requirement. A DTO-first entry has no place to put an input that is - not in the DTO; a peer model does. -* The entry is untyped because the source-agnostic model removes the reason to - type it. The command is no longer "built over a DTO"; it is assembled at the - terminal from peers. Naming the command type at the terminal resolves the - three-way tension in Context: the terminal infers it from the assembler, so it - is named nowhere else, one binder type serves top-level and nested binding - alike, and — because the nested type now appears only in the nested binding's - *return* position — method-group inference recovers it with no explicit type - argument. Intention is still expressed, by the envelope factory's name and the - command's own type, not by a redundant type parameter on the entry. -* An out-of-DTO argument states its own name because there is no property to - derive it from; the name is used verbatim as the path, so the caller controls - the wire key directly, exactly where a DTO property defers to the name provider. -* Capturing an argument's provenance, and *only* an argument's, matches where the - information exists and is worth keeping: an argument's origin was stated by the - caller and distinguishes otherwise-similar failures, whereas a DTO property's - origin is the single implicit body and would be noise on every property. The - asymmetry is deliberate, not an omission. -* Reusing the existing converter surface (`AsRequired`, `AsOptional`, the value - and reference optionals, and the list form) for arguments keeps one mental model - for every input: an argument differs from a property only in how it is named and - sourced, never in how it is bound. -* Omitting a complex out-of-DTO argument is a consequence of what the two concepts - are, not a feature cut: a complex property dereferences a DTO path, and an - argument has no DTO to dereference. A complex value assembled from several - arguments is expressed by binding each as a peer and combining them in the - terminal — no new concept required. -* The pre-release status means the entry-point shape is settled now, when there - are no consumers to migrate. +Treating sources as peers allows all failures to accumulate in one envelope regardless of where the host extracted the value. -## Alternatives Considered +The untyped entry removes a redundant command type from intermediate binding steps and preserves inference for nested bindings, while the terminal still states the constructed type through the assembler. + +An argument must own its explicit path because no reflected property exists. Its provenance remains a separate typed diagnostic fact rather than being encoded into the path string. -### Keep the DTO-first entry and treat an out-of-DTO value as a synthetic one-property DTO +Reusing the same converter vocabulary for properties and arguments preserves one mental model: sources differ in naming and provenance, not in validation or conversion. -Considered because it reuses the existing property path unchanged. +Complex values assembled from multiple loose arguments are expressed by binding each argument as a peer and composing them at the terminal. The decision does not prohibit a future first-class complex argument if a concrete host-agnostic semantic emerges; it only declines to invent one without such a referent. -Rejected because it forces the caller to wrap each loose value in a throwaway DTO -purely to satisfy the entry shape, and the reflection-derived path then reports -that wrapper's property name rather than the caller's intended wire key — the -inverse of what an out-of-DTO argument needs. +Exact entry types, source APIs, provenance helpers, generic signatures, and examples are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) and the Request Binder guide. -### Type the entry point on the command (a generic `Bind.To`) +## Alternatives Considered -Considered because it states the target type at the top, reads as intention-first, -and keeps a single binder type. +### Keep the DTO-first entry and wrap arguments in synthetic DTOs -Rejected because, with the command type fixed on the entry, a nested complex -binding puts the nested type in a delegate parameter position where method-group -inference cannot recover it, so every nested call site must spell out an explicit -type argument — a persistent papercut across the most common binder shape. The -untyped entry removes the papercut while still expressing intention through the -envelope and the command type. +Considered because it reuses the existing path. Rejected because it adds ceremony and reports wrapper-property names rather than the caller's actual wire keys. -### Add a complex out-of-DTO argument mirroring the complex property +### Type the entry point on the command -Considered for symmetry with the DTO side, so arguments and properties would offer -the same shapes. +Considered because it makes the target explicit early. Rejected because it degrades nested method-group inference and forces repeated explicit type arguments. -Rejected because it has no referent: a complex property binds a nested DTO reached -by a path, and an out-of-DTO argument has no DTO and no path. The symmetric-looking -API would name a thing that does not exist; the existing peers-plus-terminal -composition already covers "a complex value built from arguments". +### Add a complex out-of-DTO argument immediately -### Encode provenance in the argument's error path (for example `route:bookingId`) +Considered for symmetry with complex DTO properties. Rejected because a complex property represents a path into a DTO, while a loose argument has no equivalent object graph to traverse. Peer composition already covers construction from several loose values. -Considered because it needs no second context key. +### Encode provenance into the error path -Rejected because it conflates two distinct facts — *where* the input is (the path, -used to locate the field) and *what kind of origin* it has (the source, used to -classify the failure) — into one string that consumers would then have to parse, -the very message-parsing the binder exists to avoid. A separate, typed context key -keeps both facts first-class. +Considered to avoid a second context value. Rejected because path and origin answer different questions and should remain independently typed. ## Consequences ### Positive -* A command assembled from a body and any mix of route/query/header values binds - in one pass, into one envelope, with one set of paths and codes. -* The nested-binding call site needs no explicit type argument; one binder type - serves top-level and nested binding. -* An argument failure carries its provenance, so a consumer can classify failures - (a bad route vs a bad header) without parsing messages. -* The converter surface is unchanged, so an argument is bound with exactly the - verbs a property is. +* Body, route, query, header, and similar values bind together in one result. +* Nested binding retains method-group inference without explicit type arguments. +* Argument failures carry typed provenance without message parsing. +* Properties and arguments reuse the same converter model. ### Negative -* The entry-point shape changes from the DTO-first form that ADR-0007, ADR-0012 - and ADR-0017 describe in prose; those ADRs' decisions are unaffected, but their - illustrative surface is now historical. -* Two ways to attach an input (property source, argument source) are a slightly - larger surface than one — accepted because they model two genuinely different - provenances, not two flavours of the same thing. +* The entry-point shape differs from the DTO-first examples in earlier ADRs and documentation. +* The public surface now distinguishes property sources from argument sources. +* Callers must provide stable names and provenance conventions for loose arguments. ### Risks -* A caller could reach for a non-existent "complex argument" and be briefly - surprised by its absence; mitigated by documenting the peers-plus-terminal - composition as the intended way to build a complex value from arguments. -* Free-form provenance labels could drift across a codebase ("route" vs "path"); - mitigated by the provenance-shortcut helpers (`FromRoute`, `FromQuery`, …) that - fix the common labels, leaving the raw `From(source, …)` for the rest. +* Provenance labels could drift across an application. Mitigation: provide and document standard shortcuts for common origins. +* Users may expect a complex-argument API from visual symmetry alone. Mitigation: document peer composition and revisit only from a concrete use case. ## Follow-up Actions -* Update the request-binder guide (EN + FR) and the package README to the - source-agnostic entry and the out-of-DTO argument section. -* Consider a host-integration package that extracts values from an incoming HTTP - request (rather than tagging already-extracted ones), if consumer demand - appears. +* Keep the bilingual Request Binder guide and package README aligned with the source-agnostic entry. +* Evaluate host-integration packages separately if consumers need framework-specific extraction rather than binding of already-extracted values. ## References -* ADR-0007 — name the binder terminals New and Create; the terminal that now also - carries the command type parameter. Unchanged decision. -* ADR-0012 — fix the binder options before binding begins; the options are still - fixed at the (now source-agnostic) entry point. Unchanged decision. -* ADR-0017 — provide a configurable application-wide default for the binder - options; the default still backs the bare entry point. Unchanged decision. -* Issue #148 — the request this decision resolves. -* [`fluent-request-binder`](https://github.com/Reefact/fluent-request-binder) — - the prior-art binder whose source/argument model informed this decision. +* [ADR implementation reference — Request Binder implementation contracts](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) +* [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) — terminal naming remains valid; illustrative entry shape updated by this ADR. +* [ADR-0012](0012-fix-the-binder-options-before-binding-begins.md) — options remain fixed at the source-agnostic entry; illustrative API shape updated by this ADR. +* [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) — the default remains valid; illustrative API shape updated by this ADR. +* Issue #148. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From 687172df2b29784ee52ff6b19471740b2ce82858 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:53:30 +0200 Subject: [PATCH 25/33] docs: translate ADR-0021's editorial rewrite to French --- ...eers-through-a-source-agnostic-entry.fr.md | 222 +++++------------- 1 file changed, 58 insertions(+), 164 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md index 15c1e1b3..f34fe63a 100644 --- a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md +++ b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md @@ -1,195 +1,89 @@ -# ADR-0021 | Lier les arguments hors-DTO comme des pairs via un point d’entrée agnostique de la source et non typé +# ADR-0021 | Lier les arguments hors DTO comme des pairs via un point d'entrée agnostique de la source et non typé 🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md) -**Statut :** Proposé +**Statut :** Accepté **Date :** 2026-07-19 **Décideurs :** Reefact ## Contexte -* Le binder construisait une commande à partir d’un unique DTO de requête. Son - point d’entrée était centré-DTO — un binder était *démarré sur* un DTO, et - l’enveloppe d’échec était déclarée sur le DTO en deuxième temps. Il n’existait - aucune couture pour une entrée qui ne vit pas dans le DTO. -* De vrais adaptateurs primaires assemblent régulièrement une commande à partir de - plus que le corps : un identifiant de route, un paramètre de query, un en-tête de - requête, un claim. L’hôte a déjà extrait ces valeurs individuelles ; elles ont - besoin de la même liaison — collecte exhaustive, codée, porteuse de chemin — que - les propriétés du DTO, dans la **même** enveloppe — pour qu’un mauvais segment de - route et un mauvais champ de corps soient rapportés ensemble. -* Le binder est agnostique du framework (contrôleurs HTTP, consommateurs de - messages, CLI, handlers gRPC). Extraire une valeur d’une requête HTTP entrante - relève de la connaissance de l’hôte ; la bibliothèque voit des valeurs déjà - extraites. -* Le chemin d’erreur d’une propriété de DTO est dérivé par réflexion depuis le nom - de propriété C# (via l’`IArgumentNameProvider` configuré) ; une valeur hors-DTO - n’a pas de propriété sur laquelle réfléchir, son chemin doit donc être indiqué par - l’appelant. -* La provenance d’une propriété de DTO est uniforme et implicite — chaque propriété - vient de l’unique corps de requête. La provenance d’une valeur hors-DTO ne l’est - pas : « route », « query » et « header » sont des origines distinctes que - l’appelant a dû indiquer, et qui sont utiles au diagnostic pour distinguer une - entrée en échec d’une autre. -* Nommer le type de commande au point d’entrée, l’inférer partout ailleurs, et - garder un seul type de binder ne peuvent pas tenir tous les trois à la fois : - avec le type de commande fixé sur une entrée générique, une liaison complexe - imbriquée place le type imbriqué en position de *paramètre* de délégué, où - l’inférence de groupe de méthodes de C# ne peut pas le récupérer — forçant un - argument de type explicite à chaque site d’appel imbriqué. -* Une propriété complexe de DTO est, par construction, un chemin dans un DTO ; un - argument hors-DTO n’a pas de DTO où cheminer. -* Les erreurs portent un contexte typé ; une clé d’accès publique permet à un - consommateur de lire une entrée de contexte sans dépendre de son nom interne. -* La bibliothèque est en pré-release, non publiée sur NuGet et sans consommateur - externe : la surface du point d’entrée peut donc encore changer sans migration. -* L’ADR-0007 a nommé les terminaux de construction `New` / `Create` ; l’ADR-0012 a - fixé les options d’un binder à son point d’entrée ; l’ADR-0017 a rendu réglable le - défaut d’options applicatif. Les trois décrivent le point d’entrée dans sa forme - précédente, centrée-DTO. +Les adaptateurs primaires assemblent couramment une commande à partir de plusieurs origines : corps de requête, valeurs de route, paramètres de requête, en-têtes, claims ou métadonnées de message. + +Le Request Binder d'origine partait du DTO et ne disposait d'aucun emplacement naturel pour une valeur déjà extraite qui n'appartenait pas à ce DTO. Envelopper chaque valeur dans un DTO synthétique déformerait son chemin et sa provenance. + +Toutes les entrées doivent participer au même résultat collectant les erreurs afin que les échecs du corps et des arguments externes soient rapportés ensemble avec des erreurs structurelles et des chemins cohérents. + +Une propriété de DTO peut dériver son nom par réflexion ; un argument hors DTO doit déclarer son propre chemin et peut également porter une provenance distinguant route, query ou header. + +Typer le point d'entrée sur la commande finale entre en conflit avec la préservation de l'inférence des groupes de méthodes pour les bindings imbriqués. Le type de commande n'est requis qu'au moment de construire l'objet final. ## Décision -Le binder est agnostique de la source : son point d’entrée non typé déclare -l’enveloppe d’échec d’emblée et attache les entrées comme des pairs — un DTO via une -source de propriétés, et des arguments hors-DTO nommés individuellement (chacun -indiquant sa provenance) via une source d’arguments — le type de commande n’étant -nommé qu’au terminal `New` / `Create`, et sans argument hors-DTO complexe. +Le Request Binder utilise un point d'entrée non typé et agnostique de la source qui déclare d'abord l'enveloppe d'échec, attache comme pairs des sources de propriétés de DTO et des sources d'arguments hors DTO nommés individuellement, puis ne nomme le type de commande qu'au terminal `New` ou `Create`. ## Justification -* Faire déclarer l’enveloppe par l’entrée et attacher le DTO et les arguments comme - des pairs est ce qui permet à une valeur de route/query/en-tête de se lier dans la - *même* enveloppe, avec les *mêmes* chemins et les *mêmes* deux codes structurels, - qu’une propriété de corps — c’est l’exigence. Une entrée centrée-DTO n’a nulle - part où placer une entrée absente du DTO ; un modèle de pairs, si. -* L’entrée est non typée parce que le modèle agnostique de la source retire la - raison de la typer. La commande n’est plus « construite sur un DTO » ; elle est - assemblée au terminal à partir de pairs. Nommer le type de commande au terminal - résout la tension à trois du Contexte : le terminal l’infère depuis l’assembleur, - il n’est donc nommé nulle part ailleurs, un seul type de binder sert la liaison de - premier niveau et la liaison imbriquée, et — parce que le type imbriqué n’apparaît - désormais qu’en position de *retour* de la liaison imbriquée — l’inférence de - groupe de méthodes le récupère sans argument de type explicite. L’intention reste - exprimée, par le nom de la fabrique d’enveloppe et par le type propre de la - commande, pas par un paramètre de type redondant sur l’entrée. -* Un argument hors-DTO indique son propre nom parce qu’il n’y a pas de propriété - d’où le dériver ; le nom est utilisé tel quel comme chemin, l’appelant contrôle - donc directement la clé du fil, là précisément où une propriété de DTO s’en remet - au fournisseur de noms. -* Capturer la provenance d’un argument, et *seulement* celle d’un argument, colle à - l’endroit où l’information existe et vaut la peine d’être conservée : l’origine - d’un argument a été indiquée par l’appelant et distingue des échecs par ailleurs - semblables, tandis que l’origine d’une propriété de DTO est l’unique corps - implicite et serait du bruit sur chaque propriété. L’asymétrie est délibérée, pas - un oubli. -* Réutiliser la surface de convertisseurs existante (`AsRequired`, `AsOptional`, les - optionnels valeur et référence, et la forme liste) pour les arguments garde un - seul modèle mental pour chaque entrée : un argument ne diffère d’une propriété que - par la façon dont il est nommé et sourcé, jamais par la façon dont il est lié. -* Omettre un argument hors-DTO complexe est une conséquence de ce que sont les deux - concepts, pas une coupe de fonctionnalité : une propriété complexe déréférence un - chemin de DTO, et un argument n’a pas de DTO à déréférencer. Une valeur complexe - assemblée à partir de plusieurs arguments s’exprime en liant chacun comme un pair - et en les combinant dans le terminal — aucun nouveau concept requis. -* Le statut de pré-release signifie que la forme du point d’entrée est arrêtée - maintenant, quand il n’y a aucun consommateur à migrer. - -## Alternatives considérées - -### Garder l’entrée centrée-DTO et traiter une valeur hors-DTO comme un DTO synthétique à une propriété - -Considérée parce qu’elle réutilise le chemin de propriété existant sans changement. - -Rejetée parce qu’elle force l’appelant à emballer chaque valeur libre dans un DTO -jetable dans le seul but de satisfaire la forme d’entrée, et le chemin dérivé par -réflexion rapporte alors le nom de propriété de cet emballage plutôt que la clé du -fil voulue par l’appelant — l’inverse de ce dont un argument hors-DTO a besoin. - -### Typer le point d’entrée sur la commande (un `Bind.To` générique) - -Considérée parce qu’elle énonce le type cible en tête, se lit comme intention-d’abord, -et garde un seul type de binder. - -Rejetée parce que, le type de commande étant fixé sur l’entrée, une liaison complexe -imbriquée place le type imbriqué en position de paramètre de délégué où l’inférence -de groupe de méthodes ne peut pas le récupérer, donc chaque site d’appel imbriqué -doit épeler un argument de type explicite — une écharde persistante sur la forme de -binder la plus courante. L’entrée non typée retire l’écharde tout en exprimant -l’intention via l’enveloppe et le type de commande. - -### Ajouter un argument hors-DTO complexe reflétant la propriété complexe - -Considérée par symétrie avec le côté DTO, pour que les arguments et les propriétés -offrent les mêmes formes. - -Rejetée parce qu’elle n’a pas de référent : une propriété complexe lie un DTO -imbriqué atteint par un chemin, et un argument hors-DTO n’a ni DTO ni chemin. L’API -d’apparence symétrique nommerait une chose qui n’existe pas ; la composition -existante pairs-plus-terminal couvre déjà « une valeur complexe bâtie à partir -d’arguments ». - -### Encoder la provenance dans le chemin d’erreur de l’argument (par exemple `route:bookingId`) - -Considérée parce qu’elle ne nécessite aucune deuxième clé de contexte. - -Rejetée parce qu’elle amalgame deux faits distincts — *où* est l’entrée (le chemin, -servant à localiser le champ) et *quel type d’origine* elle a (la source, servant à -classer l’échec) — en une seule chaîne que les consommateurs devraient alors parser, -le parsing de message même que le binder existe pour éviter. Une clé de contexte -séparée et typée garde les deux faits de première classe. +Traiter les sources comme des pairs permet d'accumuler tous les échecs dans une seule enveloppe, quelle que soit l'origine depuis laquelle l'hôte a extrait la valeur. + +Le point d'entrée non typé supprime un type de commande redondant dans les étapes intermédiaires et préserve l'inférence des bindings imbriqués, tandis que le terminal exprime toujours le type construit au moyen de l'assembleur. + +Un argument doit posséder son chemin explicite puisqu'aucune propriété réfléchie n'existe. Sa provenance reste un fait diagnostique typé distinct au lieu d'être encodée dans la chaîne du chemin. + +Réutiliser le même vocabulaire de convertisseurs pour les propriétés et les arguments préserve un modèle mental unique : les sources diffèrent par leur nommage et leur provenance, pas par leur validation ou conversion. + +Les valeurs complexes assemblées à partir de plusieurs arguments libres sont exprimées en liant chaque argument comme pair puis en les composant au terminal. La décision n'interdit pas un futur argument complexe first-class si une sémantique agnostique de l'hôte et concrète apparaît ; elle refuse seulement d'en inventer une sans référent réel. + +Les types d'entrée exacts, les API de sources, les helpers de provenance, les signatures génériques et les exemples sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) et le guide du Request Binder. + +## Alternatives envisagées + +### Conserver le point d'entrée centré DTO et envelopper les arguments dans des DTO synthétiques + +Envisagé pour réutiliser le chemin existant. Rejeté parce que cela ajoute de la cérémonie et rapporte les noms de propriétés du wrapper plutôt que les véritables clés du protocole. + +### Typer le point d'entrée sur la commande + +Envisagé pour rendre la cible explicite plus tôt. Rejeté parce que cela dégrade l'inférence des groupes de méthodes dans les bindings imbriqués et impose des arguments de type explicites répétés. + +### Ajouter immédiatement un argument complexe hors DTO + +Envisagé pour la symétrie avec les propriétés complexes de DTO. Rejeté parce qu'une propriété complexe représente un chemin dans un DTO, tandis qu'un argument libre n'a aucun graphe d'objets équivalent à parcourir. La composition de pairs couvre déjà la construction depuis plusieurs valeurs libres. + +### Encoder la provenance dans le chemin d'erreur + +Envisagé pour éviter une seconde valeur de contexte. Rejeté parce que le chemin et l'origine répondent à deux questions distinctes et doivent rester typés séparément. ## Conséquences ### Positives -* Une commande assemblée à partir d’un corps et de tout mélange de valeurs de - route/query/en-tête se lie en une passe, dans une enveloppe, avec un seul jeu de - chemins et de codes. -* Le site d’appel de la liaison imbriquée n’a besoin d’aucun argument de type - explicite ; un seul type de binder sert la liaison de premier niveau et imbriquée. -* L’échec d’un argument porte sa provenance, un consommateur peut donc classer les - échecs (une mauvaise route vs un mauvais en-tête) sans parser les messages. -* La surface de convertisseurs est inchangée, un argument est donc lié avec - exactement les verbes d’une propriété. +* Le corps, la route, la query, les headers et les valeurs similaires se lient ensemble dans un même résultat. +* Le binding imbriqué conserve l'inférence des groupes de méthodes sans arguments de type explicites. +* Les échecs d'arguments portent une provenance typée sans analyse de messages. +* Les propriétés et arguments réutilisent le même modèle de conversion. ### Négatives -* La forme du point d’entrée change de la forme centrée-DTO que l’ADR-0007, - l’ADR-0012 et l’ADR-0017 décrivent en prose ; les décisions de ces ADR sont - inchangées, mais leur surface illustrative est désormais historique. -* Deux façons d’attacher une entrée (source de propriétés, source d’arguments) sont - une surface un peu plus large qu’une seule — acceptée parce qu’elles modélisent - deux provenances réellement différentes, pas deux saveurs de la même chose. +* La forme du point d'entrée diffère des exemples centrés DTO des ADR et documents antérieurs. +* La surface publique distingue désormais les sources de propriétés des sources d'arguments. +* Les appelants doivent fournir des conventions stables de noms et de provenance pour les arguments libres. ### Risques -* Un appelant pourrait chercher un « argument complexe » inexistant et être - brièvement surpris par son absence ; atténué en documentant la composition - pairs-plus-terminal comme la voie prévue pour bâtir une valeur complexe à partir - d’arguments. -* Des étiquettes de provenance libres pourraient dériver dans une base de code - (« route » vs « path ») ; atténué par les raccourcis de provenance (`FromRoute`, - `FromQuery`, …) qui fixent les étiquettes courantes, laissant le `From(source, …)` - brut pour le reste. +* Les labels de provenance pourraient dériver au sein d'une application. Mesure : fournir et documenter des raccourcis standards pour les origines courantes. +* Les utilisateurs pourraient attendre une API d'argument complexe par simple symétrie visuelle. Mesure : documenter la composition de pairs et ne réexaminer la question qu'à partir d'un cas concret. ## Actions de suivi -* Mettre à jour le guide du request-binder (EN + FR) et le README du paquet vers - l’entrée agnostique de la source et la section des arguments hors-DTO. -* Envisager un paquet d’intégration hôte qui extrait les valeurs d’une requête HTTP - entrante (plutôt que d’étiqueter celles déjà extraites), si une demande de - consommateur apparaît. +* Maintenir le guide bilingue du Request Binder et le README du package alignés sur le point d'entrée agnostique de la source. +* Évaluer séparément des packages d'intégration hôte si les consommateurs ont besoin d'une extraction spécifique à un framework plutôt que du binding de valeurs déjà extraites. ## Références -* ADR-0007 — nommer les terminaux du binder New et Create ; le terminal qui porte - désormais aussi le paramètre de type de commande. Décision inchangée. -* ADR-0012 — fixer les options du binder avant le début de la liaison ; les options - sont toujours fixées au point d’entrée (désormais agnostique de la source). - Décision inchangée. -* ADR-0017 — fournir un défaut applicatif configurable pour les options du binder ; - le défaut soutient toujours le point d’entrée nu. Décision inchangée. -* Issue #148 — la demande que cette décision résout. -* [`fluent-request-binder`](https://github.com/Reefact/fluent-request-binder) — le - binder antérieur dont le modèle source/argument a informé cette décision. +* [Référence d'implémentation des ADR — Contrats d'implémentation du Request Binder](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) +* [ADR-0007](0007-name-the-binder-terminals-new-and-create.fr.md) — le nom des terminaux reste valide ; la forme illustrative du point d'entrée est mise à jour par cet ADR. +* [ADR-0012](0012-fix-the-binder-options-before-binding-begins.fr.md) — les options restent fixées au point d'entrée agnostique de la source ; la forme illustrative de l'API est mise à jour par cet ADR. +* [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md) — la valeur par défaut reste valide ; la forme illustrative de l'API est mise à jour par cet ADR. +* Issue #148. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 62b2dd582a5e9242ef33c92999689d2acf996117 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:54:00 +0200 Subject: [PATCH 26/33] docs: separate the .NET Framework floor decision from test mechanics --- ...loor-the-library-on-net-framework-4-7-2.md | 150 +++++------------- 1 file changed, 36 insertions(+), 114 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md index c3b5c52c..4922caf3 100644 --- a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md +++ b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md @@ -8,150 +8,72 @@ ## Context -The shipped libraries — `FirstClassErrors`, `FirstClassErrors.Testing` and -`FirstClassErrors.RequestBinder` — target **`netstandard2.0`**. A `netstandard2.0` -assembly is *consumable* by any runtime that implements the standard, and the -standard names **.NET Framework 4.6.1** as its minimum on that platform. - -That 4.6.1 minimum is retrofitted. `netstandard2.0` shipped after .NET Framework -4.6.1, and support for it was added back: on 4.6.1 through 4.7.1 the `netstandard.dll` -facade and a set of `System.*` shims are delivered as NuGet assets and require -consumer-side binding redirects to load. **.NET Framework 4.7.2 is the first version -that ships those facades in-box**, and Microsoft's own `netstandard2.0` support -guidance recommends 4.7.2 or later. - -Servicing reinforces the same line. .NET Framework 4.6, 4.6.1 (and 4.5.2) reached -end of support in April 2022; 4.6.2 is the oldest still serviced; and **4.8.1 is the -last .NET Framework** — there will be no 4.9. - -Until now the product advertised, in `FirstClassErrors/README.nuget.md` and as an -incidental line in the Rationale of [ADR-0002](0002-floor-the-tooling-runtime.md), -that the library "runs on .NET Framework 4.6.1+". Nothing in CI ever loaded the -assemblies on a .NET Framework runtime: `build-test` runs the suite on .NET 10 and -the `floor` job runs only the *tooling* on the .NET 8 runtime. The compatibility -claim was therefore never verified. - -The test stack is **xUnit v3**, whose lowest supported .NET Framework target is -**`net472`**; there is no supported way to run these test projects on an earlier -.NET Framework. CI already guards the two other runtime bounds of the product: the -tooling's .NET 8 floor ([ADR-0002](0002-floor-the-tooling-runtime.md)) and, ahead of -release, the next .NET preview (`canary.yml`). +The shipped libraries target `netstandard2.0`, whose formal .NET Framework minimum is 4.6.1. + +On .NET Framework versions before 4.7.2, `netstandard2.0` support relies on retrofitted facades, additional package assets, and consumer-side binding redirects. .NET Framework 4.7.2 is the first version that provides the relevant facades in-box and is the practical minimum recommended for reliable consumption. + +The repository previously advertised .NET Framework 4.6.1 support without executing the libraries on that runtime. A compatibility promise that is not exercised cannot provide a trustworthy support boundary. + +The current test stack can execute on .NET Framework 4.7.2 but not on earlier framework versions. The tooling runtime has a separate floor defined by ADR-0002. ## Decision -The supported .NET Framework floor for the `netstandard2.0` libraries is **4.7.2**. +The supported .NET Framework floor for the shipped `netstandard2.0` libraries is **4.7.2**. ## Rationale -4.7.2 is the lowest .NET Framework version on which the library runs *without -consumer-side plumbing*: it is the first to ship the `netstandard2.0` facades in-box, -so the "runs almost everywhere" promise becomes true rather than conditional on -binding redirects. It is therefore the honest floor, where 4.6.1 was the theoretical -one. - -A support claim is only worth what verifies it. The 4.6.1 line was never exercised, -which is a liability for a library whose entire purpose is production diagnosability. -Flooring at 4.7.2 makes the claim *checkable on every pull request*, because 4.7.2 is -also the lowest framework the test stack itself can run on — the same number closes -both the support question and the verification question. - -4.6.x is the wrong place to anchor the guard. 4.6 and 4.6.1 are end-of-life, and the -4.6.1–4.7.1 facade-and-binding-redirect fragility would make a red signal ambiguous — -the library's fault, or the platform's? A floor exists to give an unambiguous signal, -and only 4.7.2 provides one with the current stack. - -Flooring the library at 4.7.2 mirrors the tooling floor at .NET 8: each supported -runtime bound is proven by its own dedicated job, so the product states its supported -runtimes precisely instead of by assertion. This decision **refines** the incidental -"4.6.1" claim in [ADR-0002](0002-floor-the-tooling-runtime.md) without superseding it: -that ADR's decision is the *tooling's* net8 floor, not the library's .NET Framework -floor, which had no ADR of its own until now. +4.7.2 is the lowest version on which the libraries can be consumed without the fragile compatibility plumbing required by earlier framework versions. -## Alternatives Considered +It is also the lowest version the repository can exercise with its supported test stack. Aligning the documented floor with a continuously verified runtime turns an aspirational compatibility statement into an enforceable contract. + +The decision intentionally chooses the practical and testable boundary rather than the theoretical `netstandard2.0` minimum. Lower versions would require a second test stack and environment-specific binding behavior for little continuing user value. -### Keep advertising .NET Framework 4.6.1+ (status quo) +This ADR refines the incidental .NET Framework 4.6.1 statement that previously appeared in ADR-0002; it does not supersede ADR-0002 because that decision concerns runnable tooling rather than the libraries. -Considered because it is the `netstandard2.0` minimum on paper and demands no change. +The exact Windows job, conditioned test targets, polyfills, project exclusions, and preview coverage are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#tooling-runtime-floor) and the CI workflow reference. -Rejected because it was never verified and cannot be verified cheaply: 4.6.1's -`netstandard2.0` support is retrofitted and fragile, the versions concerned are -largely end-of-life, and xUnit v3 cannot target below `net472`, so proving the claim -would require a second test stack and consumer binding redirects for a runtime almost -nobody should still deploy. +## Alternatives Considered -### Floor at 4.6.2 (the oldest serviced 4.6.x) +### Keep advertising .NET Framework 4.6.1 -Considered because 4.6.2, unlike 4.6/4.6.1, is still serviced, so it would keep the -lowest still-supported number. +Considered because it is the formal `netstandard2.0` minimum. Rejected because the claim was unverified and depends on fragile consumer-side plumbing on largely obsolete runtime versions. -Rejected because 4.6.2 predates the in-box facades: it carries the same -binding-redirect fragility as 4.6.1, and it is still below the xUnit v3 floor, so it -remains unverifiable with the current stack — the guard's signal would stay -ambiguous. +### Floor at .NET Framework 4.6.2 -### Floor the libraries across the whole modern .NET matrix (net6/net8/… as blocking legs) +Considered because it remains serviced longer than 4.6.1. Rejected because it has the same facade and binding-redirect constraints and cannot be verified with the supported test stack. -Considered as the conventional "test on everything" answer for broad reassurance. +### Test every modern .NET major as a blocking matrix -Rejected because the library is a single `netstandard2.0` assembly and the modern -runtimes are one CoreCLR family: the behavioural delta across majors, for -zero-dependency value objects, is negligible; end-of-life majors should not be -floored at all; and a per-major matrix re-introduces exactly the per-release treadmill -that [ADR-0002](0002-floor-the-tooling-runtime.md) rejected. The single valuable -boundary is .NET Framework versus modern .NET, which `net472` covers, while the latest -runtime (`build-test`) and the next preview (`canary.yml`) already cover the modern -end. +Considered for broad reassurance. Rejected because the valuable compatibility boundary is .NET Framework versus modern .NET, while the latest runtime and preview can cover the modern end without creating a per-release treadmill. ## Consequences ### Positive -* The advertised .NET Framework support is now **verified on every pull request** by a - dedicated Windows job, not merely claimed. -* The floor is **frozen**: 4.8.1 is the last .NET Framework, so this guard never - chases a moving target and needs no per-release upkeep. -* The product's supported-runtime story is symmetric and precise: a library - .NET Framework floor (4.7.2) and a tooling floor (.NET 8), each proven by its own - job, with the next preview watched ahead of release. +* The .NET Framework support statement is continuously verified rather than merely asserted. +* The practical floor avoids consumer-side binding-redirect fragility. +* The library and tooling runtime boundaries are stated separately and precisely. +* The .NET Framework floor is stable because the platform is no longer adding new major versions. ### Negative -* Consumers pinned to .NET Framework 4.6.1–4.7.1 lose a claim of support they never - actually had verified; they must be on 4.7.2 or later. Accepted: 4.7.2 is the - practical `netstandard2.0` floor and the lower versions are largely end-of-life. -* A small, test-only `IsExternalInit` polyfill and a `net472`-conditioned build path - are added to the affected test projects. The **shipped libraries are untouched** — - they use neither `init` nor records — so nothing in the product depends on the - polyfill. +* Consumers on .NET Framework 4.6.1 through 4.7.1 are outside the supported range. +* Dedicated Windows compatibility coverage and test-target plumbing must remain maintained. ### Risks -* The `net472` leg runs on Windows only, so a .NET-Framework-specific regression is - invisible on the Linux legs until the Windows job runs. Mitigated by running the job - on every pull request. -* `FirstClassErrors.RequestBinder.UnitTests` cannot join the floor because its - fixtures bind `DateOnly`, a .NET 6+ type absent from .NET Framework; RequestBinder is - floored through its property tests instead. Accepted: the excluded scenarios exercise - a type that cannot exist on `net472` in the first place. +* Some Request Binder scenarios use modern-only types and cannot run on the framework floor. Mitigation: cover the shipped binder assembly through compatible test suites and keep exclusions explicit in the implementation reference. +* A required floor job could be configured but not enforced by branch protection. Mitigation: maintain the job as a required status check when repository settings permit. ## Follow-up Actions -* State 4.7.2+ in `FirstClassErrors/README.nuget.md` (done in this change). -* Add the `framework-floor` job to `ci.yml` and the shared `build/Net472TestFloor.props` - that carries the gated `net472` leg (done in this change). -* Make `framework-floor` a **required status check** in branch protection so it blocks - merges, matching the intent that the floor is enforced, not advisory. -* Should a maintainer wish to reconcile the incidental "4.6.1" line in - [ADR-0002](0002-floor-the-tooling-runtime.md), add an erratum note there pointing to - this ADR; this ADR is the authority on the library's .NET Framework floor. +* Keep the user-facing support statement at .NET Framework 4.7.2 or later. +* Keep the framework-floor check required for merges. ## References -* [ADR-0002](0002-floor-the-tooling-runtime.md) — the tooling runtime floor; the - sibling runtime-bound decision this ADR refines. -* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) — the analyzer's Roslyn floor, - the third supported-tooling bound. -* `FirstClassErrors/README.nuget.md` — the user-facing support statement. -* `build/Net472TestFloor.props`, the `framework-floor` job in - `.github/workflows/ci.yml`, and the library preview run in - `.github/workflows/canary.yml` — where this decision is enforced. +* [ADR implementation reference — Tooling runtime floor](../specifications/adr-implementation-reference.md#tooling-runtime-floor) +* [ADR-0002](0002-floor-the-tooling-runtime.md) — refined by this ADR for the library's .NET Framework floor. +* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) +* `FirstClassErrors/README.nuget.md` and the CI workflow reference. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From 10b5f23577c9c3b28b821d36d339db85a1ff841a Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:54:29 +0200 Subject: [PATCH 27/33] docs: translate ADR-0022's editorial rewrite to French --- ...r-the-library-on-net-framework-4-7-2.fr.md | 163 ++++-------------- 1 file changed, 37 insertions(+), 126 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md index 6f9f0333..726b31b3 100644 --- a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md +++ b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md @@ -1,4 +1,4 @@ -# ADR-0022 | Fixer le floor .NET Framework de la librairie à 4.7.2 +# ADR-0022 | Fixer le plancher .NET Framework de la bibliothèque à 4.7.2 🌍 🇬🇧 [English](0022-floor-the-library-on-net-framework-4-7-2.md) · 🇫🇷 Français (ce fichier) @@ -8,161 +8,72 @@ ## Contexte -Les librairies livrées — `FirstClassErrors`, `FirstClassErrors.Testing` et -`FirstClassErrors.RequestBinder` — ciblent **`netstandard2.0`**. Un assembly -`netstandard2.0` est *consommable* par n'importe quel runtime qui implémente le -standard, et le standard désigne **.NET Framework 4.6.1** comme son minimum sur cette -plateforme. - -Ce minimum 4.6.1 est rétro-ajouté. `netstandard2.0` est sorti après .NET Framework -4.6.1, et sa prise en charge a été greffée après coup : sur 4.6.1 à 4.7.1, la façade -`netstandard.dll` et un ensemble de shims `System.*` sont livrés comme des ressources -NuGet et exigent des *binding redirects* côté consommateur pour se charger. **.NET -Framework 4.7.2 est la première version qui embarque ces façades in-box**, et la -recommandation officielle de Microsoft pour `netstandard2.0` est d'utiliser 4.7.2 ou -ultérieur. - -Le support renforce la même ligne. .NET Framework 4.6, 4.6.1 (et 4.5.2) sont en fin -de support depuis avril 2022 ; 4.6.2 est la plus ancienne encore maintenue ; et -**4.8.1 est la dernière version de .NET Framework** — il n'y aura pas de 4.9. - -Jusqu'ici le produit annonçait, dans `FirstClassErrors/README.nuget.md` et comme une -phrase incidente de la Justification de l'[ADR-0002](0002-floor-the-tooling-runtime.fr.md), -que la librairie « tourne sur .NET Framework 4.6.1+ ». Rien dans la CI n'a jamais -chargé les assemblies sur un runtime .NET Framework : `build-test` exécute la suite sur -.NET 10 et le job `floor` n'exécute que *l'outillage* sur le runtime .NET 8. L'annonce -de compatibilité n'a donc jamais été vérifiée. - -La stack de test est **xUnit v3**, dont la cible .NET Framework la plus basse est -**`net472`** ; il n'existe aucun moyen supporté de faire tourner ces projets de tests -sur un .NET Framework antérieur. La CI garde déjà les deux autres bornes runtime du -produit : le floor .NET 8 de l'outillage ([ADR-0002](0002-floor-the-tooling-runtime.fr.md)) -et, en amont de la publication, la prochaine preview de .NET (`canary.yml`). +Les bibliothèques livrées ciblent `netstandard2.0`, dont le minimum formel sur .NET Framework est 4.6.1. + +Sur les versions antérieures à .NET Framework 4.7.2, la prise en charge de `netstandard2.0` dépend de façades ajoutées a posteriori, d'assets de packages supplémentaires et de redirects de binding côté consommateur. .NET Framework 4.7.2 est la première version qui fournit les façades nécessaires nativement et constitue le minimum pratique recommandé pour une consommation fiable. + +Le dépôt annonçait auparavant une prise en charge de .NET Framework 4.6.1 sans exécuter les bibliothèques sur ce runtime. Une promesse de compatibilité qui n'est pas exercée ne peut pas constituer une frontière de support fiable. + +La pile de tests actuelle peut s'exécuter sur .NET Framework 4.7.2 mais pas sur les versions antérieures. L'outillage possède un plancher distinct défini par l'ADR-0002. ## Décision -Le floor .NET Framework supporté pour les librairies `netstandard2.0` est **4.7.2**. +Le plancher .NET Framework pris en charge pour les bibliothèques `netstandard2.0` livrées est **4.7.2**. ## Justification -4.7.2 est la plus basse version de .NET Framework sur laquelle la librairie tourne -*sans plomberie côté consommateur* : c'est la première à embarquer les façades -`netstandard2.0` in-box, si bien que la promesse « tourne presque partout » devient -vraie au lieu d'être conditionnée à des *binding redirects*. C'est donc le floor -honnête, là où 4.6.1 était le floor théorique. - -Une annonce de support ne vaut que ce qui la vérifie. La ligne 4.6.1 n'a jamais été -exercée, ce qui est un passif pour une librairie dont la raison d'être est la -diagnosticabilité en production. Fixer le floor à 4.7.2 rend l'annonce *vérifiable à -chaque pull request*, car 4.7.2 est aussi le plus bas framework sur lequel la stack de -test elle-même peut tourner — le même nombre clôt à la fois la question du support et -celle de la vérification. - -4.6.x est le mauvais endroit pour ancrer le garde-fou. 4.6 et 4.6.1 sont en fin de vie, -et la fragilité façade-et-binding-redirects de 4.6.1–4.7.1 rendrait un signal rouge -ambigu — la faute de la librairie, ou celle de la plateforme ? Un floor existe pour -donner un signal sans ambiguïté, et seule 4.7.2 en fournit un avec la stack actuelle. - -Fixer le floor de la librairie à 4.7.2 est le symétrique du floor .NET 8 de -l'outillage : chaque borne runtime supportée est prouvée par son propre job, de sorte -que le produit énonce ses runtimes supportés précisément plutôt que par affirmation. -Cette décision **précise** la phrase incidente « 4.6.1 » de -l'[ADR-0002](0002-floor-the-tooling-runtime.fr.md) sans la remplacer : la décision de -cet ADR est le floor net8 de *l'outillage*, non le floor .NET Framework de la -librairie, qui n'avait pas d'ADR propre jusqu'ici. +4.7.2 est la version la plus basse sur laquelle les bibliothèques peuvent être consommées sans la plomberie de compatibilité fragile exigée par les versions antérieures. -## Alternatives envisagées +C'est également la plus basse version que le dépôt peut exercer avec sa pile de tests prise en charge. Aligner le plancher documenté sur un runtime vérifié en continu transforme une déclaration de compatibilité théorique en contrat imposable. + +La décision choisit volontairement la frontière pratique et testable plutôt que le minimum théorique de `netstandard2.0`. Les versions inférieures exigeraient une seconde pile de tests et des comportements de binding spécifiques à l'environnement pour une valeur utilisateur désormais limitée. -### Continuer d'annoncer .NET Framework 4.6.1+ (statu quo) +Cet ADR raffine la mention incidente de .NET Framework 4.6.1 auparavant présente dans l'ADR-0002 ; il ne remplace pas l'ADR-0002, car cette décision concerne l'outillage exécutable et non les bibliothèques. -Envisagé parce que c'est le minimum `netstandard2.0` sur le papier et que cela -n'exige aucun changement. +Le job Windows exact, les cibles de tests conditionnées, les polyfills, les exclusions de projets et la couverture des previews sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#plancher-dexécution-des-outils) et la référence du workflow CI. -Rejeté parce que cela n'a jamais été vérifié et ne peut l'être à moindre coût : la -prise en charge de `netstandard2.0` par 4.6.1 est rétro-ajoutée et fragile, les -versions concernées sont largement en fin de vie, et xUnit v3 ne peut pas cibler en -dessous de `net472` — prouver l'annonce demanderait une seconde stack de test et des -*binding redirects* côté consommateur, pour un runtime que presque plus personne ne -devrait déployer. +## Alternatives envisagées -### Fixer le floor à 4.6.2 (la plus ancienne 4.6.x encore maintenue) +### Continuer à annoncer .NET Framework 4.6.1 -Envisagé parce que 4.6.2, contrairement à 4.6/4.6.1, est encore maintenue, ce qui -garderait le plus bas numéro encore supporté. +Envisagé parce qu'il s'agit du minimum formel de `netstandard2.0`. Rejeté parce que cette déclaration n'était pas vérifiée et dépend d'une plomberie fragile côté consommateur sur des runtimes largement obsolètes. -Rejeté parce que 4.6.2 précède les façades in-box : elle porte la même fragilité de -*binding redirects* que 4.6.1, et reste sous le floor de xUnit v3 — elle demeure donc -invérifiable avec la stack actuelle, et le signal du garde-fou resterait ambigu. +### Fixer le plancher à .NET Framework 4.6.2 -### Fixer le floor des librairies sur toute la matrice .NET moderne (net6/net8/… en jambes bloquantes) +Envisagé car cette version est restée maintenue plus longtemps que 4.6.1. Rejeté parce qu'elle présente les mêmes contraintes de façades et de redirects de binding et ne peut pas être vérifiée avec la pile de tests prise en charge. -Envisagé comme la réponse conventionnelle « tester sur tout » pour une réassurance -large. +### Tester chaque version majeure moderne de .NET dans une matrice bloquante -Rejeté parce que la librairie est un unique assembly `netstandard2.0` et que les -runtimes modernes forment une seule famille CoreCLR : le delta comportemental entre -majors, pour des value objects sans dépendance, est négligeable ; les majors en fin de -vie ne devraient pas être « floorés » du tout ; et une matrice par-major réintroduit -exactement le tapis roulant par-release que l'[ADR-0002](0002-floor-the-tooling-runtime.fr.md) -a rejeté. La seule frontière qui compte est .NET Framework versus .NET moderne, que -`net472` couvre, tandis que le dernier runtime (`build-test`) et la prochaine preview -(`canary.yml`) couvrent déjà l'extrémité moderne. +Envisagé pour une assurance large. Rejeté parce que la frontière de compatibilité utile est celle entre .NET Framework et .NET moderne, tandis que le dernier runtime et la preview couvrent l'autre extrémité sans recréer une maintenance à chaque release. ## Conséquences ### Positives -* Le support .NET Framework annoncé est désormais **vérifié à chaque pull request** par - un job Windows dédié, et non plus seulement affirmé. -* Le floor est **gelé** : 4.8.1 est la dernière version de .NET Framework, donc ce - garde-fou ne poursuit jamais une cible mouvante et n'exige aucun entretien - par-release. -* L'histoire des runtimes supportés du produit est symétrique et précise : un floor - .NET Framework de la librairie (4.7.2) et un floor de l'outillage (.NET 8), chacun - prouvé par son propre job, avec la prochaine preview surveillée en amont. +* La prise en charge de .NET Framework est vérifiée en continu plutôt que simplement affirmée. +* Le plancher pratique évite la fragilité des redirects de binding côté consommateur. +* Les frontières de runtime de la bibliothèque et de l'outillage sont énoncées séparément et précisément. +* Le plancher .NET Framework est stable puisque la plateforme n'ajoute plus de nouvelles versions majeures. ### Négatives -* Les consommateurs figés sur .NET Framework 4.6.1–4.7.1 perdent une annonce de - support qui n'avait jamais été vérifiée ; ils doivent être sur 4.7.2 ou ultérieur. - Accepté : 4.7.2 est le floor `netstandard2.0` pratique et les versions inférieures - sont largement en fin de vie. -* Un petit polyfill `IsExternalInit` réservé aux tests et un chemin de build - conditionné à `net472` sont ajoutés aux projets de tests concernés. Les **librairies - livrées ne sont pas touchées** — elles n'utilisent ni `init` ni records — donc rien - dans le produit ne dépend du polyfill. +* Les consommateurs sur .NET Framework 4.6.1 à 4.7.1 sortent de la plage prise en charge. +* Une couverture de compatibilité Windows et une plomberie de cibles de tests dédiées doivent être maintenues. ### Risques -* La jambe `net472` ne tourne que sous Windows ; une régression spécifique à - .NET Framework est invisible sur les jambes Linux jusqu'à l'exécution du job Windows. - Atténué en exécutant le job à chaque pull request. -* `FirstClassErrors.RequestBinder.UnitTests` ne peut pas rejoindre le floor car ses - fixtures lient `DateOnly`, un type .NET 6+ absent de .NET Framework ; RequestBinder - est « flooré » via ses property tests à la place. Accepté : les scénarios exclus - exercent un type qui ne peut de toute façon pas exister sur `net472`. +* Certains scénarios du Request Binder utilisent des types réservés au .NET moderne et ne peuvent pas s'exécuter sur le plancher framework. Mesure : couvrir l'assembly livré du binder par des suites compatibles et conserver les exclusions explicites dans la référence d'implémentation. +* Un job de plancher peut exister sans être imposé par la protection de branche. Mesure : maintenir ce job comme statut obligatoire lorsque les réglages du dépôt le permettent. ## Actions de suivi -* Indiquer 4.7.2+ dans `FirstClassErrors/README.nuget.md` (fait dans ce changement). -* Ajouter le job `framework-floor` à `ci.yml` et le fichier partagé - `build/Net472TestFloor.props` qui porte la jambe `net472` conditionnée (fait dans ce - changement). -* Faire de `framework-floor` un **status check requis** dans la protection de branche - pour qu'il bloque les merges, conformément à l'intention que le floor soit imposé, et - non pas indicatif. -* Si un mainteneur souhaite réconcilier la phrase incidente « 4.6.1 » de - l'[ADR-0002](0002-floor-the-tooling-runtime.fr.md), y ajouter une note d'errata - renvoyant à cet ADR ; cet ADR fait autorité sur le floor .NET Framework de la - librairie. +* Maintenir la déclaration utilisateur à .NET Framework 4.7.2 ou supérieur. +* Maintenir le contrôle du plancher framework comme condition obligatoire de fusion. ## Références -* [ADR-0002](0002-floor-the-tooling-runtime.fr.md) — le floor du runtime de - l'outillage ; la décision sœur, sur une borne runtime, que cet ADR précise. -* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) — le floor Roslyn de - l'analyseur, la troisième borne d'outillage supporté. -* `FirstClassErrors/README.nuget.md` — l'énoncé de support côté utilisateur. -* `build/Net472TestFloor.props`, le job `framework-floor` dans - `.github/workflows/ci.yml`, et l'exécution des tests de librairies sur la preview - dans `.github/workflows/canary.yml` — là où cette décision est imposée. +* [Référence d'implémentation des ADR — Plancher d'exécution des outils](../specifications/adr-implementation-reference.fr.md#plancher-dexécution-des-outils) +* [ADR-0002](0002-floor-the-tooling-runtime.fr.md) — raffiné par cet ADR pour le plancher .NET Framework de la bibliothèque. +* [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) +* `FirstClassErrors/README.nuget.md` et la référence du workflow CI. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 6ef0daf86d55e3eb2d6c02625ef5aca5c0c9c1a0 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:55:11 +0200 Subject: [PATCH 28/33] docs: sync accepted ADR statuses in the index --- doc/handwritten/for-maintainers/adr/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 22891edf..cb6e8c13 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -190,20 +190,20 @@ Optional supporting material: | [ADR-0005](0005-reserve-the-plain-factory-name-for-the-outcome-returning-variant.md) | Reserve the plain factory name for the Outcome-returning variant | Accepted | | [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) | Supply arbitrary test values from a single seedable source | Accepted | | [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) | Name the binder terminals New and Create | Accepted | -| [ADR-0008](0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md) | Bind nullable value-type properties through a struct-constrained overload | Proposed | +| [ADR-0008](0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md) | Bind nullable value-type properties through a struct-constrained overload | Accepted | | [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.md) | Report the tooling's failures as first-class errors | Accepted | | [ADR-0010](0010-treat-gendocs-error-catalog-as-a-versioned-contract.md) | Treat GenDoc's error catalog as a versioned contract | Accepted | -| [ADR-0011](0011-host-dummies-as-a-standalone-package.md) | Host Dummies as a standalone package in this repository | Proposed | +| [ADR-0011](0011-host-dummies-as-a-standalone-package.md) | Host Dummies as a standalone package in this repository | Accepted | | [ADR-0012](0012-fix-the-binder-options-before-binding-begins.md) | Fix the binder options before binding begins | Accepted | | [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) | Gate distinct collections by cardinality, otherwise by a bounded draw | Accepted | | [ADR-0014](0014-bind-a-required-list-by-presence-not-cardinality.md) | Bind a required list by presence, not cardinality | Accepted | -| [ADR-0015](0015-cap-any-combine-at-arity-eight.md) | Cap Any.Combine at arity eight | Proposed | +| [ADR-0015](0015-cap-any-combine-at-arity-eight.md) | Cap Any.Combine at arity eight | Accepted | | [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.md) | Make the binder's structural error codes configurable | Superseded | | [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) | Provide a configurable application-wide default for the binder options | Accepted | | [ADR-0018](0018-bundle-the-binders-structural-error-code-and-messages.md) | Bundle the binder's structural error code and messages in one definition | Accepted | | [ADR-0019](0019-document-overridden-binder-errors-in-the-consumers-catalog.md) | Document overridden binder errors in the consumer's own catalog | Accepted | | [ADR-0020](0020-materialize-dummies-only-through-generate.md) | Materialize dummies only through Generate() | Accepted | -| [ADR-0021](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md) | Bind out-of-DTO arguments as peers through a source-agnostic untyped entry | Proposed | +| [ADR-0021](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md) | Bind out-of-DTO arguments as peers through a source-agnostic untyped entry | Accepted | | [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) | Floor the library's .NET Framework support at 4.7.2 | Accepted | | [ADR-0023](0023-keep-expression-tree-selectors-for-the-v1-binder-api.md) | Keep expression-tree selectors for the v1 binder API | Accepted | | [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) | Allow a one-time editorial refactoring of accepted ADRs | Accepted | From e8110e2b44e5fb6163402492c211f9e2531f5629 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:55:51 +0200 Subject: [PATCH 29/33] docs: separate the binder-options decision from its API mechanics --- ...he-binder-options-before-binding-begins.md | 128 +++++------------- 1 file changed, 32 insertions(+), 96 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md index 6a5a5bdf..10e1da3f 100644 --- a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md +++ b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md @@ -8,132 +8,68 @@ ## Context -* The binder resolves each bound property's argument path — the key reported in error - paths, such as `GuestEmail` or `Stay.CheckIn` — through the `IArgumentNameProvider` - carried by `RequestBinderOptions`. -* Before this decision, options were set through an instance method `WithOptions` on - `RequestBinder`, callable at any point in the fluent chain, including after - some properties had already been bound. -* Each property binding reads the options in effect at the moment it is bound, so - changing the provider between two bindings produces argument paths under two different - naming policies within one failure envelope — for example `GuestEmail` beside - `guest_email`. -* The binder collects every failure into a single envelope; a client reads the argument - paths to map them back to the keys it sent, and relies on one consistent naming policy - across that envelope. -* The binder already draws a hard line between a client error (recorded, surfaced once) - and a programming error (thrown), and treats a misuse of its API as a programming - error surfaced loudly rather than a silent inconsistency. -* The instance `WithOptions` was documented "call before binding any property", but - nothing enforced it: the ordering was a prose convention, not a compile-time or - runtime guarantee. -* The library exposes no ambient mutable state elsewhere: the clock, instance-id and - arbitrary-value seams are all an immutable default plus an `AsyncLocal`, scoped, - test-only override (ADR-0006), and the core deliberately exposes zero global mutable - state. -* `RequestBinderOptions` carries no per-request state: the provider maps a - `PropertyInfo` to a name and depends on nothing about the request instance. -* The library is pre-release, unpublished on NuGet with no external consumers, so moving - where options are set carries no downstream migration cost. +The Request Binder resolves argument paths through an `IArgumentNameProvider` carried by `RequestBinderOptions`. + +The previous fluent surface allowed options to change after some properties had already been bound. Because each binding read the options active at that moment, one failure envelope could contain paths produced by different naming policies. + +A consumer relies on a single naming policy to map every error path back to the input it sent. Documentation alone could not prevent the invalid call order. + +`RequestBinderOptions` carries no per-request state and can therefore be configured once and reused across requests. ## Decision -The request binder's options are fixed once, at the entry point — -`Bind.WithOptions(options).PropertiesOf(request)` — before any property is bound, and -the ability to change them after binding has started is removed. +A binder's options are fixed at its entry point before any source is bound, and the public API does not permit them to change after binding begins. ## Rationale -* Fixing the options before the binder exists makes an inconsistent envelope impossible - to write rather than merely discouraged: once binding has begun there is no point in - the fluent chain at which the naming policy can be swapped, so the two-policy envelope - the Context describes cannot arise. This closes the defect at the API shape, in the - spirit of the binder's existing programming-error channel but one step stronger — the - mistake is uncompilable, not thrown. -* Placing the options before `PropertiesOf` rather than between `PropertiesOf` and - `FailWith` keeps them independent of the request type: a naming policy is about how a - property is named, not which request is bound, so it need not — and now does not — - depend on `TRequest`, which is also what lets the configured entry point be reused - across requests. -* Keeping options an explicit argument passed at the entry point, rather than an ambient - default the binder reads, is consistent with the library's settled stance that a - genuine production dependency is passed explicitly and the core exposes no global - mutable state (ADR-0006): a naming policy is a real production choice with legitimate - variation, so it is an explicit dependency, not ambient configuration. -* Because the options carry no per-request state, fixing them at a reusable entry point - lets an application configure the policy once — for example at startup — and reuse it - for every request without threading it through each binding: the ergonomic the removed - instance setter reached for, now without the ordering hazard. -* The pre-release status means the API shape is settled now, when there are no consumers - to migrate. +Fixing the options before the binder exists makes mixed-policy envelopes unrepresentable rather than merely detecting them later. -## Alternatives Considered +Keeping options outside the request-specific binder also reflects their actual scope: a naming and structural-error policy is application configuration, not request data, and a configured entry can be reused safely. -### Lock the options at runtime on the first binding +The decision concerns when options become fixed, not whether the application may provide a default. [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) later revisits the process-wide-default alternative with additional freezing and test-isolation safeguards while preserving this entry-point immutability. -Considered because it keeps the existing instance `WithOptions` and only adds a guard: a -late call, once a property is bound, throws — consistent with the binder's -programming-error channel. +The exact configured-entry type, fluent call shape, inheritance by nested binders, and examples are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) and the Request Binder guide. -Rejected because it detects the misuse instead of preventing it: the inconsistent-envelope -code still compiles and only fails at runtime, and the options still needlessly depend on -the request type. Moving the setter before `PropertiesOf` makes the same mistake -unrepresentable at no extra cost. +## Alternatives Considered -### A process-wide ambient default configured once (a static `Configure`) +### Lock options at runtime on the first binding -Considered because it would let `Bind.PropertiesOf(request)` pick up an application-wide -policy with nothing threaded through call sites at all. +Considered because it preserves more of the old surface. Rejected because invalid ordering would still compile and fail only at runtime. -Rejected because it would introduce the first piece of global mutable state in the -library, contradicting the settled no-ambient-mutable-state stance (ADR-0006): it would -leak across tests running in parallel and reintroduce a "configured at the wrong time" -hazard of its own. The application-wide configuration ergonomic belongs in the future -ASP.NET Core integration, through dependency injection, where it is test-safe by -construction. +### Provide only a process-wide ambient default -### Keep the instance setter and document the ordering more firmly +Considered because it removes configuration from call sites. Rejected at the time because an unrestricted ambient default could drift during execution and leak across parallel tests. ADR-0017 later adopts a constrained, freeze-on-first-use form without changing this ADR's decision. -Considered because it is the smallest change. +### Keep the mutable instance setter and strengthen documentation -Rejected because a prose "call before binding" is exactly the unenforced convention that -produced the defect; documentation cannot make the inconsistent envelope unrepresentable. +Considered because it requires the smallest code change. Rejected because documentation cannot make an inconsistent envelope impossible. ## Consequences ### Positive -* A single failure envelope always reports argument paths under one naming policy; the - two-policy inconsistency is impossible to write. -* Options no longer depend on the request type, and the configured entry point is - reusable across requests — one policy configured once, reused per request. -* The binder keeps the library's no-global-mutable-state property intact. +* Every failure envelope uses one naming and structural-error policy. +* Invalid late configuration becomes impossible through the public shape. +* Configured entries can be reused across requests. ### Negative -* The fluent chain gains a distinct entry point (`Bind.WithOptions(...).PropertiesOf(...)`) - beside the default `Bind.PropertiesOf(...)`, and a new public `ConfiguredBind` type to - document. -* A consumer who set options after `PropertiesOf` / `FailWith` must move the call before - `PropertiesOf` — a source change, mitigated by the pre-release status (no external - consumers). +* Configuration has a distinct entry path that consumers must learn. +* Existing code using a late options setter must move configuration before binding. ### Risks -* A future need to vary options per nested binder would not fit the "fixed at the entry - point" model; mitigated by nested binders inheriting the parent options by design, and - by this being outside the current requirements. +* A future need for intentionally different nested options would not fit this model. Mitigation: require an explicit new decision rather than introducing a late mutation path. ## Follow-up Actions -* Document the application-level configuration ergonomic (dependency injection) when the - ASP.NET Core integration is built, rather than adding ambient configuration to the core. +* Keep dependency-injection and application-default guidance in integration and user documentation. ## References -* ADR-0006 — supply arbitrary test values from a single seedable source; the - no-ambient-mutable-state stance this decision keeps. -* ADR-0007 — name the binder terminals New and Create; a sibling public-API decision on - the same binder. -* Issue #145 — the finding this decision resolves. -* Pull request #126 — the request binder feature these options belong to. +* [ADR implementation reference — Request Binder implementation contracts](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) +* [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) — revisits the process-wide default alternative while preserving fixed options per binder. +* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) +* [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) +* Issue #145 and pull request #126. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From a360a760d2955806343e425d93723f736ef9bfa8 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:56:14 +0200 Subject: [PATCH 30/33] docs: translate ADR-0012's editorial rewrite to French --- ...binder-options-before-binding-begins.fr.md | 152 +++++------------- 1 file changed, 42 insertions(+), 110 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md index 0f038211..81686db0 100644 --- a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md +++ b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md @@ -8,136 +8,68 @@ ## Contexte -* Le binder résout le chemin d'argument de chaque propriété liée — la clé rapportée dans - les chemins d'erreur, comme `GuestEmail` ou `Stay.CheckIn` — via l'`IArgumentNameProvider` - porté par `RequestBinderOptions`. -* Avant cette décision, les options étaient posées par une méthode d'instance `WithOptions` - sur `RequestBinder`, appelable à n'importe quel point de la chaîne fluide, y - compris après que certaines propriétés aient déjà été liées. -* Chaque liaison de propriété lit les options en vigueur au moment où elle est liée : changer - le provider entre deux liaisons produit donc des chemins d'argument sous deux politiques de - nommage différentes dans une même enveloppe d'échec — par exemple `GuestEmail` à côté de - `guest_email`. -* Le binder collecte chaque échec dans une seule enveloppe ; un client lit les chemins - d'argument pour les remapper vers les clés qu'il a envoyées, et compte sur une politique de - nommage cohérente sur toute l'enveloppe. -* Le binder trace déjà une frontière nette entre une erreur client (enregistrée, surfacée une - fois) et une erreur de programmation (levée), et traite un mauvais usage de son API comme - une erreur de programmation surfacée bruyamment plutôt qu'une incohérence silencieuse. -* La `WithOptions` d'instance était documentée « appelez-la avant de lier la moindre - propriété », mais rien ne l'imposait : l'ordre était une convention en prose, pas une - garantie à la compilation ni à l'exécution. -* La bibliothèque n'expose aucun état ambiant mutable ailleurs : les points d'extension de - l'horloge, de l'identifiant d'instance et des valeurs arbitraires sont tous un défaut - immuable plus un override `AsyncLocal`, scoped, réservé aux tests (ADR-0006), et le cœur - n'expose délibérément aucun état global mutable. -* `RequestBinderOptions` ne porte aucun état par requête : le provider mappe un `PropertyInfo` - vers un nom et ne dépend de rien de l'instance de requête. -* La bibliothèque est en pré-version, non publiée sur NuGet et sans consommateur externe : - déplacer l'endroit où les options sont posées n'entraîne donc aucun coût de migration en - aval. +Le Request Binder résout les chemins d'arguments au moyen d'un `IArgumentNameProvider` porté par `RequestBinderOptions`. + +La surface fluent précédente permettait de modifier les options après que certaines propriétés avaient déjà été liées. Chaque binding lisant les options actives à cet instant, une même enveloppe d'échec pouvait contenir des chemins produits par des politiques de nommage différentes. + +Un consommateur dépend d'une politique unique pour faire correspondre chaque chemin d'erreur à l'entrée qu'il a envoyée. La documentation seule ne pouvait pas empêcher l'ordre d'appel invalide. + +`RequestBinderOptions` ne porte aucun état propre à une requête et peut donc être configuré une fois puis réutilisé entre les requêtes. ## Décision -Les options du binder sont fixées une seule fois, au point d'entrée — -`Bind.WithOptions(options).PropertiesOf(request)` — avant que la moindre propriété ne soit -liée, et la possibilité de les changer une fois la liaison commencée est retirée. +Les options d'un binder sont fixées à son point d'entrée avant qu'une source ne soit liée, et l'API publique ne permet pas de les modifier après le début du binding. ## Justification -* Fixer les options avant que le binder n'existe rend une enveloppe incohérente impossible à - écrire plutôt que simplement déconseillée : une fois la liaison commencée, il n'existe aucun - point de la chaîne fluide où la politique de nommage puisse être échangée, donc l'enveloppe - à deux politiques décrite dans le Contexte ne peut pas survenir. Cela ferme le défaut au - niveau de la forme de l'API, dans l'esprit du canal d'erreur de programmation déjà présent - mais d'un cran plus fort — l'erreur est non-compilable, pas levée. -* Placer les options avant `PropertiesOf` plutôt qu'entre `PropertiesOf` et `FailWith` les - garde indépendantes du type de requête : une politique de nommage concerne comment une - propriété est nommée, pas quelle requête est liée, donc elle n'a pas à — et désormais ne — - dépend du `TRequest`, ce qui permet aussi de réutiliser le point d'entrée configuré d'une - requête à l'autre. -* Garder les options comme un argument explicite passé au point d'entrée, plutôt qu'un défaut - ambiant que le binder lit, est cohérent avec la position établie de la bibliothèque : une - vraie dépendance de production se passe explicitement et le cœur n'expose aucun état global - mutable (ADR-0006). Une politique de nommage est un vrai choix de production à variation - légitime : c'est donc une dépendance explicite, pas une configuration ambiante. -* Parce que les options ne portent aucun état par requête, les fixer à un point d'entrée - réutilisable laisse une application configurer la politique une fois — par exemple au - démarrage — et la réutiliser pour chaque requête sans la faire transiter par chaque liaison : - l'ergonomie que visait le setter d'instance retiré, désormais sans le piège de l'ordre. -* Le statut de pré-version signifie que la forme de l'API est arrêtée maintenant, quand il n'y - a aucun consommateur à migrer. - -## Alternatives considérées - -### Verrouiller les options à l'exécution dès la première liaison - -Considérée parce qu'elle garde la `WithOptions` d'instance et n'ajoute qu'une garde : un appel -tardif, une fois une propriété liée, lève — cohérent avec le canal d'erreur de programmation -du binder. - -Rejetée parce qu'elle détecte le mauvais usage au lieu de l'empêcher : le code de l'enveloppe -incohérente compile toujours et n'échoue qu'à l'exécution, et les options dépendent encore -inutilement du type de requête. Déplacer le setter avant `PropertiesOf` rend la même erreur -non-représentable sans coût supplémentaire. - -### Un défaut ambiant à l'échelle du processus, configuré une fois (un `Configure` statique) - -Considérée parce qu'elle laisserait `Bind.PropertiesOf(request)` ramasser une politique à -l'échelle de l'application sans rien faire transiter par les points d'appel. - -Rejetée parce qu'elle introduirait le premier état global mutable de la bibliothèque, -contredisant la position établie « aucun état ambiant mutable » (ADR-0006) : il fuirait entre -les tests exécutés en parallèle et réintroduirait un piège « configuré au mauvais moment » qui -lui est propre. L'ergonomie de configuration applicative appartient à la future intégration -ASP.NET Core, via l'injection de dépendances, où elle est sûre vis-à-vis des tests par -construction. - -### Garder le setter d'instance et documenter l'ordre plus fermement - -Considérée parce que c'est le plus petit changement. - -Rejetée parce qu'un « appelez avant de lier » en prose est exactement la convention non imposée -qui a produit le défaut ; la documentation ne peut pas rendre l'enveloppe incohérente -non-représentable. +Fixer les options avant l'existence du binder rend les enveloppes mélangeant plusieurs politiques impossibles à représenter plutôt que de seulement les détecter plus tard. + +Conserver les options hors du binder spécifique à la requête reflète également leur véritable portée : une politique de nommage et d'erreurs structurelles est une configuration applicative, pas une donnée de requête, et un point d'entrée configuré peut être réutilisé en toute sécurité. + +La décision porte sur le moment où les options deviennent fixes, pas sur l'existence éventuelle d'une valeur par défaut applicative. L'[ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md) réexamine plus tard l'alternative du défaut global avec des garde-fous supplémentaires de gel et d'isolation des tests, tout en préservant l'immutabilité au point d'entrée définie ici. + +Le type exact du point d'entrée configuré, la forme fluent, l'héritage par les binders imbriqués et les exemples sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) et le guide du Request Binder. + +## Alternatives envisagées + +### Verrouiller les options à l'exécution lors du premier binding + +Envisagé pour préserver davantage la surface précédente. Rejeté parce que l'ordre invalide continuerait à compiler et n'échouerait qu'à l'exécution. + +### Fournir uniquement une valeur par défaut globale au processus + +Envisagé pour supprimer la configuration des points d'appel. Rejeté à l'époque parce qu'un défaut global non contraint pouvait dériver pendant l'exécution et fuiter entre tests parallèles. L'ADR-0017 adopte ensuite une forme contrainte, gelée à la première utilisation, sans modifier la décision de cet ADR. + +### Conserver le setter d'instance mutable et renforcer la documentation + +Envisagé pour minimiser les changements. Rejeté parce que la documentation ne peut pas rendre une enveloppe incohérente impossible. ## Conséquences ### Positives -* Une seule enveloppe d'échec rapporte toujours les chemins d'argument sous une seule politique - de nommage ; l'incohérence à deux politiques est impossible à écrire. -* Les options ne dépendent plus du type de requête, et le point d'entrée configuré est - réutilisable d'une requête à l'autre — une politique configurée une fois, réutilisée par - requête. -* Le binder préserve intacte la propriété « aucun état global mutable » de la bibliothèque. +* Chaque enveloppe d'échec utilise une politique unique de nommage et d'erreurs structurelles. +* Une configuration tardive invalide devient impossible par la forme publique. +* Les points d'entrée configurés peuvent être réutilisés entre les requêtes. ### Négatives -* La chaîne fluide gagne un point d'entrée distinct - (`Bind.WithOptions(...).PropertiesOf(...)`) à côté du défaut `Bind.PropertiesOf(...)`, et un - nouveau type public `ConfiguredBind` à documenter. -* Un consommateur qui posait les options après `PropertiesOf` / `FailWith` doit déplacer - l'appel avant `PropertiesOf` — un changement de source, atténué par le statut de pré-version - (aucun consommateur externe). +* La configuration possède un chemin d'entrée distinct que les consommateurs doivent apprendre. +* Le code existant utilisant un setter tardif doit déplacer la configuration avant le binding. ### Risques -* Un besoin futur de faire varier les options par binder imbriqué ne cadrerait pas avec le - modèle « fixé au point d'entrée » ; atténué par le fait que les binders imbriqués héritent - des options du parent par conception, et que cela est hors des exigences actuelles. +* Un futur besoin d'options volontairement différentes dans un binder imbriqué ne rentrerait pas dans ce modèle. Mesure : exiger une nouvelle décision explicite plutôt que de réintroduire une mutation tardive. ## Actions de suivi -* Documenter l'ergonomie de configuration applicative (injection de dépendances) lors de la - construction de l'intégration ASP.NET Core, plutôt que d'ajouter une configuration ambiante - au cœur. +* Maintenir les recommandations d'injection de dépendances et de valeur par défaut applicative dans la documentation d'intégration et utilisateur. ## Références -* ADR-0006 — fournir les valeurs de test arbitraires depuis une source unique réamorçable ; la - position « aucun état ambiant mutable » que cette décision préserve. -* ADR-0007 — nommer les terminaux du binder New et Create ; une décision d'API publique sœur - sur le même binder. -* Issue #145 — le constat que cette décision résout. -* Pull request #126 — la fonctionnalité de request binder à laquelle ces options appartiennent. +* [Référence d'implémentation des ADR — Contrats d'implémentation du Request Binder](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) +* [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md) — réexamine l'alternative du défaut global tout en préservant des options fixes par binder. +* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) +* [ADR-0007](0007-name-the-binder-terminals-new-and-create.fr.md) +* Issue #145 et pull request #126. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 0fa459d6d55671995121455eb3df1275a9e5f359 Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:56:34 +0200 Subject: [PATCH 31/33] docs: separate the binder-default decision from its implementation --- ...ion-wide-default-for-the-binder-options.md | 121 +++++------------- 1 file changed, 34 insertions(+), 87 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md index 571fc433..1a3a808e 100644 --- a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md +++ b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md @@ -8,123 +8,70 @@ ## Context -* `Bind.PropertiesOf(request)` binds with `RequestBinderOptions.Default`. Binding with - custom options otherwise requires `Bind.WithOptions(options).PropertiesOf(...)` — - threading the configured entry point through each call, or resolving it from a DI - container. -* A host without a DI container — a CLI, a worker, a small tool — has no host-agnostic - way to set an application-wide default that the bare `Bind.PropertiesOf` picks up. -* ADR-0012 fixed a binder's options at its entry point (no change once binding has - begun) and, among its rejected alternatives, rejected "a process-wide ambient default - configured once (a static `Configure`)" on the grounds that it would introduce global - mutable state, leak across tests running in parallel, and could be configured at the - wrong time. -* `RequestBinderOptions` is an immutable value: a shared instance carries no mutable - settings state, so the classic hazard — mutating a shared settings object while it is - in use — does not apply to it. -* The .NET convention is split: `Newtonsoft.Json`'s `JsonConvert.DefaultSettings` is a - freely re-settable global; `System.Text.Json`'s `JsonSerializerOptions` becomes - immutable on first use (frozen) and is configured per-instance or through DI. -* The library exposes no other ambient mutable state: the clock, instance-id and - arbitrary-value seams are immutable defaults with an `AsyncLocal`, scoped, test-only - override (ADR-0006). -* The library is pre-release, unpublished on NuGet with no external consumers. +ADR-0012 fixed a binder's options before binding begins and retained an explicit configured entry point. + +Applications without a dependency-injection container can still need one application-wide naming and structural-error policy while using the bare binding entry. Repeating or manually threading a configured entry through every call site is possible but less convenient. + +A freely mutable process-wide default would introduce runtime drift and parallel-test interference. `RequestBinderOptions` itself is immutable, so the remaining hazard is reassignment of the shared reference after use. ## Decision -`RequestBinderOptions.Default` — the options `Bind.PropertiesOf` binds with — is a -settable application default, configured once at application startup and frozen on the -first bind that reads it. +`RequestBinderOptions.Default`, used by the bare binding entry, is configurable once during application composition and becomes immutable after its first binding use. ## Rationale -* A settable process default is the only host-agnostic way for the bare - `Bind.PropertiesOf` to pick up an application-wide policy without a DI container, which - a CLI or worker needs; the DI-friendly entry point (`Bind.WithOptions`) stays available - where a container exists. -* The hazard ADR-0012 guarded against — an ambient default that drifts at runtime — is - removed by freezing on first use: once the first bind reads it, reassignment throws, so - it is a composition-time choice that cannot change while requests are flowing. This is - the `System.Text.Json` discipline (immutable-once-used), not the freely-mutable - `JsonConvert.DefaultSettings` one. -* Because `RequestBinderOptions` is immutable, a shared default carries no mutable - settings state, so the classic global-settings footgun does not apply; the only global - state is which immutable options the default points at, fixed once. -* Keeping the configuration on `RequestBinderOptions.Default` rather than on a method of - `Bind` leaves the binding entry point free of configuration surface: a developer binding - requests sees only binding verbs. -* The parallel-test-isolation concern ADR-0012 raised is confined to the library's own - tests, and is met by a scoped, test-only override (an `AsyncLocal`) — the same seam - pattern the clock uses (ADR-0006) — which never touches or freezes the production - default. -* The pre-release status means the surface is settled now, when there are no consumers to - migrate. +A configurable default gives hosts without dependency injection a host-agnostic way to establish one application policy while preserving the explicit `Bind.WithOptions` path for injected or per-call configuration. -## Alternatives Considered +Freezing the reference on first use constrains the global state to application startup and prevents the runtime drift that ADR-0012 rejected. The shared object is itself immutable, so consumers cannot mutate an in-use settings instance. -### Keep options entry-point-only (the status quo of ADR-0012) +A scoped test-only override preserves parallel test isolation without making the production default freely resettable. -Considered because it is already shipped and has no global state at all. +This decision deliberately revisits one alternative rejected by ADR-0012 with stronger constraints; it does not change ADR-0012's decision that every individual binder receives fixed options before binding begins. -Rejected because it offers no host-agnostic application-wide default: every call site must -thread the configured entry point, or a DI container must supply it — unavailable to a CLI -or worker that wants to configure the binder once. +The exact freeze semantics, exception behavior, test seam, and entry-point interaction are documented in the [ADR implementation reference](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) and the Request Binder documentation. + +## Alternatives Considered -### A freely re-settable global default (the JsonConvert.DefaultSettings model) +### Keep only explicit configured entries -Considered because it is the simplest settable global and a widely-used convention. +Considered because it avoids all process-global state. Rejected because container-free hosts would need to thread configuration through every binding call even when the policy is application-wide. -Rejected because a default that can be reassigned while requests are flowing can drift, -reintroducing the runtime-configuration hazard ADR-0012 warned about. Freezing on first -use keeps the ergonomic while removing the drift. +### Use a freely resettable global default -### Dependency injection only (the System.Text.Json / ASP.NET model) +Considered because it is the simplest global configuration model. Rejected because it can change while requests are being processed and makes test isolation unsafe. -Considered because it is the modern idiom where a container exists, and is fully -test-safe. +### Require dependency injection -Rejected as the sole mechanism because it is not host-agnostic: a CLI, a worker, or any -host without a DI container cannot use it to make the bare `Bind.PropertiesOf` pick up an -application default. The injected entry point stays available; this decision adds the -container-free path. +Considered because it is test-safe and idiomatic where a container exists. Rejected as the only mechanism because the library is host-agnostic and supports CLIs, workers, and small tools without DI. ## Consequences ### Positive -* Any host — with or without DI — configures the binder's naming policy and structural - codes once at startup, and the bare `Bind.PropertiesOf` uses them. -* Freezing on first use prevents runtime drift; the only mutable global is an - immutable-options reference set once. -* `Bind`'s surface stays free of configuration; a per-call `Bind.WithOptions` still - overrides the default. +* Hosts with or without dependency injection can configure one application-wide binder policy. +* The default cannot drift after binding starts. +* Explicit configured entries remain available and can override the default. ### Negative -* The library gains one piece of process-global state (the settable default) — the first - such state in the library, accepted deliberately for the host-agnostic ergonomic. -* The library's own tests need a scoped, test-only override seam to stay parallel-safe; - the production default is not directly settable within a parallel suite. +* The library accepts one process-global configuration reference. +* Reading the default too early can freeze it before intended application configuration. +* Tests require a dedicated scoped override rather than resetting production state. ### Risks -* A consumer that reads `RequestBinderOptions.Default` before configuring it freezes it - and can then no longer configure it; mitigated by the throwing setter's diagnostic - ("configure at startup, before the first bind") and by documentation. +* Hidden initialization order can make configuration fail if another component binds first. Mitigation: document startup ordering and fail loudly on late assignment. +* Consumers may use the global default where explicit configuration would be clearer. Mitigation: keep `Bind.WithOptions` prominent and recommend it for libraries, tests, and composition roots with DI. ## Follow-up Actions -* Surface the test-override seam to consumers through a dedicated testing package if - consumer demand appears (it is currently internal, for the library's own tests). +* Expose a consumer testing seam only if demand justifies adding it to a dedicated testing package. +* Reconsider the global default before the stable release if real usage shows that explicit reusable configured entries are sufficient. ## References -* ADR-0012 — fix the binder options before binding begins; this decision revisits the - process-wide ambient default that ADR-0012 weighed and rejected as an alternative, - adopting it with mitigations. ADR-0012's own decision — a binder's options are fixed at - its entry point — is unchanged, so ADR-0012 is not superseded. -* ADR-0006 — supply arbitrary test values from a single seedable source; the `AsyncLocal` - test-seam pattern this decision reuses for its own tests. -* Issue #181 — the request this decision resolves. -* `JsonConvert.DefaultSettings` (Newtonsoft.Json) and `JsonSerializerOptions` - (System.Text.Json) — the two conventions weighed. +* [ADR implementation reference — Request Binder implementation contracts](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) +* [ADR-0012](0012-fix-the-binder-options-before-binding-begins.md) — this ADR revisits one rejected alternative while preserving fixed options per binder. +* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) +* Issue #181. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. From 7dfb5e993d256c185d5d431fee36baf72f4e82eb Mon Sep 17 00:00:00 2001 From: Sylvain Aurat Date: Sun, 19 Jul 2026 23:57:00 +0200 Subject: [PATCH 32/33] docs: translate ADR-0017's editorial rewrite to French --- ...-wide-default-for-the-binder-options.fr.md | 146 ++++++------------ 1 file changed, 44 insertions(+), 102 deletions(-) diff --git a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md index 6ffd2b5f..3333a16c 100644 --- a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md +++ b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md @@ -8,128 +8,70 @@ ## Contexte -* `Bind.PropertiesOf(request)` lie avec `RequestBinderOptions.Default`. Lier avec des - options personnalisées exige sinon `Bind.WithOptions(options).PropertiesOf(...)` — faire - transiter le point d'entrée configuré par chaque appel, ou le résoudre depuis un - conteneur DI. -* Un hôte sans conteneur DI — une CLI, un worker, un petit outil — n'a aucun moyen - host-agnostic de poser un défaut applicatif que le simple `Bind.PropertiesOf` ramasse. -* L'ADR-0012 a fixé les options d'un binder à son point d'entrée (aucun changement une - fois la liaison commencée) et, parmi ses alternatives rejetées, a écarté « un défaut - ambiant à l'échelle du processus, configuré une fois (un `Configure` statique) » au - motif qu'il introduirait un état global mutable, fuirait entre les tests exécutés en - parallèle, et pourrait être configuré au mauvais moment. -* `RequestBinderOptions` est une valeur immuable : une instance partagée ne porte aucun - état de settings mutable, donc le hasard classique — muter un objet de settings partagé - pendant qu'il est utilisé — ne s'y applique pas. -* La convention .NET est partagée : `JsonConvert.DefaultSettings` de `Newtonsoft.Json` est - un global librement re-posable ; `JsonSerializerOptions` de `System.Text.Json` devient - immuable à la première utilisation (gelé) et se configure par-instance ou via DI. -* La bibliothèque n'expose aucun autre état ambiant mutable : les points d'extension de - l'horloge, de l'identifiant d'instance et des valeurs arbitraires sont des défauts - immuables avec un override `AsyncLocal`, scoped, réservé aux tests (ADR-0006). -* La bibliothèque est en pré-version, non publiée sur NuGet et sans consommateur externe. +L'ADR-0012 a fixé les options d'un binder avant le début du binding et conservé un point d'entrée configuré explicite. + +Les applications sans conteneur d'injection de dépendances peuvent néanmoins avoir besoin d'une politique unique de nommage et d'erreurs structurelles à l'échelle de l'application tout en utilisant le point d'entrée de binding simple. Répéter ou transmettre manuellement un point d'entrée configuré à chaque appel reste possible mais moins pratique. + +Une valeur par défaut globale librement mutable introduirait une dérive à l'exécution et des interférences entre tests parallèles. `RequestBinderOptions` est lui-même immuable ; le risque restant est la réaffectation de la référence partagée après utilisation. ## Décision -`RequestBinderOptions.Default` — les options avec lesquelles `Bind.PropertiesOf` lie — -est un défaut applicatif posable, configuré une fois au démarrage de l'application et gelé -à la première liaison qui le lit. +`RequestBinderOptions.Default`, utilisé par le point d'entrée de binding simple, est configurable une seule fois pendant la composition de l'application puis devient immuable après sa première utilisation par un binding. ## Justification -* Un défaut processus posable est le seul moyen host-agnostic pour que le simple - `Bind.PropertiesOf` ramasse une politique applicative sans conteneur DI, ce dont une CLI - ou un worker a besoin ; le point d'entrée favorable à la DI (`Bind.WithOptions`) reste - disponible là où un conteneur existe. -* Le hasard contre lequel l'ADR-0012 se prémunissait — un défaut ambiant qui dérive à - l'exécution — est supprimé par le gel à la première utilisation : dès que la première - liaison le lit, une réaffectation lève, donc c'est un choix au moment de la composition - qui ne peut pas changer une fois les requêtes en vol. C'est la discipline de - `System.Text.Json` (immuable une fois utilisé), pas celle, librement mutable, de - `JsonConvert.DefaultSettings`. -* Parce que `RequestBinderOptions` est immuable, un défaut partagé ne porte aucun état de - settings mutable, donc le piège classique des settings globaux ne s'applique pas ; le - seul état global est vers quelles options immuables pointe le défaut, fixé une fois. -* Garder la configuration sur `RequestBinderOptions.Default` plutôt que sur une méthode de - `Bind` laisse le point d'entrée de liaison sans surface de configuration : un - développeur qui lie des requêtes ne voit que des verbes de liaison. -* La préoccupation d'isolation des tests parallèles soulevée par l'ADR-0012 se limite aux - tests de la bibliothèque elle-même, et est satisfaite par un override scoped réservé aux - tests (un `AsyncLocal`) — le même patron que l'horloge (ADR-0006) — qui ne touche ni ne - gèle jamais le défaut de production. -* Le statut de pré-version signifie que la surface est arrêtée maintenant, quand il n'y a - aucun consommateur à migrer. - -## Alternatives considérées - -### Garder les options au seul point d'entrée (le statu quo de l'ADR-0012) - -Considérée parce qu'elle est déjà livrée et n'a aucun état global. - -Rejetée parce qu'elle n'offre aucun défaut applicatif host-agnostic : chaque point d'appel -doit faire transiter le point d'entrée configuré, ou un conteneur DI doit le fournir — -indisponible pour une CLI ou un worker qui veut configurer le binder une fois. - -### Un défaut global librement re-posable (le modèle JsonConvert.DefaultSettings) - -Considérée parce que c'est le global posable le plus simple et une convention répandue. - -Rejetée parce qu'un défaut réaffectable pendant que les requêtes sont en vol peut dériver, -réintroduisant le hasard de configuration à l'exécution contre lequel l'ADR-0012 -prévenait. Le gel à la première utilisation garde l'ergonomie tout en supprimant la -dérive. - -### L'injection de dépendances seule (le modèle System.Text.Json / ASP.NET) - -Considérée parce que c'est l'idiome moderne là où un conteneur existe, et pleinement -sûre vis-à-vis des tests. - -Rejetée comme unique mécanisme parce qu'elle n'est pas host-agnostic : une CLI, un worker, -ou tout hôte sans conteneur DI ne peut pas s'en servir pour que le simple -`Bind.PropertiesOf` ramasse un défaut applicatif. Le point d'entrée injecté reste -disponible ; cette décision ajoute le chemin sans conteneur. +Une valeur par défaut configurable offre aux hôtes sans injection de dépendances un moyen agnostique d'établir une politique applicative unique, tout en préservant le chemin explicite `Bind.WithOptions` pour la configuration injectée ou propre à un appel. + +Geler la référence lors de la première utilisation limite l'état global à la phase de démarrage et empêche la dérive d'exécution rejetée par l'ADR-0012. L'objet partagé étant lui-même immuable, les consommateurs ne peuvent pas modifier une instance de configuration déjà utilisée. + +Un override local aux tests et limité à leur scope préserve l'isolation parallèle sans rendre la valeur de production librement réinitialisable. + +Cette décision réexamine volontairement une alternative rejetée par l'ADR-0012 avec des contraintes plus fortes ; elle ne modifie pas la décision de l'ADR-0012 selon laquelle chaque binder reçoit des options fixes avant le début du binding. + +La sémantique exacte du gel, le comportement d'exception, le seam de test et l'interaction avec les points d'entrée sont documentés dans la [référence d'implémentation des ADR](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) et la documentation du Request Binder. + +## Alternatives envisagées + +### Conserver uniquement les points d'entrée configurés explicitement + +Envisagé car cela évite tout état global au processus. Rejeté parce que les hôtes sans conteneur devraient transmettre la configuration à chaque appel de binding, même lorsqu'elle est unique à l'échelle de l'application. + +### Utiliser une valeur par défaut globale librement réinitialisable + +Envisagé car il s'agit du modèle global le plus simple. Rejeté parce qu'elle pourrait changer pendant le traitement des requêtes et rendrait l'isolation des tests dangereuse. + +### Exiger l'injection de dépendances + +Envisagé car elle est sûre pour les tests et idiomatique lorsqu'un conteneur existe. Rejeté comme mécanisme unique parce que la bibliothèque est agnostique de l'hôte et prend en charge les CLI, workers et petits outils sans DI. ## Conséquences ### Positives -* N'importe quel hôte — avec ou sans DI — configure la politique de nommage et les codes - structurels du binder une fois au démarrage, et le simple `Bind.PropertiesOf` les - utilise. -* Le gel à la première utilisation empêche la dérive à l'exécution ; le seul global mutable - est une référence d'options immuables posée une fois. -* La surface de `Bind` reste sans configuration ; un `Bind.WithOptions` par appel surcharge - quand même le défaut. +* Les hôtes avec ou sans injection de dépendances peuvent configurer une politique applicative unique du binder. +* La valeur par défaut ne peut plus dériver après le début du binding. +* Les points d'entrée explicitement configurés restent disponibles et peuvent remplacer le défaut. ### Négatives -* La bibliothèque gagne un état global de processus (le défaut posable) — le premier de la - bibliothèque, accepté délibérément pour l'ergonomie host-agnostic. -* Les tests de la bibliothèque ont besoin d'un seam d'override scoped réservé aux tests pour - rester parallèle-safe ; le défaut de production n'est pas directement posable dans une - suite parallèle. +* La bibliothèque accepte une référence de configuration globale au processus. +* Lire le défaut trop tôt peut le figer avant la configuration applicative prévue. +* Les tests nécessitent un override dédié et scoped plutôt qu'une réinitialisation de l'état de production. ### Risques -* Un consommateur qui lit `RequestBinderOptions.Default` avant de le configurer le gèle et - ne peut alors plus le configurer ; atténué par le diagnostic du setter qui lève - (« configurez au démarrage, avant la première liaison ») et par la documentation. +* Un ordre d'initialisation caché peut faire échouer la configuration si un autre composant lie avant elle. Mesure : documenter l'ordre de démarrage et échouer bruyamment en cas d'affectation tardive. +* Les consommateurs peuvent utiliser le défaut global alors qu'une configuration explicite serait plus claire. Mesure : conserver `Bind.WithOptions` visible et le recommander pour les bibliothèques, les tests et les composition roots avec DI. ## Actions de suivi -* Faire apparaître le seam d'override de test aux consommateurs via un paquet de test dédié - si une demande apparaît (il est actuellement interne, pour les tests de la bibliothèque). +* N'exposer un seam de test aux consommateurs que si la demande justifie son ajout dans un package de test dédié. +* Réexaminer le défaut global avant la version stable si l'usage réel montre que les points d'entrée configurés et réutilisables suffisent. ## Références -* ADR-0012 — fixer les options du binder avant le début de la liaison ; cette décision - revisite le défaut ambiant à l'échelle du processus que l'ADR-0012 avait pesé puis - rejeté comme alternative, en l'adoptant avec des garde-fous. La décision propre à - l'ADR-0012 — les options d'un binder sont fixées à son point d'entrée — est inchangée, - donc l'ADR-0012 n'est pas supersédé. -* ADR-0006 — fournir les valeurs de test arbitraires depuis une source unique réamorçable ; - le patron de seam de test `AsyncLocal` que cette décision réutilise pour ses tests. -* Issue #181 — la demande que cette décision résout. -* `JsonConvert.DefaultSettings` (Newtonsoft.Json) et `JsonSerializerOptions` - (System.Text.Json) — les deux conventions pesées. +* [Référence d'implémentation des ADR — Contrats d'implémentation du Request Binder](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) +* [ADR-0012](0012-fix-the-binder-options-before-binding-begins.fr.md) — cet ADR réexamine une alternative rejetée tout en préservant des options fixes par binder. +* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) +* Issue #181. +* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. From 7d74b82ea9dda498da1d8aeb7444d14eaee47ba9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 22:36:03 +0000 Subject: [PATCH 33/33] docs: renumber the ADR editorial-migration record to ADR-0024 PR #201 merged and claimed ADR-0023 (keep expression-tree selectors for the v1 binder API) while this branch was still open with its own ADR-0023 (the one-time editorial-refactoring exception). Renumbers the latter to ADR-0024 and updates every cross-reference so no two records share a number. --- .../adr/0001-lock-the-analyzer-roslyn-floor.fr.md | 2 +- .../adr/0001-lock-the-analyzer-roslyn-floor.md | 2 +- .../for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md | 2 +- .../for-maintainers/adr/0002-floor-the-tooling-runtime.md | 2 +- .../0004-check-every-pull-request-against-the-adr-base.fr.md | 2 +- .../adr/0004-check-every-pull-request-against-the-adr-base.md | 2 +- ...ype-properties-through-a-struct-constrained-overload.fr.md | 2 +- ...e-type-properties-through-a-struct-constrained-overload.md | 2 +- ...-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md | 2 +- ...010-treat-gendocs-error-catalog-as-a-versioned-contract.md | 2 +- .../adr/0011-host-dummies-as-a-standalone-package.fr.md | 2 +- .../adr/0011-host-dummies-as-a-standalone-package.md | 2 +- .../0012-fix-the-binder-options-before-binding-begins.fr.md | 2 +- .../adr/0012-fix-the-binder-options-before-binding-begins.md | 2 +- ...istinct-collections-by-cardinality-else-bounded-draw.fr.md | 2 +- ...e-distinct-collections-by-cardinality-else-bounded-draw.md | 2 +- .../adr/0015-cap-any-combine-at-arity-eight.fr.md | 2 +- .../adr/0015-cap-any-combine-at-arity-eight.md | 2 +- ...able-application-wide-default-for-the-binder-options.fr.md | 2 +- ...gurable-application-wide-default-for-the-binder-options.md | 2 +- ...nt-overridden-binder-errors-in-the-consumers-catalog.fr.md | 2 +- ...ument-overridden-binder-errors-in-the-consumers-catalog.md | 2 +- ...o-arguments-as-peers-through-a-source-agnostic-entry.fr.md | 2 +- ...-dto-arguments-as-peers-through-a-source-agnostic-entry.md | 2 +- .../adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md | 2 +- .../adr/0022-floor-the-library-on-net-framework-4-7-2.md | 2 +- ...w-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md} | 4 ++-- ...llow-a-one-time-editorial-refactoring-of-accepted-adrs.md} | 4 ++-- 28 files changed, 30 insertions(+), 30 deletions(-) rename doc/handwritten/for-maintainers/adr/{0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md => 0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md} (97%) rename doc/handwritten/for-maintainers/adr/{0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md => 0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md} (97%) diff --git a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md index dd17c9f2..5eba826c 100644 --- a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md +++ b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.fr.md @@ -70,4 +70,4 @@ Envisagé car il est rapide et déterministe. Rejeté parce qu'il ne peut prouve * [Référence d'implémentation des ADR — Plancher de compatibilité de l'analyseur](../specifications/adr-implementation-reference.fr.md#plancher-de-compatibilité-de-lanalyseur) * [Référence du workflow `analyzers`](../workflows/analyzers.fr.md) * [ADR-0002](0002-floor-the-tooling-runtime.fr.md) — la décision équivalente pour le runtime. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md index 7a5fca69..40aa17d8 100644 --- a/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md +++ b/doc/handwritten/for-maintainers/adr/0001-lock-the-analyzer-roslyn-floor.md @@ -70,4 +70,4 @@ Considered because it is fast and deterministic. Rejected because it cannot prov * [ADR implementation reference — Analyzer compatibility floor](../specifications/adr-implementation-reference.md#analyzer-compatibility-floor) * [`analyzers` workflow reference](../workflows/analyzers.en.md) * [ADR-0002](0002-floor-the-tooling-runtime.md) — the runtime counterpart of this compatibility decision. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md index 5e4a720f..9bb24e49 100644 --- a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md +++ b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.fr.md @@ -69,4 +69,4 @@ Envisagé comme stratégie classique de compatibilité. Rejeté parce qu'un buil * [Référence du workflow `ci`](../workflows/ci.fr.md) * [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) — la décision correspondante pour l'hôte de l'analyseur. * [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.fr.md) — raffine le plancher .NET Framework de la bibliothèque et remplace la mention incidente de 4.6.1 auparavant présente dans cet ADR. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md index 27b0d0d2..8d6e3eb1 100644 --- a/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md +++ b/doc/handwritten/for-maintainers/adr/0002-floor-the-tooling-runtime.md @@ -69,4 +69,4 @@ Considered as the conventional compatibility strategy. Rejected because one floo * [`ci` workflow reference](../workflows/ci.en.md) * [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) — the analyzer-host counterpart. * [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) — refines the library's .NET Framework floor; it replaces the incidental 4.6.1 statement formerly present in this ADR. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md index 9521804e..855ab310 100644 --- a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md +++ b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.fr.md @@ -74,4 +74,4 @@ Envisagé car cela ne nécessite aucun processus. Rejeté parce que cela laisse * `CLAUDE.md` — les instructions en session. * [Référence d'implémentation des ADR — Vérification ADR des pull requests](../specifications/adr-implementation-reference.fr.md#vérification-adr-des-pull-requests) * [Référence du workflow `adr-check`](../workflows/adr-check.fr.md) -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md index da4989f1..2d21ff4b 100644 --- a/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md +++ b/doc/handwritten/for-maintainers/adr/0004-check-every-pull-request-against-the-adr-base.md @@ -74,4 +74,4 @@ Considered because it requires no process. Rejected because it leaves exactly th * `CLAUDE.md` — the in-session guidance. * [ADR implementation reference — ADR pull-request check](../specifications/adr-implementation-reference.md#adr-pull-request-check) * [`adr-check` workflow reference](../workflows/adr-check.en.md) -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md index 84f5fcc1..6901629f 100644 --- a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md +++ b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.fr.md @@ -72,4 +72,4 @@ Envisagé car le flux de conversion est autrement similaire. Rejeté parce que l * [Référence d'implémentation des ADR — Contrats d'implémentation du Request Binder](../specifications/adr-implementation-reference.fr.md#contrats-dimplémentation-du-request-binder) * [ADR-0007](0007-name-the-binder-terminals-new-and-create.fr.md) * Issue #144 et pull requests #126 et #141. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md index be58670c..ecba044b 100644 --- a/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md +++ b/doc/handwritten/for-maintainers/adr/0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md @@ -72,4 +72,4 @@ Considered because the conversion flow is otherwise similar. Rejected because nu * [ADR implementation reference — Request Binder implementation contracts](../specifications/adr-implementation-reference.md#request-binder-implementation-contracts) * [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) * Issue #144 and pull requests #126 and #141. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md index 82084ab7..33bcdc9f 100644 --- a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md +++ b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md @@ -72,4 +72,4 @@ Envisagé pour donner au catalogue sa propre version. Rejeté parce que GenDoc n * [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.fr.md) * [ADR-0002](0002-floor-the-tooling-runtime.fr.md) * Issue #167. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md index b988b112..808fa9d8 100644 --- a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md +++ b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md @@ -72,4 +72,4 @@ Considered because it would give the catalog its own version. Rejected because G * [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.md) * [ADR-0002](0002-floor-the-tooling-runtime.md) * Issue #167. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md index b43d4899..342b59d5 100644 --- a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md +++ b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.fr.md @@ -72,4 +72,4 @@ Envisagé parce qu'elle est déjà publiée. Rejeté parce que cela couplerait u * [Référence d'implémentation des ADR — Contrats de génération de Dummies](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) * [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) * Tests d'architecture dans `Dummies.UnitTests`. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md index 9e9e2645..12191950 100644 --- a/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md +++ b/doc/handwritten/for-maintainers/adr/0011-host-dummies-as-a-standalone-package.md @@ -72,4 +72,4 @@ Considered because it already ships. Rejected because it would couple a generic * [ADR implementation reference — Dummies generation contracts](../specifications/adr-implementation-reference.md#dummies-generation-contracts) * [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) * Architecture tests in `Dummies.UnitTests`. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md index 81686db0..4a6a4e89 100644 --- a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md +++ b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.fr.md @@ -72,4 +72,4 @@ Envisagé pour minimiser les changements. Rejeté parce que la documentation ne * [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) * [ADR-0007](0007-name-the-binder-terminals-new-and-create.fr.md) * Issue #145 et pull request #126. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md index 10e1da3f..87eeb4cf 100644 --- a/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md +++ b/doc/handwritten/for-maintainers/adr/0012-fix-the-binder-options-before-binding-begins.md @@ -72,4 +72,4 @@ Considered because it requires the smallest code change. Rejected because docume * [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) * [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) * Issue #145 and pull request #126. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md index e1cb07e0..9ca2a365 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md @@ -74,4 +74,4 @@ Envisagé car une demande satisfaisable finirait par aboutir. Rejeté parce qu'u * [Référence d'implémentation des ADR — Contrats de génération de Dummies](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) * [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) * `CollectionState` et `ICardinalityHint` dans le projet `Dummies`. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md index e03a75b5..f07e1ec2 100644 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md +++ b/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md @@ -74,4 +74,4 @@ Considered because a satisfiable request would eventually complete. Rejected bec * [ADR implementation reference — Dummies generation contracts](../specifications/adr-implementation-reference.md#dummies-generation-contracts) * [ADR-0011](0011-host-dummies-as-a-standalone-package.md) * `CollectionState` and `ICardinalityHint` in the `Dummies` project. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md index 8ffbe578..861de058 100644 --- a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md +++ b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md @@ -73,4 +73,4 @@ Envisagé car cette forme est naturellement variadique. Rejeté parce qu'elle ne * [Référence d'implémentation des ADR — Contrats de génération de Dummies](../specifications/adr-implementation-reference.fr.md#contrats-de-génération-de-dummies) * [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md index b47a0cc2..27b1e5fe 100644 --- a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md +++ b/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md @@ -73,4 +73,4 @@ Considered because it is naturally variadic. Rejected because it does not serve * [ADR implementation reference — Dummies generation contracts](../specifications/adr-implementation-reference.md#dummies-generation-contracts) * [ADR-0011](0011-host-dummies-as-a-standalone-package.md) -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md index 3333a16c..e29edca1 100644 --- a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md +++ b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md @@ -74,4 +74,4 @@ Envisagé car elle est sûre pour les tests et idiomatique lorsqu'un conteneur e * [ADR-0012](0012-fix-the-binder-options-before-binding-begins.fr.md) — cet ADR réexamine une alternative rejetée tout en préservant des options fixes par binder. * [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) * Issue #181. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md index 1a3a808e..97b78c08 100644 --- a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md +++ b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md @@ -74,4 +74,4 @@ Considered because it is test-safe and idiomatic where a container exists. Rejec * [ADR-0012](0012-fix-the-binder-options-before-binding-begins.md) — this ADR revisits one rejected alternative while preserving fixed options per binder. * [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) * Issue #181. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md index 9d11e094..82a387b0 100644 --- a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md +++ b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md @@ -71,4 +71,4 @@ Envisagé pour conserver le câblage hors du code. Rejeté parce que les noms de * [ADR-0018](0018-bundle-the-binders-structural-error-code-and-messages.fr.md) * [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.fr.md) — origine remplacée de la question différée. * Issue #140 et analyseur FCE009. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md index d83f8224..242b2fb7 100644 --- a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md +++ b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md @@ -71,4 +71,4 @@ Considered to keep the wiring outside code. Rejected because member names would * [ADR-0018](0018-bundle-the-binders-structural-error-code-and-messages.md) * [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.md) — superseded origin of the deferred question. * Issue #140 and analyzer FCE009. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md index f34fe63a..8a0440aa 100644 --- a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md +++ b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md @@ -86,4 +86,4 @@ Envisagé pour éviter une seconde valeur de contexte. Rejeté parce que le chem * [ADR-0012](0012-fix-the-binder-options-before-binding-begins.fr.md) — les options restent fixées au point d'entrée agnostique de la source ; la forme illustrative de l'API est mise à jour par cet ADR. * [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md) — la valeur par défaut reste valide ; la forme illustrative de l'API est mise à jour par cet ADR. * Issue #148. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md index 0aa37756..352a272c 100644 --- a/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md +++ b/doc/handwritten/for-maintainers/adr/0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md @@ -86,4 +86,4 @@ Considered to avoid a second context value. Rejected because path and origin ans * [ADR-0012](0012-fix-the-binder-options-before-binding-begins.md) — options remain fixed at the source-agnostic entry; illustrative API shape updated by this ADR. * [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) — the default remains valid; illustrative API shape updated by this ADR. * Issue #148. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md index 726b31b3..9543c4d2 100644 --- a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md +++ b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md @@ -76,4 +76,4 @@ Envisagé pour une assurance large. Rejeté parce que la frontière de compatibi * [ADR-0002](0002-floor-the-tooling-runtime.fr.md) — raffiné par cet ADR pour le plancher .NET Framework de la bibliothèque. * [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) * `FirstClassErrors/README.nuget.md` et la référence du workflow CI. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) — autorise cette extraction éditoriale. diff --git a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md index 4922caf3..ec16c347 100644 --- a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md +++ b/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md @@ -76,4 +76,4 @@ Considered for broad reassurance. Rejected because the valuable compatibility bo * [ADR-0002](0002-floor-the-tooling-runtime.md) — refined by this ADR for the library's .NET Framework floor. * [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) * `FirstClassErrors/README.nuget.md` and the CI workflow reference. -* [ADR-0023](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. +* [ADR-0024](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) — authorizes this editorial extraction. diff --git a/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md b/doc/handwritten/for-maintainers/adr/0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md similarity index 97% rename from doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md rename to doc/handwritten/for-maintainers/adr/0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md index 86c9df11..ddd6ceb3 100644 --- a/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md +++ b/doc/handwritten/for-maintainers/adr/0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md @@ -1,6 +1,6 @@ -# ADR-0023 | Autoriser un refactoring éditorial unique des ADR acceptés +# ADR-0024 | Autoriser un refactoring éditorial unique des ADR acceptés -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) +🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md) **Statut :** Accepté **Date :** 2026-07-19 diff --git a/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md b/doc/handwritten/for-maintainers/adr/0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md similarity index 97% rename from doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md rename to doc/handwritten/for-maintainers/adr/0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md index 6f1855e3..6a9b87de 100644 --- a/doc/handwritten/for-maintainers/adr/0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md +++ b/doc/handwritten/for-maintainers/adr/0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.md @@ -1,6 +1,6 @@ -# ADR-0023 | Allow a one-time editorial refactoring of accepted ADRs +# ADR-0024 | Allow a one-time editorial refactoring of accepted ADRs -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0023-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0024-allow-a-one-time-editorial-refactoring-of-accepted-adrs.fr.md) **Status:** Accepted **Date:** 2026-07-19