diff --git a/doc/handwritten/for-maintainers/AddingAReleaseTrain.en.md b/doc/handwritten/for-maintainers/AddingAReleaseTrain.en.md index dd782c7d..dfba7af8 100644 --- a/doc/handwritten/for-maintainers/AddingAReleaseTrain.en.md +++ b/doc/handwritten/for-maintainers/AddingAReleaseTrain.en.md @@ -84,6 +84,22 @@ everything else about the train from `trains.sh`.) missing). You may pre-create it by hand for a tidier first pull request, but you do not have to. +## Removing a train + +The reverse of this runbook, with one step it does not have a mirror for: **a train +whose workflow publishes a REQUIRED status check leaves that requirement behind.** +Deleting the workflow does not delete the rule that waits for it, so every later pull +request sits at `mergeable_state: blocked` with every check green — a state that reads +as a content problem and is not one. Remove the check names from the branch ruleset +(*Settings → Rules*) in the same breath as the workflow. + +This is not hypothetical: removing the `dum` train left `JustDummies mutation gate` and +`JustDummies packaged-asset compatibility` required, and the pull request that removed +it could not merge until they were dropped from the ruleset. + +Dropping a requirement for code the repository no longer contains is not a weakened +protection. Keeping it protects nothing and blocks everything. + ## Verify - **Commit convention:** make a commit under a new scope and confirm diff --git a/doc/handwritten/for-maintainers/AddingAReleaseTrain.fr.md b/doc/handwritten/for-maintainers/AddingAReleaseTrain.fr.md index 60270703..8c9fc736 100644 --- a/doc/handwritten/for-maintainers/AddingAReleaseTrain.fr.md +++ b/doc/handwritten/for-maintainers/AddingAReleaseTrain.fr.md @@ -91,6 +91,23 @@ workflow lit tout le reste du train depuis `trains.sh`.) Vous pouvez le pré-créer à la main pour une première pull request plus propre, mais ce n'est pas obligatoire. +## Retirer un train + +L'inverse de ce runbook, avec une étape dont il n'a pas le miroir : **un train dont le +workflow publie un check de statut REQUIS laisse cette exigence derrière lui.** +Supprimer le workflow ne supprime pas la règle qui l'attend, et toute pull request +ultérieure reste en `mergeable_state: blocked` avec tous ses checks au vert — un état +qui se lit comme un problème de contenu alors qu'il n'en est pas un. Retirez les noms +de ces checks du ruleset de la branche (*Settings → Rules*) dans le même mouvement que +le workflow. + +Ce n'est pas hypothétique : retirer le train `dum` a laissé `JustDummies mutation gate` +et `JustDummies packaged-asset compatibility` requis, et la pull request qui le +retirait n'a pas pu être mergée avant qu'ils ne soient sortis du ruleset. + +Retirer une exigence portant sur du code que le dépôt ne contient plus n'est pas un +affaiblissement de la protection. La garder ne protège rien et bloque tout. + ## Vérifier - **Convention de commit :** faites un commit sous un nouveau scope et confirmez que 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 0bcdf844..b37b9894 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 @@ -15,7 +15,7 @@ L'outillage ciblait auparavant la dernière version de .NET. Cela empêchait les 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. +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'[just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-floor-the-library-on-net-framework-4-7-2.md), qui raffine la mention incidente auparavant présente ici. ## Décision @@ -69,5 +69,5 @@ Envisagé comme stratégie classique de compatibilité. Rejeté parce qu'un buil * [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. +* [just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-floor-the-library-on-net-framework-4-7-2.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-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 748dd768..56abe236 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 @@ -15,7 +15,7 @@ The tooling previously targeted the latest .NET runtime. That prevented consumer 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. +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 [just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-floor-the-library-on-net-framework-4-7-2.md), which refines the incidental statement previously carried here. ## Decision @@ -69,5 +69,5 @@ Considered as the conventional compatibility strategy. Rejected because one floo * [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. +* [just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-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-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 deleted file mode 100644 index d77cf9e1..00000000 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md +++ /dev/null @@ -1,78 +0,0 @@ -# ADR-0013 | Contrôler les collections distinctes par la cardinalité, sinon par un tirage borné - -🌍 🇬🇧 [English](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-19 -**Accepté :** 2026-07-19 -**Décideurs :** Reefact - -## Contexte - -JustDummies 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 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 - -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 JustDummies. - -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 JustDummies. - -## 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 - -* 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 diffère entre domaines connus et inconnus. -* Un tirage borné peut échouer pour un générateur théoriquement satisfaisable mais fortement biaisé. - -### Risques - -* 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 et la seed de rejeu dans le guide JustDummies. -* Réexaminer le budget si l'usage réel révèle des épuisements indus. - -## Références - -* [Référence d'implémentation des ADR — Contrats de génération de JustDummies](../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 `JustDummies`. -* [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 deleted file mode 100644 index 9707f55e..00000000 --- a/doc/handwritten/for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md +++ /dev/null @@ -1,78 +0,0 @@ -# ADR-0013 | Gate distinct collections by cardinality, otherwise by a bounded draw - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-19 -**Accepted:** 2026-07-19 -**Decision Makers:** Reefact - -## Context - -JustDummies 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 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 - -When the domain size is known, the contradiction is certain and belongs at declaration time with the rest of JustDummies' 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 JustDummies user documentation. - -## Alternatives Considered - -### Always fail at generation - -Considered because one failure point is simpler. Rejected because it discards an exact declaration-time diagnosis for generators whose domain size is known. - -### Require every generator to expose cardinality - -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 - -Considered because a satisfiable request would eventually complete. Rejected because an unsatisfiable request could loop forever. - -## Consequences - -### Positive - -* 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 differs between known and unknown domains. -* A bounded draw can fail for a theoretically satisfiable but heavily biased generator. - -### Risks - -* 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 both failure channels and the replay seed in the JustDummies guide. -* Revisit the budget if real usage reveals false exhaustion. - -## References - -* [ADR implementation reference — JustDummies 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 `JustDummies` project. -* [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 deleted file mode 100644 index 69922da4..00000000 --- a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.fr.md +++ /dev/null @@ -1,77 +0,0 @@ -# ADR-0015 | Plafonner Any.Combine à l'arité huit - -🌍 🇬🇧 [English](0015-cap-any-combine-at-arity-eight.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-19 -**Accepté :** 2026-07-19 -**Décideurs :** Reefact - -## Contexte - -JustDummies 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` fournit des surcharges hétérogènes plates de l'arité deux à l'arité huit et s'arrête volontairement à ce seuil. - -## Justification - -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 JustDummies. - -## 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 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 - -* 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 - -* 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 - -* Rendre explicite la plage d'arités supportée dans la documentation de JustDummies. -* Étudier séparément une composition variadique homogène si un cas réel apparaît. - -## Références - -* [Référence d'implémentation des ADR — Contrats de génération de JustDummies](../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-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 deleted file mode 100644 index bee26b68..00000000 --- a/doc/handwritten/for-maintainers/adr/0015-cap-any-combine-at-arity-eight.md +++ /dev/null @@ -1,77 +0,0 @@ -# ADR-0015 | Cap Any.Combine at arity eight - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0015-cap-any-combine-at-arity-eight.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-19 -**Accepted:** 2026-07-19 -**Decision Makers:** Reefact - -## Context - -JustDummies 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` provides flat heterogeneous overloads from arity two through arity eight and deliberately stops there. - -## Rationale - -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 JustDummies API reference. - -## Alternatives Considered - -### Keep only the smallest overloads - -Considered because it minimizes API surface. Rejected because larger compositions become substantially less readable through nested lambdas or positional tuple members. - -### Use a fluent tuple-accumulating builder - -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 to the maximum arity supported by `Func` - -Considered for completeness. Rejected because the maintenance cost and normalization of extremely wide constructors outweigh the marginal convenience. - -### Accept only homogeneous generators through `params` - -Considered because it is naturally variadic. Rejected because it does not serve the differently typed constructor parameters for which `Combine` exists. - -## Consequences - -### Positive - -* 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 - -* 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 - -* 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 - -* Keep the supported arity range explicit in the JustDummies documentation. -* Consider homogeneous variadic composition separately if a real use case emerges. - -## References - -* [ADR implementation reference — JustDummies generation contracts](../specifications/adr-implementation-reference.md#dummies-generation-contracts) -* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) -* [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/0020-materialize-dummies-only-through-generate.fr.md b/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.fr.md deleted file mode 100644 index ee31002e..00000000 --- a/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.fr.md +++ /dev/null @@ -1,161 +0,0 @@ -# ADR-0020 | Matérialiser les dummies uniquement via Generate() - -🌍 🇬🇧 [English](0020-materialize-dummies-only-through-generate.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-19 -**Accepté :** 2026-07-19 -**Décideurs :** Reefact - -## Contexte - -`JustDummies` est une DSL fluide de générateurs typés porteurs de contraintes. Chaque -générateur implémente `IAny`, dont l'unique membre `Generate()` tire une valeur -satisfaisant les contraintes déclarées ; les points de composition `As` et -`Combine` construisent des générateurs plus larges et matérialisent leurs parties -en appelant `Generate()`. Le modèle affiché de la bibliothèque est qu'un générateur -est une **recette immuable, pas une valeur** : l'aléatoire n'est tiré que lorsque -`Generate()` s'exécute, et une même recette peut être générée plusieurs fois, en -produisant une valeur fraîche à chaque fois. - -Jusqu'ici, chaque générateur concret définissait aussi une **conversion -implicite** vers son type généré — 28 opérateurs au total, un par type simple et -un par type de collection (`List`, `T[]`, `HashSet`, -`Dictionary`). Cette conversion rendait une affectation à type -explicite concise, par exemple une variable locale `string` affectée directement -depuis un générateur de chaînes. - -Plusieurs faits sur cette conversion ont émergé lors d'une revue ciblée de la -bibliothèque (issue #190) : - -* La conversion a des **effets de bord** : elle tire de l'aléatoire, ce n'est donc - pas un élargissement ; elle peut **lever une exception** - (`AnyGenerationException`, `ConflictingAnyConstraintException`) sur un site qui - se lit comme une simple affectation ; et elle n'est **pas idempotente** — chaque - conversion tire une valeur fraîche, si bien que lire deux fois la « même » - variable donne deux valeurs. -* La conversion ne se déclenche que dans **une** forme syntaxique — une variable - locale ou un paramètre à type explicite. Dans les formes voisines, elle fait - silencieusement autre chose : `var` lie le générateur, `object` et - `params object[]` boxent le générateur, l'inférence générique passe le - générateur, et des surcharges concurrentes peuvent se résoudre vers le - générateur plutôt que la valeur. La suite de tests devait déjà utiliser des - locales à type explicite autour d'une API `params object[]` pour cette raison. -* `Generate()` fonctionne déjà de manière uniforme dans chacun de ces contextes, - est le membre par lequel passe l'inférence générique, et est l'opération - qu'utilisent les points de composition. C'est l'idiome dominant dans la suite de - tests. - -Deux contraintes bornent le calendrier. `JustDummies` est un package autonome -pré-1.0 dont l'API évoluera le plus dans ses premières itérations, et il n'est -référencé que par son propre projet de test (ADR-0011) ; retirer une surface -d'opérateurs publique est donc peu coûteux maintenant et deviendrait un -changement cassant une fois un `1.0` stable publié. L'issue #190 exige aussi que -le contrat de ces conversions soit décidé et consigné avant ce `1.0`. - -## Décision - -Les générateurs concrets de `JustDummies` n'exposent aucune conversion implicite vers -leur type généré : une valeur n'est matérialisée que par `Generate()`, appelé -directement ou par les points de composition `As` et `Combine` qui l'appellent en -interne. - -## Justification - -* **Une conversion implicite devrait être bon marché, totale et - référentiellement transparente ; celle-ci n'est aucune des trois.** Parce - qu'elle tire de l'aléatoire, peut lever une exception et renvoie une valeur - différente à chaque exécution, c'est un appel de méthode à effet de bord - déguisé derrière une affectation. Cela contredit directement le modèle que la - bibliothèque enseigne — un générateur est une recette, et la valeur n'est tirée - qu'à `Generate()` — en fournissant l'unique chemin qui laisse l'appelant oublier - que le tirage a lieu. -* **La commodité est une abstraction partielle et surprenante.** Elle se comporte - comme annoncé dans une seule forme syntaxique et se comporte silencieusement mal - dans les formes voisines. La garder en documentant le piège décrirait une - complexité accidentelle au lieu de la retirer ; la complexité est accidentelle - précisément parce que `Generate()` couvre déjà tous les contextes de manière - uniforme. -* **Le retrait ne coûte aucune capacité.** `Generate()` est déjà le chemin - canonique — au niveau de l'interface, cible de l'inférence générique, opération - qu'appellent les points de composition, et idiome dominant dans la suite. Ce qui - est perdu est un raccourci qui économisait un appel dans un seul contexte, pas - une quelconque expressivité. -* **C'est le moment le moins coûteux pour décider.** Le package est pré-1.0, - autonome et auto-consommé (ADR-0011), donc le changement ne touche aujourd'hui - que ses propres tests ; le même retrait après un `1.0` stable casserait chaque - consommateur qui affectait un générateur à une locale typée. L'issue #190 exige - que la décision soit consignée avant cette publication. - -## Alternatives considérées - -### Garder les conversions et documenter le contrat - -La direction que privilégie l'issue #190. Envisagée parce qu'elle préserve le site -d'appel vedette concis et indique, en documentation, où la conversion s'exécute ou -non. Rejetée parce qu'elle documente un piège au lieu d'en retirer un, et conserve -une conversion à effet de bord, non idempotente et pouvant lever une exception qui -contredit le modèle recette-contre-valeur au centre de la bibliothèque. - -### Garder les conversions et ajouter un analyzer pour les contextes trompeurs - -Envisagée parce qu'un analyzer signalant les usages `var`, `object` et par -inférence générique pourrait préserver l'ergonomie tout en attrapant les pièges. -Rejetée parce que c'est une surface large et permanente — 28 opérateurs plus un -analyzer et ses tests — pour préserver un raccourci d'un appel, et parce qu'un -contrat « convertit, sauf là où l'analyzer dit que non » est lui-même -schizophrène. Retirer les opérateurs rend l'analyzer sans objet, ce pourquoi -l'issue #190 le liste comme optionnel. - -### Retirer les conversions de certains types seulement - -Envisagée comme compromis — par exemple les garder sur les types simples immuables -et ne les retirer que des collections. Rejetée parce qu'une règle par type est -plus difficile à expliquer que l'un ou l'autre choix uniforme, et laisse quand -même la surprise de l'affectation à effet de bord sur les types qui les gardent. - -## Conséquences - -### Positives - -* Il existe une seule façon évidente et uniforme de matérialiser une valeur, et la - distinction recette-contre-valeur que la bibliothèque enseigne n'est plus - contredite par une fonctionnalité qui masque le tirage. -* Un générateur ne se substitue jamais silencieusement à sa valeur sous `var`, - `object`, `params object[]`, inférence générique ou résolution de surcharge ; - ces sites échouent désormais à la compilation au lieu de mal se comporter. - -### Négatives - -* Le site d'appel vedette est plus verbeux : une affectation à type explicite gagne - un `.Generate()`. -* Vingt-huit opérateurs, ainsi que leurs tests et exemples de documentation, sont - retirés ; la surface publique pré-1.0 change — acceptable maintenant, et la - raison pour laquelle la décision est prise avant le `1.0`. - -### Risques - -* Un utilisateur portant un modèle mental de conversion implicite pourrait au - début omettre `.Generate()`. Le risque est borné : l'omission est une erreur de - compilation au message actionnable (affecter via `IAny` ou appeler - `Generate()`), jamais une valeur fausse silencieuse. - -## Actions de suivi - -* Mettre à jour la documentation pour que `.Generate()` soit présenté comme - l'unique matérialisation — le README du package et les docs XML — fait dans le - même changement que cette décision. -* Ne pas poursuivre l'analyzer optionnel suggéré par l'issue #190 ; le retrait le - rend inutile. -* À revisiter seulement si un futur consommateur hors tests démontre un besoin - ergonomique que la forme `Generate()` ne peut satisfaire. - -## Références - -* Issue #190 — Définir et documenter le contrat des conversions implicites de - générateurs. -* ADR-0011 — Héberger JustDummies comme package autonome (churn pré-1.0, - auto-consommé). -* ADR-0006 — Fournir des valeurs de test arbitraires depuis une source unique à - graine. -* `JustDummies/IAny.cs` — le contrat `Generate()` par lequel passent ces générateurs. diff --git a/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.md b/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.md deleted file mode 100644 index 32f75ccb..00000000 --- a/doc/handwritten/for-maintainers/adr/0020-materialize-dummies-only-through-generate.md +++ /dev/null @@ -1,149 +0,0 @@ -# ADR-0020 | Materialize dummies only through Generate() - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0020-materialize-dummies-only-through-generate.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-19 -**Accepted:** 2026-07-19 -**Decision Makers:** Reefact - -## Context - -`JustDummies` is a fluent DSL of typed, constraint-carrying generators. Every -generator implements `IAny`, whose single member `Generate()` draws one value -satisfying the declared constraints; the composition seams `As` and `Combine` -build larger generators and materialize their parts by calling `Generate()`. The -library's stated model is that a generator is an **immutable recipe, not a -value**: randomness is drawn only when `Generate()` runs, and the same recipe can -be generated from several times, yielding a fresh value each time. - -Until now, each concrete generator also defined an **implicit conversion** to its -generated type — 28 operators in total, one per simple type and one per -collection type (`List`, `T[]`, `HashSet`, `Dictionary`). That -conversion let an explicitly-typed assignment read tersely, for example a -`string` local assigned directly from a string generator. - -Several facts about that conversion were surfaced during a focused review of the -library (issue #190): - -* The conversion has **side effects**: it draws randomness, so it is not a - widening; it can **throw** (`AnyGenerationException`, - `ConflictingAnyConstraintException`) at a site that reads like a plain - assignment; and it is **not idempotent** — each conversion draws a fresh value, - so reading the "same" variable twice yields two values. -* The conversion fires in only **one** syntactic shape — an explicitly-typed - local or parameter. In the adjacent shapes it silently does something else: - `var` binds the generator, `object` and `params object[]` box the generator, - generic inference passes the generator, and competing overloads can resolve to - the generator rather than the value. The test suite already had to use - explicitly-typed locals around a `params object[]` API for this reason. -* `Generate()` already works uniformly in every one of those contexts, is the - member generic inference flows through, and is the operation the composition - seams use. It is the dominant idiom across the test suite. - -Two constraints bound the timing. `JustDummies` is a pre-1.0, standalone package -whose API is expected to churn most in its early iterations, and it is referenced -only by its own test project (ADR-0011); removing a public operator surface is -therefore cheap now and a breaking change once a stable `1.0` is published. Issue -#190 also requires the contract of these conversions to be decided and recorded -before that `1.0`. - -## Decision - -Concrete `JustDummies` generators expose no implicit conversion to their generated -type: a value is materialized only by `Generate()`, called directly or by the -`As` and `Combine` composition seams that call it internally. - -## Rationale - -* **An implicit conversion should be cheap, total, and referentially - transparent; this one is none of those.** Because it draws randomness, can - throw, and returns a different value on each run, it is an effectful method - call disguised behind an assignment. That directly contradicts the model the - library teaches — a generator is a recipe, and the value is drawn only at - `Generate()` — by providing the one path that lets a caller forget the draw is - happening at all. -* **The convenience is a partial, surprising abstraction.** It behaves as - advertised in a single syntactic shape and silently misbehaves in the shapes - next to it. Keeping it and documenting the hazard would describe an accidental - complexity rather than remove it; the complexity is accidental precisely - because `Generate()` already covers every context uniformly. -* **Removal costs no capability.** `Generate()` is already the canonical path — - interface-level, the target of generic inference, the operation the - composition seams call, and the dominant idiom in the suite. What is lost is a - shorthand that saved one call in one context, not any expressiveness. -* **This is the cheapest moment to decide.** The package is pre-1.0, standalone, - and self-consumed (ADR-0011), so the change touches only its own tests today; - the same removal after a stable `1.0` would break every consumer that assigned - a generator to a typed local. Issue #190 requires the decision to be recorded - before that release. - -## Alternatives Considered - -### Keep the conversions and document the contract - -The direction issue #190 leads with. Considered because it preserves the terse -headline call site and states, in documentation, where the conversion does and -does not run. Rejected because it documents a hazard instead of removing one, and -keeps an effectful, non-idempotent, throwing conversion that contradicts the -recipe-versus-value model at the center of the library. - -### Keep the conversions and add an analyzer for the misleading contexts - -Considered because an analyzer flagging `var`, `object`, and generic-inference -uses could preserve the ergonomics while catching the traps. Rejected because it -is a large, permanent surface — 28 operators plus an analyzer and its tests — to -preserve a one-call shorthand, and a "converts, except where the analyzer says it -does not" contract is itself split-brained. Removing the operators makes the -analyzer moot, which is why issue #190 lists it as optional. - -### Remove the conversions only from some types - -Considered as a compromise — for example keeping them on immutable simple types -and dropping them only on collections. Rejected because a per-type rule is harder -to explain than either uniform choice and still leaves the effectful-assignment -surprise on the types that keep it. - -## Consequences - -### Positive - -* There is one obvious, uniform way to materialize a value, and the - recipe-versus-value distinction the library teaches is no longer contradicted - by a feature that hides the draw. -* A generator never silently stands in for its value under `var`, `object`, - `params object[]`, generic inference, or overload resolution; those sites now - fail to compile instead of misbehaving. - -### Negative - -* The headline call site is more verbose: an explicitly-typed assignment gains a - `.Generate()`. -* Twenty-eight operators, along with their tests and documentation examples, are - removed; the pre-1.0 public surface changes — acceptable now, and the reason - the decision is taken before `1.0`. - -### Risks - -* A user carrying an implicit-conversion mental model may at first omit - `.Generate()`. The risk is bounded: the omission is a compile-time error with - an actionable message (assign through `IAny` or call `Generate()`), never a - silent wrong value. - -## Follow-up Actions - -* Update the documentation so `.Generate()` is presented as the sole - materialization — the package README and the XML docs — done in the same - change as this decision. -* Do not pursue the optional analyzer suggested in issue #190; the removal makes - it unnecessary. -* Revisit only if a future, non-test consumer demonstrates an ergonomic need the - `Generate()` form cannot meet. - -## References - -* Issue #190 — Define and document the contract of implicit generator - conversions. -* ADR-0011 — Host JustDummies as a standalone package (pre-1.0 churn, self-consumed). -* ADR-0006 — Supply arbitrary test values from a single seedable source. -* `JustDummies/IAny.cs` — the `Generate()` contract these generators flow through. 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 deleted file mode 100644 index ffc819be..00000000 --- a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.fr.md +++ /dev/null @@ -1,80 +0,0 @@ -# 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) - -**Statut :** Accepté -**Proposé :** 2026-07-19 -**Accepté :** 2026-07-19 -**Décideurs :** Reefact - -## Contexte - -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 plancher .NET Framework pris en charge pour les bibliothèques `netstandard2.0` livrées est **4.7.2**. - -## Justification - -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. - -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. - -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. - -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. - -## Alternatives envisagées - -### Continuer à annoncer .NET Framework 4.6.1 - -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. - -### Fixer le plancher à .NET Framework 4.6.2 - -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. - -### Tester chaque version majeure moderne de .NET dans une matrice bloquante - -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 - -* 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 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 - -* 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 - -* 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 - -* [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-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 deleted file mode 100644 index 7a3aeff5..00000000 --- a/doc/handwritten/for-maintainers/adr/0022-floor-the-library-on-net-framework-4-7-2.md +++ /dev/null @@ -1,80 +0,0 @@ -# ADR-0022 | Floor the library's .NET Framework support at 4.7.2 - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0022-floor-the-library-on-net-framework-4-7-2.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-19 -**Accepted:** 2026-07-19 -**Decision Makers:** Reefact - -## Context - -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 shipped `netstandard2.0` libraries is **4.7.2**. - -## Rationale - -4.7.2 is the lowest version on which the libraries can be consumed without the fragile compatibility plumbing required by earlier framework versions. - -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. - -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. - -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. - -## Alternatives Considered - -### Keep advertising .NET Framework 4.6.1 - -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. - -### Floor at .NET Framework 4.6.2 - -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. - -### Test every modern .NET major as a blocking matrix - -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 .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 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 - -* 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 - -* Keep the user-facing support statement at .NET Framework 4.7.2 or later. -* Keep the framework-floor check required for merges. - -## References - -* [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-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-keep-expression-tree-selectors-for-the-v1-binder-api.fr.md b/doc/handwritten/for-maintainers/adr/0023-keep-expression-tree-selectors-for-the-v1-binder-api.fr.md index 395be0ff..183a0a19 100644 --- a/doc/handwritten/for-maintainers/adr/0023-keep-expression-tree-selectors-for-the-v1-binder-api.fr.md +++ b/doc/handwritten/for-maintainers/adr/0023-keep-expression-tree-selectors-for-the-v1-binder-api.fr.md @@ -151,6 +151,6 @@ venait à compter. * [ADR-0021](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.fr.md) — l'entrée hors-DTO, dont le chemin par nom est la forme non-expression existante du binder. -* [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.fr.md) — le +* [just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-floor-the-library-on-net-framework-4-7-2.md) — le floor .NET Framework 4.7.2 contre lequel le cache de getters compilés a été vérifié. diff --git a/doc/handwritten/for-maintainers/adr/0023-keep-expression-tree-selectors-for-the-v1-binder-api.md b/doc/handwritten/for-maintainers/adr/0023-keep-expression-tree-selectors-for-the-v1-binder-api.md index 080c1661..3cbfe03e 100644 --- a/doc/handwritten/for-maintainers/adr/0023-keep-expression-tree-selectors-for-the-v1-binder-api.md +++ b/doc/handwritten/for-maintainers/adr/0023-keep-expression-tree-selectors-for-the-v1-binder-api.md @@ -135,5 +135,5 @@ the natural successor if the residual cost ever matters. * [ADR-0021](0021-bind-out-of-dto-arguments-as-peers-through-a-source-agnostic-entry.md) — the out-of-DTO entry, whose name-based path is the binder's existing non-expression shape. -* [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) — the .NET +* [just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-floor-the-library-on-net-framework-4-7-2.md) — the .NET Framework 4.7.2 floor the compiled-getter cache was verified against. diff --git a/doc/handwritten/for-maintainers/adr/0025-generate-strings-from-a-home-grown-regular-subset.fr.md b/doc/handwritten/for-maintainers/adr/0025-generate-strings-from-a-home-grown-regular-subset.fr.md deleted file mode 100644 index 0a11522b..00000000 --- a/doc/handwritten/for-maintainers/adr/0025-generate-strings-from-a-home-grown-regular-subset.fr.md +++ /dev/null @@ -1,123 +0,0 @@ -# ADR-0025 | Générer les chaînes qui matchent depuis un sous-ensemble régulier maison - -🌍 🇬🇧 [English](0025-generate-strings-from-a-home-grown-regular-subset.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -`JustDummies` permet à un test de fournir des valeurs arbitraires mais valides. Une règle de validité très courante est -une **expression régulière** de format : un objet-valeur valide son entrée contre un motif (une référence de -commande, un SKU, un code devise), et un test a besoin d'une valeur qui passe cette validation sans réécrire le -format à la main. `Any.StringMatching(motif)` répond à ce besoin — générer une chaîne que le motif matche. - -Trois faits cadrent sa construction : - -* La bibliothèque est livrée **sans aucune dépendance runtime** et en fait un élément de son identité ; la - frontière est vérifiée par un test d'architecture (ADR-0011). Ajouter une référence de paquet est donc un choix - délibéré et visible, pas un détail. -* Générer une chaîne depuis un motif revient à parcourir le motif comme un automate fini. Une bibliothèque .NET - existe (Fare, le port xeger/brics). C'est une dépendance, peu maintenue, et — comme tout générateur à base - d'automate — elle ne peut honorer les constructs **non réguliers** (lookaround, backreferences) ; elle tend à les - ignorer ou les mal traiter silencieusement. -* Les constructs non réguliers ne sont pas un sous-ensemble qu'on choisit d'écarter par confort : un lookahead, une - backreference, une limite de mot ne sont **pas réguliers**, donc aucun générateur fini ne peut produire de chaînes - les honorant. Ils sont absents de toute approche à base d'automate, maison ou non. - -Le générateur est terminal : le motif est toute la spécification, il n'y a donc rien à réconcilier avec les autres -contraintes de chaîne, et les seules questions de forme ouvertes sont l'univers de caractères et jusqu'où étendre un -quantifieur non borné. - -## Décision - -`Any.StringMatching` analyse le **sous-ensemble régulier** du langage de motifs avec le parseur propre à la -bibliothèque et génère à partir de lui — en refusant par une `UnsupportedRegexException` first-class un construct -bien formé mais non régulier ou hors périmètre — plutôt que de dépendre d'une bibliothèque d'automates de regex. - -## Justification - -* **Elle préserve l'identité zéro-dépendance.** Une dépendance d'automate de regex serait la première dépendance - runtime de la bibliothèque, apparaîtrait dans l'arbre et le SBOM de chaque consommateur, et contredirait une - propriété que la bibliothèque annonce et garde. Le parseur maison couvre les formats qui comptent sans ce coût. -* **Elle rend la frontière honnête et first-class.** Les constructs écartés sont les non-réguliers qu'un générateur - ne peut de toute façon pas honorer ; les refuser à la déclaration, en nommant le construct, est la signature de la - bibliothèque — une erreur claire vaut mieux qu'une dépendance qui émet en silence une valeur qui ne matche pas - réellement. -* **Le sous-ensemble régulier est toute la surface utile pour la validation de format.** Littéraux, classes, - raccourcis courants, quantifieurs, alternation, groupes et ancres expriment les formats que les objets-valeurs - valident réellement ; les constructs exclus servent à l'analyse, pas aux formats à forme fixe que vise cette - fonctionnalité. -* **Les choix de forme restants suivent le reste de la bibliothèque.** Là où le motif laisse un caractère libre — un - raccourci, le point, une classe négative —, les terminaux puisent dans l'ASCII imprimable pour qu'un dummy reste - lisible et que chaque caractère émis soit un vrai membre de sa classe, `\s` puisant dans une paire lisible qui - inclut la tabulation ; un caractère que le motif nomme explicitement est émis tel quel, caractères de contrôle - compris. Un quantifieur non borné tire son minimum plus un petit intervalle borné, le même défaut - « 0 à une poignée » qu'utilisent déjà les générateurs de chaînes et de collections. - -## Alternatives considérées - -### Dépendre de Fare (ou d'une autre bibliothèque d'automates de regex) - -Considérée parce qu'elle est éprouvée, large, et livrerait la fonctionnalité plus vite. Rejetée parce qu'elle -introduit la première dépendance runtime de la bibliothèque — contredisant l'identité zéro-dépendance que garde le -test d'architecture — pour un dialecte qui n'est lui-même que le sous-ensemble régulier, et parce qu'elle renonce au -refus first-class des constructs non supportés en les traitant silencieusement. Le niveau de maintenance de la -bibliothèque est une préoccupation secondaire. - -### Maison, mais visant le dialecte .NET complet - -Considérée par souci d'exhaustivité. Rejetée parce que les constructs exclus (lookaround, backreferences) ne sont -pas réguliers et ne peuvent être générés par aucun moyen fini : le « dialecte complet » est donc inatteignable par -principe ; poursuivre les catégories Unicode et le reste serait un travail sans fin, pour des constructs hors de la -validation de format. - -### Garder le générateur chaînable avec les autres contraintes de chaîne - -Considérée pour qu'un motif puisse se combiner avec `WithLength`, `Numeric`, etc. Rejetée parce que le motif est -déjà toute la spécification : la longueur et la forme des caractères s'expriment dedans. Un générateur terminal -supprime d'emblée une classe de combinaisons contradictoires et garde la surface petite, tandis que la composition -via `As`, `OrNull`, `Combine` et les générateurs de collections — tous définis sur `IAny` — reste disponible. - -## Conséquences - -### Positives - -* La regex de format d'un objet-valeur devient une source de dummies valides en une ligne, sans nouvelle - dépendance. -* Un construct non supporté échoue à la déclaration avec un message le nommant, jamais sous forme d'une valeur qui - ne matche pas en silence. -* Le générateur se compose comme tous les autres et reste reproductible sous une graine. - -### Négatives - -* La bibliothèque porte et doit maintenir son propre parseur et générateur de regex — le plus gros bloc de logique - qu'elle contienne — et sa correction repose sur la suite de tests (un property test vérifie les valeurs générées - contre le vrai moteur .NET). -* Le dialecte supporté est un **contrat** : l'élargir ou le restreindre plus tard est un changement pertinent pour - la compatibilité, et l'univers de caractères comme l'intervalle du quantifieur non borné sont des comportements - sur lesquels des consommateurs peuvent finir par compter. - -### Risques - -* **Dérive du dialecte** — un motif que l'utilisateur attend voir fonctionner peut tomber hors du sous-ensemble. - Atténué par l'erreur first-class explicite, qui nomme le construct au lieu de mal générer. -* **Bugs du parseur** — un parseur écrit à la main peut mal gérer un cas limite. Atténué par le property test contre - le vrai moteur ; une défaillance apparaît comme une valeur que le moteur .NET rejette, attrapée en CI plutôt que - dans le test d'un consommateur. - -## Actions de suivi - -* N'élargir le sous-ensemble supporté qu'en réponse à des motifs réels, en gardant le refus first-class comme filet - de sécurité. -* Documenter le dialecte supporté dans la documentation utilisateur une fois la surface stabilisée. -* Si une génération adossée à un automate et réconciliant la longueur devient nécessaire, réexaminer — l'API - terminale laisse cette voie ouverte sans casser les appelants. - -## Références - -* ADR-0011 — Héberger JustDummies comme un paquet autonome dans ce dépôt (la frontière zéro-dépendance). -* Le parseur de regex, l'arbre de nœuds et le générateur, ainsi que le property test contre - `System.Text.RegularExpressions`, dans le projet `JustDummies` et ses tests. diff --git a/doc/handwritten/for-maintainers/adr/0025-generate-strings-from-a-home-grown-regular-subset.md b/doc/handwritten/for-maintainers/adr/0025-generate-strings-from-a-home-grown-regular-subset.md deleted file mode 100644 index 02051f06..00000000 --- a/doc/handwritten/for-maintainers/adr/0025-generate-strings-from-a-home-grown-regular-subset.md +++ /dev/null @@ -1,115 +0,0 @@ -# ADR-0025 | Generate matching strings from a home-grown regular subset - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0025-generate-strings-from-a-home-grown-regular-subset.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -`JustDummies` lets a test supply arbitrary yet valid values. A very common validity rule is a format -**regular expression**: a value object validates its input against a pattern (an order reference, a SKU, a -currency code), and a test needs a value that passes that validation without duplicating the format by hand. -`Any.StringMatching(pattern)` fills that need — generate a string the pattern matches. - -Three facts frame how it is built: - -* The library ships with **zero runtime dependencies** and treats that as part of its identity; the boundary is - machine-checked by an architecture test (ADR-0011). Adding a package reference is therefore a deliberate, - visible change, not a detail. -* Generating a string from a pattern means walking the pattern as a finite automaton. A .NET library exists for - this (Fare, the xeger/brics port). It is a dependency, is lightly maintained, and — like every automaton-based - generator — cannot honour **non-regular** constructs (lookaround, backreferences); it tends to drop or mishandle - them silently. -* Non-regular constructs are not a subset the library is choosing to skip for convenience: a lookahead, a - backreference, a word boundary are **not regular**, so no finite generator can produce strings honouring them. - They are absent from any automaton-based approach, home-grown or not. - -The generator is a terminal: the pattern is the whole specification, so there is nothing to reconcile with the -other string constraints, and the only open shaping questions are the character universe and how far to expand an -unbounded quantifier. - -## Decision - -`Any.StringMatching` parses the **regular subset** of the pattern language with the library's own parser and -generates from it — refusing a well-formed but non-regular or out-of-scope construct with a first-class -`UnsupportedRegexException` — rather than taking a dependency on a regex-automaton library. - -## Rationale - -* **It keeps the zero-dependency identity intact.** A regex-automaton dependency would be the library's first - runtime dependency, would show in every consumer's tree and SBOM, and would contradict a property the library - advertises and guards. The home-grown parser covers the formats that matter without that cost. -* **It makes the boundary honest and first-class.** The constructs left out are the non-regular ones a generator - fundamentally cannot honour anyway; refusing them at declaration time, naming the construct, is the library's - signature — a clear error beats a dependency that silently emits a value which does not actually match. -* **The regular subset is the whole useful surface for format validation.** Literals, classes, the common - shorthands, quantifiers, alternation, grouping and anchors express the formats value objects actually validate; - the excluded constructs are used for parsing, not for the fixed-shape formats this feature targets. -* **The remaining shaping choices follow the rest of the library.** Where the pattern leaves a character free — a - shorthand, the dot, a negated class — terminals draw from printable ASCII so a dummy stays legible and every - emitted character is a genuine class member, `\s` drawing from a readable pair that includes a tab; a character - the pattern names explicitly is emitted as written, control characters included. An unbounded quantifier draws - its minimum plus a small bounded spread, the same "0 to a handful" default the string and collection generators - already use. - -## Alternatives Considered - -### Depend on Fare (or another regex-automaton library) - -Considered because it is battle-tested, broad, and would ship the feature faster. Rejected because it introduces -the library's first runtime dependency — contradicting the zero-dependency identity the architecture test guards — -for a dialect that is itself only the regular subset, and because it forfeits the first-class rejection of -unsupported constructs by handling them silently. The maintenance status of the library is a secondary concern. - -### Home-grown, but targeting the full .NET regex dialect - -Considered for completeness. Rejected because the excluded constructs (lookaround, backreferences) are not regular -and cannot be generated by any finite means, so "full dialect" is unreachable in principle; chasing Unicode -categories and the rest would be unbounded work for constructs outside format validation. - -### Keep the generator chainable with the other string constraints - -Considered so a pattern could combine with `WithLength`, `Numeric`, and the like. Rejected because the pattern is -already the whole specification: length and character shape belong inside it. A terminal generator removes a class -of contradictory combinations entirely and keeps the surface small, while composition through `As`, `OrNull`, -`Combine` and the collection generators — all defined over `IAny` — stays available. - -## Consequences - -### Positive - -* A value object's format regex becomes a one-line source of valid dummies, with no new dependency. -* An unsupported construct fails at declaration time with a message naming it, never as a silently non-matching - value. -* The generator composes like every other one and stays reproducible under a seed. - -### Negative - -* The library carries and must maintain its own regex parser and generator — the largest single piece of logic in - it — and its correctness rests on the test suite (a property test checks generated values against the real .NET - engine). -* The supported dialect is a **contract**: widening or narrowing it later is a compatibility-relevant change, and - the character universe and the unbounded-quantifier spread are behaviours consumers may come to rely on. - -### Risks - -* **Dialect drift** — a pattern a user expects to work may fall outside the subset. Mitigated by the explicit - first-class error, which names the construct rather than mis-generating. -* **Parser bugs** — a hand-written parser can mis-handle an edge case. Mitigated by the real-engine property test; - a failure surfaces as a value the .NET engine rejects, caught in CI rather than in a consumer's test. - -## Follow-up Actions - -* Grow the supported subset only in response to real patterns, keeping the first-class rejection as the safety net. -* Document the supported dialect in the user documentation once the surface stabilizes. -* Should an automaton-backed, length-reconciling generation become necessary, revisit — the terminal API leaves - that path open without breaking callers. - -## References - -* ADR-0011 — Host JustDummies as a standalone package in this repository (the zero-dependency boundary). -* The regex parser, node tree and generator, and the property test against `System.Text.RegularExpressions`, in - the `JustDummies` project and its tests. diff --git a/doc/handwritten/for-maintainers/adr/0030-draw-arbitrary-strings-from-an-explicit-terminal-set.fr.md b/doc/handwritten/for-maintainers/adr/0030-draw-arbitrary-strings-from-an-explicit-terminal-set.fr.md deleted file mode 100644 index f99cec51..00000000 --- a/doc/handwritten/for-maintainers/adr/0030-draw-arbitrary-strings-from-an-explicit-terminal-set.fr.md +++ /dev/null @@ -1,141 +0,0 @@ -# ADR-0030 | Tirer des chaînes arbitraires depuis un ensemble de valeurs explicite et terminal - -🌍 🇬🇧 [English](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Superseded par l'[ADR-0054](0054-decide-a-constraint-surface-by-constructive-versus-rejective.fr.md) -**Proposé :** 2026-07-21 -**Accepté :** 2026-07-21 -**Décideurs :** Reefact - -## Contexte - -`JustDummies` fournit des valeurs arbitraires mais valides, avec des contraintes qui expriment ce que le code environnant -exige d'une valeur. Un besoin récurrent est une valeur dont le domaine est une **liste fixe et fermée** que le test -n'assère pas — un code devise tiré d'une petite table, un libellé de statut, le nom d'une entreprise connue. La -bibliothèque génère des formes **structurelles** (une longueur, une famille de caractères, un motif régulier) ; elle -ne sait pas synthétiser un tel ensemble du monde réel, et c'est l'appelant qui détient les valeurs. - -Plusieurs faits établis cadrent le choix : - -* Les générateurs de scalaires et d'enums exposent déjà `OneOf(params T[])` — tirer uniformément dans une liste - explicite — mais il s'agit là d'une contrainte **composable** : elle restreint *au sein* de l'intervalle ou du pool - du type et se cross-valide avec les autres contraintes. `AnyString` n'a aucun `OneOf`. -* `Any.StringMatching` (ADR-0025) est un générateur **terminal** : le motif est toute la spécification, il n'expose - donc aucune contrainte de forme ou de longueur supplémentaire, tout en se composant via `As`, `OrNull`, `Combine` - et les générateurs de collections comme n'importe quel `IAny`. -* La surface de mise en forme d'une chaîne est bien plus large que l'intervalle d'un scalaire : préfixe, suffixe, - fragments contenus, famille de caractères, casse et longueur, chacun déjà cross-validé avec les autres. -* Les collections distinctes bornent à la déclaration selon la cardinalité annoncée par le générateur d'éléments - (ADR-0013), via l'interface interne `ICardinalityHint` ; un générateur qui n'en annonce pas retombe sur un - tirage dédupliquant borné. -* La bibliothèque puise dans une source unique seedable pour que tout run soit reproductible (ADR-0006), construit - les valeurs pour satisfaire les contraintes plutôt que de générer-puis-filtrer, et est livrée **sans aucune - dépendance runtime ni jeu de données** — son README liste « pas de fausses données réalistes (noms, e-mails, - adresses) » comme non-objectif explicite (ADR-0011). -* La forme d'appel demandée est `Any.String().OneOf(...)` — chaînée depuis le point d'entrée des chaînes. - -## Décision - -`Any.String().OneOf(...)` tire la chaîne dans un ensemble de valeurs explicite fourni par l'appelant, sous la forme -d'un générateur **terminal** — l'ensemble est toute la spécification et ne se combine pas avec les autres contraintes -de chaîne — plutôt que comme une contrainte composable à la manière du `OneOf` des générateurs de scalaires. - -## Justification - -* **Un ensemble terminal garde la surface petite et sans contradiction.** Réconcilier un ensemble de valeurs - explicite avec le préfixe, le suffixe, les fragments, la famille de caractères, la casse et la longueur d'une chaîne - multiplierait les combinaisons contradictoires et leurs messages de conflit, pour une combinaison dont personne n'a - besoin — un appelant qui fournit des valeurs littérales en fixe déjà la forme. Faire de l'ensemble toute la - spécification supprime cette classe entière d'un coup. `Any.StringMatching` a tranché de même, pour la même raison - (ADR-0025) ; s'aligner sur ce précédent garde les deux terminaux de chaîne cohérents. -* **Il reste sur `Any.String()` pour la découvrabilité, et reste honnête par l'échec précoce.** Un appelant part de - `Any.String()` et trouve `OneOf` à côté des autres façons d'obtenir une chaîne. La nature terminale est garantie de - deux façons : le générateur renvoyé ne porte aucune méthode de mise en forme, et déclarer `OneOf` après une autre - contrainte lève une `ConflictingAnyConstraintException` à la déclaration — la même règle « un Arrange impossible est - un défaut du test » que la bibliothèque applique à tout autre conflit. -* **Des valeurs fournies par l'appelant préservent l'identité de la bibliothèque.** Le contenu réaliste vit dans le - test du consommateur, pas dans le paquet : le non-objectif « pas de fausses données réalistes » tient, et aucun jeu - de données, dépendance ou appel réseau n'est introduit. `OneOf` est la réponse sans dépendance et déterministe à - « donne-moi une valeur plausible tirée d'un ensemble connu ». -* **Annoncer la cardinalité garde les collections distinctes précoces.** Un ensemble explicite est un petit domaine - dénombrable, donc le générateur implémente `ICardinalityHint` ; une collection distincte le borne à la - déclaration (ADR-0013), exactement comme sur `AnyChar` ou `AnyEnum`, au lieu de compter silencieusement sur le repli - par tirage dédupliquant borné. -* **La reproductibilité est préservée.** La valeur est un tirage uniforme dans l'ensemble dédupliqué, via la même - source seedable que tout autre générateur : un run se rejoue sous une graine (ADR-0006) ; dédupliquer empêche - qu'une valeur listée soit implicitement surpondérée. - -## Alternatives considérées - -### Un `OneOf` composable sur `AnyString`, comme les générateurs de scalaires - -Considérée par symétrie de surface avec `AnyInt32.OneOf` et ses pairs. Rejetée parce que les contraintes de mise en -forme d'une chaîne croisent un ensemble de valeurs explicite de multiples façons, chacune nécessitant sa propre -analyse de conflit précoce et son message, pour une combinaison dont un appelant fournissant des littéraux n'a jamais -besoin — la forme terminale supprime la classe entière, cohérente avec ADR-0025. - -### Une factory statique `Any.StringOneOf(...)` (ou `Any.OneOf(...)`), parallèle à `Any.StringMatching` - -Considérée parce qu'une factory statique est terminale dès le premier appel et esquive tout cas « une contrainte est -déjà déclarée ». Rejetée parce que la surface demandée et plus découvrable est `Any.String().OneOf(...)`, qui garde -les points d'entrée des chaînes ensemble ; le cas de la contrainte préalable est couvert par un conflit clair à la -déclaration, le mécanisme que la bibliothèque emploie déjà pour toute combinaison impossible. - -### Livrer des jeux de données réalistes curés (`Any.CompanyName()`, `Any.FirstName()`, ...) - -Considérée parce qu'elle répond directement à « donne-moi une valeur plausible ». Rejetée parce qu'elle contredit le -non-objectif affiché de ne livrer aucune fausse donnée réaliste, et ferait porter à la bibliothèque un jeu de données -ouvert qu'elle devrait maintenir, faire grossir et localiser ; le consommateur fournit l'ensemble et `OneOf` y tire à -la place. - -### Générer l'ensemble au premier run via un service externe et le mettre en cache - -Considérée comme un moyen de composer l'ensemble sans l'écrire à la main. Rejetée parce qu'elle ajouterait une -dépendance runtime et un premier run non déterministe et non hermétique à une bibliothèque dont l'identité est une -génération déterministe sans dépendance (ADR-0006, ADR-0011) ; composer l'ensemble est une préoccupation de temps de -conception, qui a sa place hors de la bibliothèque. - -## Conséquences - -### Positives - -* Une valeur dont le domaine est une liste courte et fermée devient un dummy d'une ligne, sans dépendance et - reproductible, qui se compose en objets-valeurs (`As`), en optionnels (`OrNull`) et en collections comme tout autre - générateur. -* La forme terminale garde la surface des chaînes petite et exempte d'une nouvelle classe de combinaisons de - contraintes contradictoires. -* Une collection distincte sur l'ensemble est bornée précocement par sa cardinalité, cohérente avec les autres - générateurs à domaine dénombrable. - -### Négatives - -* Un nouveau type public (`AnyStringOneOf`) et une méthode à maintenir et documenter, et une seconde forme de `OneOf` - dans la bibliothèque — terminale pour les chaînes, composable pour les scalaires — que la documentation doit - expliquer. -* La bibliothèque ne vérifie pas que les valeurs fournies respectent un format externe : c'est le contenu de - l'appelant, et un objet-valeur a toujours besoin de `As(...)` pour imposer son invariant. - -### Risques - -* Un appelant peut attendre la composabilité du `OneOf` scalaire et être surpris que celui des chaînes soit terminal. - Atténué par le type renvoyé qui ne porte aucune méthode de mise en forme et par le conflit à la déclaration lors - qu'une contrainte le précède — les deux rendent la nature terminale explicite au point d'appel. - -## Actions de suivi - -* Documenter le générateur dans le README du paquet `JustDummies` (fait) et dans la documentation utilisateur lors de la - prochaine révision de la surface des chaînes. -* Garder exact le non-objectif « pas de fausses données réalistes » du README : `OneOf` tire dans des valeurs - fournies par l'appelant et ne livre aucun jeu de données. - -## Références - -* ADR-0025 — Générer les chaînes qui matchent depuis un sous-ensemble régulier maison (le précédent du générateur - terminal). -* ADR-0013 — Borner les collections distinctes par la cardinalité, sinon par un tirage borné (le contrat - `ICardinalityHint`). -* ADR-0006 — Fournir des valeurs de test arbitraires depuis une source unique seedable (la reproductibilité). -* ADR-0011 — Héberger JustDummies comme un paquet autonome dans ce dépôt (la frontière zéro-dépendance, sans jeu de - données). -* Le type `AnyStringOneOf`, la méthode `AnyString.OneOf` et leurs tests dans le projet `JustDummies` et - `JustDummies.UnitTests`. diff --git a/doc/handwritten/for-maintainers/adr/0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md b/doc/handwritten/for-maintainers/adr/0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md deleted file mode 100644 index 130cc07a..00000000 --- a/doc/handwritten/for-maintainers/adr/0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md +++ /dev/null @@ -1,130 +0,0 @@ -# ADR-0030 | Draw arbitrary strings from an explicit, terminal value set - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.fr.md) - -**Status:** Superseded by [ADR-0054](0054-decide-a-constraint-surface-by-constructive-versus-rejective.md) -**Proposed:** 2026-07-21 -**Accepted:** 2026-07-21 -**Decision Makers:** Reefact - -## Context - -`JustDummies` supplies arbitrary yet valid values, with constraints that express what the surrounding code requires of -a value. A recurring need is a value whose domain is a **fixed, closed list** the test does not assert on — a -currency code drawn from a short table, a status label, a well-known company name. The library generates -**structural** shapes (a length, a character family, a regular pattern); it cannot synthesize such a real-world set, -and the caller holds the values. - -Several existing facts frame the choice: - -* The scalar and enum generators already expose `OneOf(params T[])` — draw uniformly from an explicit allow-list — - but there it is a **composable** constraint: it narrows *within* the type's interval or pool and cross-validates - against the other constraints. `AnyString` has no `OneOf` at all. -* `Any.StringMatching` (ADR-0025) is a **terminal** generator: the pattern is the whole specification, so it exposes - no further shape or length constraints, yet it still composes through `As`, `OrNull`, `Combine` and the collection - generators as any `IAny` does. -* A string's shaping surface is far wider than a scalar's interval: prefix, suffix, contained fragments, character - family, letter casing and length, each already cross-validated against the others. -* Distinct collections gate on an element generator's advertised cardinality at declaration time (ADR-0013), through - the internal `ICardinalityHint`; a generator that does not advertise one falls back to a bounded dedup draw. -* The library draws from a single seedable source so any run is reproducible (ADR-0006), builds values to satisfy - the constraints rather than generate-and-filter, and ships with **zero runtime dependencies and no datasets** — - its README lists "no realistic fake data (names, emails, addresses)" as an explicit non-goal (ADR-0011). -* The requested call shape is `Any.String().OneOf(...)` — chained off the string entry point. - -## Decision - -`Any.String().OneOf(...)` draws the string from an explicit set of caller-supplied values as a **terminal** -generator — the set is the whole specification and does not combine with the other string constraints — rather than -as a composable constraint like the scalar generators' `OneOf`. - -## Rationale - -* **A terminal set keeps the surface small and contradiction-free.** Reconciling an explicit value set with a - string's prefix, suffix, fragments, character family, casing and length would multiply contradictory combinations - and their conflict messages, for a combination nobody needs — a caller who supplies literal values already fixes - their shape. Making the set the whole specification removes that whole class at once. `Any.StringMatching` reached - the same conclusion for the same reason (ADR-0025); matching that precedent keeps the two string terminals - coherent. -* **It stays on `Any.String()` for discoverability, and stays honest through fail-fast.** A caller reaches for - `Any.String()` and finds `OneOf` beside the other ways to obtain a string. The terminal nature is enforced two - ways: the returned generator carries no shaping methods, and declaring `OneOf` after another constraint raises a - `ConflictingAnyConstraintException` at declaration time — the same "an impossible Arrange is a test defect" rule - the library applies to every other conflict. -* **Caller-supplied values preserve the library's identity.** The realistic content lives in the consumer's test, - not in the package, so the "no realistic fake data" non-goal holds and no dataset, dependency, or network call is - introduced. `OneOf` is the dependency-free, deterministic answer to "give me a plausible value from a known set". -* **Advertising cardinality keeps distinct collections eager.** An explicit set is a small countable domain, so the - generator implements `ICardinalityHint`; a distinct collection over it gates eagerly (ADR-0013), exactly - as it does over `AnyChar` or `AnyEnum`, instead of silently relying on the bounded dedup-draw fallback. -* **Reproducibility is preserved.** The value is a uniform pick from the deduplicated set through the same seedable - source as every other generator, so a run replays under a seed (ADR-0006); collapsing duplicates keeps a listed - value from being implicitly weighted. - -## Alternatives Considered - -### A composable `OneOf` on `AnyString`, like the scalar generators - -Considered for surface symmetry with `AnyInt32.OneOf` and its peers. Rejected because a string's shaping constraints -intersect an explicit value set in many ways, each needing its own eager conflict analysis and message, for a -combination a caller supplying literals never needs — the terminal form removes the whole class, consistent with -ADR-0025. - -### A static factory `Any.StringOneOf(...)` (or `Any.OneOf(...)`), parallel to `Any.StringMatching` - -Considered because a static factory is terminal from the first call and sidesteps any "a constraint is already -declared" case. Rejected because the requested and more discoverable surface is `Any.String().OneOf(...)`, which -keeps the string entry points together; the prior-constraint case is covered by a clear declaration-time conflict, -the mechanism the library already uses for every impossible combination. - -### Ship curated realistic datasets (`Any.CompanyName()`, `Any.FirstName()`, ...) - -Considered because it answers "give me a plausible value" directly. Rejected because it contradicts the stated -non-goal of shipping no realistic fake data, and would make the library own, grow and localize an open-ended dataset; -the consumer supplies the set and `OneOf` draws from it instead. - -### Generate the set on first run through an external service and cache it - -Considered as a way to author the set without hand-writing it. Rejected because it would add a runtime dependency and -a non-deterministic, non-hermetic first run to a library whose identity is zero-dependency, deterministic generation -(ADR-0006, ADR-0011); authoring the set is a design-time concern that belongs outside the library. - -## Consequences - -### Positive - -* A value whose domain is a short, closed list becomes a one-line, dependency-free, reproducible dummy that composes - into value objects (`As`), optionals (`OrNull`) and collections like every other generator. -* The terminal shape keeps the string surface small and free of a new class of contradictory constraint - combinations. -* A distinct collection over the set is gated eagerly by its cardinality, consistent with the other countable-domain - generators. - -### Negative - -* A new public type (`AnyStringOneOf`) and method to maintain and document, and a second `OneOf` shape in the library - — terminal for strings, composable for scalars — that the documentation must explain. -* The library does not check that the supplied values meet any external format: they are the caller's content, and a - value object still needs `As(...)` to enforce its invariant. - -### Risks - -* A caller may expect the scalar `OneOf`'s composability and be surprised the string one is terminal. Mitigated by - the returned type carrying no shaping methods and by the declaration-time conflict when a constraint precedes it — - both make the terminal nature explicit at the call site. - -## Follow-up Actions - -* Document the generator in the `JustDummies` package README (done) and in the user documentation when the string surface - is next revised. -* Keep the "no realistic fake data" non-goal in the README accurate: `OneOf` draws from caller-supplied values and - ships no dataset. - -## References - -* ADR-0025 — Generate matching strings from a home-grown regular subset (the terminal-generator precedent). -* ADR-0013 — Gate distinct collections by cardinality, otherwise by a bounded draw (the `ICardinalityHint` contract). -* ADR-0006 — Supply arbitrary test values from a single seedable source (reproducibility). -* ADR-0011 — Host JustDummies as a standalone package in this repository (the zero-dependency, no-dataset boundary). -* The `AnyStringOneOf` type, the `AnyString.OneOf` method, and their tests in the `JustDummies` project and - `JustDummies.UnitTests`. diff --git a/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.fr.md b/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.fr.md deleted file mode 100644 index 68a7b167..00000000 --- a/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.fr.md +++ /dev/null @@ -1,141 +0,0 @@ -# ADR-0031 | Nommer les fabriques scalaires de Any d'après leur type CLR - -🌍 🇬🇧 [English](0031-name-any-factories-after-their-clr-type.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-21 -**Accepté :** 2026-07-21 -**Décideurs :** Reefact - -## Contexte - -* `JustDummies` expose un point d'entrée statique `Any` dont les fabriques sans paramètre démarrent - chacune un générateur pour un type simple .NET — `Any.Int32()`, `Any.SByte()`, - `Any.Single()`, `Any.UInt64()`, `Any.String()`, `Any.Guid()`, `Any.DateTime()`, … — et - retournent chacune un type builder nommé `Any{Nom}` (`Any.Int32()` retourne `AnyInt32`). - `AnyContext` reflète chacune de ces fabriques scalaires pour la surface à contexte graine, si - bien que chaque fabrique existe en deux endroits qui doivent concorder. -* Sur toute cette surface scalaire, le nom de la fabrique et le nom du builder sont le **nom de - type CLR** — la valeur que retourne `System.Type.Name` — et non le mot-clé C# : `Int32` et non - `Int`, `Single` et non `Float`, `Int64` et non `Long`, `Byte`/`SByte`, `Decimal`, `Char`. Les - formes mots-clés C# ne peuvent pas toutes servir d'identifiants (`Any.int()` est une erreur de - syntaxe), et les noms CLR se lisent uniformément avec les noms de types builder. -* Cela reflète les familles de méthodes par primitive de la bibliothèque de base .NET, qui - nomment chaque méthode d'après le type CLR : `Convert.ToInt32` / `ToSingle` / `ToBoolean`, - `BitConverter.ToInt32` / `ToBoolean` — jamais `ToBool`, `ToFloat` ou `ToInt`. -* Une fabrique déviait : `Any.Bool()`, retournant `AnyBool`, produisait un `System.Boolean`, - dont le nom CLR est `Boolean`. C'était le seul membre de la surface scalaire non nommé d'après - son type CLR. L'audit d'architecture et de conception de JustDummies du 2026-07-20 l'a mis en - évidence (§8.2, §8.4) et a recommandé que le choix soit tranché délibérément et consigné avant - la publication. -* `JustDummies` est en pré-publication : aucun tag `dum-v*`, aucun consommateur NuGet externe, une - section *Unreleased* de changelog vide. Renommer une fabrique publique et un type builder - public est un changement cassant dès que des consommateurs en dépendent, et ne coûte rien avant - la première publication. -* Le dépôt consigne les décisions de nommage de cette classe sous forme d'ADR — ADR-0005 réserve - le nom de fabrique nu à la variante retournant un `Outcome`, ADR-0007 nomme les terminaux du - binder `New` et `Create`. - -## Décision - -Toute fabrique scalaire sans paramètre de `Any` et de `AnyContext`, ainsi que le type builder -qu'elle retourne, est nommée d'après le nom CLR du type qu'elle produit, sans exception — en -renommant `Any.Bool()` / `AnyContext.Bool()` / `AnyBool` en `Any.Boolean()` / -`AnyContext.Boolean()` / `AnyBoolean`. - -## Justification - -* La convention était déjà « noms de types CLR » pour toutes les fabriques scalaires sauf une. - Garder `Bool` ne laisse énoncer la règle qu'avec une réserve — « noms CLR, sauf celui qu'on a - raccourci » — et ne la rend vérifiable qu'avec une entrée de liste d'exception portant cette - réserve. Nommer `Boolean` rend la règle sans exception : une phrase la décrit et un seul garde - la fait respecter. -* L'argument ergonomique pour le raccourci `Bool` est exactement celui déjà écarté pour `Int` - (`int`), `Long` (`long`), `Short` (`short`) et `Float` (`float`) — toutes des graphies qu'un - développeur écrit bien plus souvent que `Boolean`. Honorer la forme courte pour le seul `bool` - privilégierait un mot-clé sans principe le distinguant des largeurs et réels que la surface - écrit déjà en toutes lettres. -* Le choix aligne la surface sur les familles par primitive `Convert` / `BitConverter` de la BCL - citées au Contexte, l'analogue existant le plus proche de « une méthode par type simple, nommée - d'après le type » ; un consommateur qui cherche `Convert.ToBoolean` trouve `Any.Boolean()` là où - il l'attend. -* Faire concorder nom de fabrique, nom de builder et nom CLR garde un seul modèle mental — - `Any.X()` → `AnyX` → `System.X` — qu'un unique garde par réflexion peut vérifier sur toute la - surface, si bien qu'un type ajouté plus tard ne peut réintroduire la déviation en silence. -* Trancher en pré-publication ne coûte rien et, selon l'audit, est le moment le moins cher pour - décider ; différer au-delà de la 1.0 transforme un renommage gratuit en changement cassant dans - un sens comme dans l'autre. - -## Alternatives envisagées - -### Garder `Bool()` / `AnyBool` et consigner la forme courte comme exception délibérée - -Envisagée parce que `bool` est la graphie quasi universelle en C# moderne tandis que `Boolean` -est rarement écrit à la main, si bien que `Any.Bool()` est marginalement plus familier au point -d'appel, et qu'une justification d'une ligne dans le README pourrait faire lire la déviation comme -choisie plutôt qu'accidentelle. - -Rejetée parce que le même argument de familiarité s'applique mot pour mot à -`Int`/`Long`/`Short`/`Float`, que la surface écarte déjà ; consigner l'exception préserve une -règle qu'il faut alors énoncer avec une réserve et garder avec une entrée de liste d'exception, -échangeant un renommage unique en pré-publication contre une asymétrie permanente dans la surface -dont toute la valeur est l'uniformité. - -### Ne renommer qu'un côté — le builder en `AnyBoolean` en gardant `Bool()`, ou l'inverse - -Envisagée parce qu'elle toucherait moins de points d'appel. - -Rejetée parce qu'elle casse la correspondance nom-de-fabrique-égale-nom-de-builder qui tient -partout ailleurs (`Any.Int32()` → `AnyInt32`), remplaçant une déviation par une autre et perdant -le bénéfice du modèle mental unique qui motive le changement. - -### Offrir à la fois `Bool()` et `Boolean()`, l'un déléguant à l'autre - -Envisagée parce qu'elle garderait le nom familier tout en ajoutant le nom conventionnel. - -Rejetée parce que deux noms pour un même générateur doublent la surface découvrable, invitent à -des points d'appel incohérents, et laissent tout de même un membre public hors convention à -documenter et à garder ; la fenêtre de pré-publication rend un nom unique et propre disponible -sans coût. - -## Conséquences - -### Positives - -* La surface de fabriques scalaires suit une règle unique sans exception (`Any.X()` → `AnyX` → - CLR `X`), énonçable en une phrase et exécutable par un seul garde par réflexion. -* La surface correspond au nommage par primitive `Convert` / `BitConverter` de la BCL, réduisant - la surprise pour les consommateurs. -* Le nommage est tranché avant la première publication, si bien qu'aucun consommateur ne migre - jamais. - -### Négatives - -* `Any.Boolean()` est plus verbeux que la graphie quasi universelle du mot-clé `bool` ; un - consommateur attendant `Bool()` doit se tourner vers `Boolean()`. -* Le renommage touche d'un coup le type builder, les deux points d'entrée, la vérification de - l'artefact empaqueté et les tests — un coût mécanique, en pré-publication. - -### Risques - -* Sans mise en application, un type scalaire futur pourrait réintroduire un nom court de mot-clé - (un second `Bool`) ; atténué par le garde de parité de nommage ajouté avec cette décision, qui - échoue lorsqu'une fabrique, son builder ou son miroir `AnyContext` s'écarte du nom CLR. - -## Actions de suivi - -* Renommer `Any.Bool()` / `AnyContext.Bool()` / `AnyBool` en `Boolean` / `AnyBoolean`, et mettre - à jour les tests, la sonde d'artefact empaqueté `justdummies-check` et le README du package *(fait - dans ce changement)*. -* Ajouter le garde de parité de nommage à `JustDummies.UnitTests` *(fait dans ce changement)*. - -## Références - -* ADR-0005 — réserver le nom de fabrique nu à la variante retournant un Outcome ; précédent de - décision de nommage. -* ADR-0007 — nommer les terminaux du binder New et Create ; précédent de décision de nommage. -* ADR-0020 — matérialiser les dummies uniquement via Generate() ; partage le cadrage pré-1.0 du - « moment le moins cher pour décider ». -* Audit d'architecture et de conception de JustDummies du 2026-07-20, §8.2 et §8.4 — a mis en - évidence la déviation. -* Issue #222. diff --git a/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.md b/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.md deleted file mode 100644 index e3e105d6..00000000 --- a/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.md +++ /dev/null @@ -1,131 +0,0 @@ -# ADR-0031 | Name Any's scalar factories after their CLR type - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0031-name-any-factories-after-their-clr-type.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-21 -**Accepted:** 2026-07-21 -**Decision Makers:** Reefact - -## Context - -* `JustDummies` exposes a static entry point `Any` whose parameterless factories each start a - generator for one .NET simple type — `Any.Int32()`, `Any.SByte()`, `Any.Single()`, - `Any.UInt64()`, `Any.String()`, `Any.Guid()`, `Any.DateTime()`, … — and each returns a - builder type named `Any{Name}` (`Any.Int32()` returns `AnyInt32`). `AnyContext` mirrors - every one of these scalar factories for the seeded-context surface, so each factory exists - in two places that must agree. -* Across that whole scalar surface the factory name and the builder name are the **CLR type - name** — the value `System.Type.Name` returns — not the C# keyword: `Int32` not `Int`, - `Single` not `Float`, `Int64` not `Long`, `Byte`/`SByte`, `Decimal`, `Char`. The C# keyword - forms cannot all serve as identifiers (`Any.int()` is a syntax error), and the CLR names - read uniformly with the builder type names. -* This mirrors the .NET base class library's own per-primitive method families, which name - each method after the CLR type: `Convert.ToInt32` / `ToSingle` / `ToBoolean`, - `BitConverter.ToInt32` / `ToBoolean` — never `ToBool`, `ToFloat`, or `ToInt`. -* One factory deviated: `Any.Bool()`, returning `AnyBool`, produced a `System.Boolean`, whose - CLR name is `Boolean`. It was the single member of the scalar surface not named after its - CLR type. The 2026-07-20 JustDummies architecture & design audit surfaced it (§8.2, §8.4) and - recommended the choice be settled deliberately and recorded before release. -* `JustDummies` is pre-release: no `dum-v*` tag, no external NuGet consumers, an empty *Unreleased* - changelog. Renaming a public factory and a public builder type is a breaking change once - consumers depend on it, and costs nothing before the first publication. -* The repository records naming decisions of this class as ADRs — ADR-0005 reserves the plain - factory name for the `Outcome`-returning variant, ADR-0007 names the binder terminals `New` - and `Create`. - -## Decision - -Every parameterless scalar factory on `Any` and `AnyContext`, and the builder type it returns, -is named after the CLR name of the type it produces, with no exception — renaming `Any.Bool()` -/ `AnyContext.Bool()` / `AnyBool` to `Any.Boolean()` / `AnyContext.Boolean()` / `AnyBoolean`. - -## Rationale - -* The convention was already "CLR type names" for every scalar factory but one. Keeping `Bool` - leaves the rule statable only with a caveat — "CLR names, except the one we shortened" — and - checkable only with an allow-list entry carrying that exception. Naming `Boolean` makes the - rule exception-free: one sentence describes it and one guard enforces it. -* The ergonomic case for the short `Bool` is exactly the case already declined for `Int` - (`int`), `Long` (`long`), `Short` (`short`), and `Float` (`float`) — all spellings a - developer writes far more often than `Boolean`. Honouring the short form for `bool` alone - would privilege one keyword with no principle distinguishing it from the widths and reals the - surface already spells out in full. -* The choice aligns the surface with the BCL's own `Convert` / `BitConverter` per-primitive - families named in Context, the closest existing analog to "one method per simple type, named - after the type"; a consumer who reaches for `Convert.ToBoolean` finds `Any.Boolean()` where - they expect it. -* Matching factory name to builder name to CLR name keeps one mental model — `Any.X()` → - `AnyX` → `System.X` — which a single reflection guard can assert for the whole surface, so a - future added type cannot silently reintroduce the deviation. -* Settling this pre-release costs nothing and, per the audit, is the cheapest moment to decide; - deferring past 1.0 turns a free rename into a breaking change in either direction. - -## Alternatives Considered - -### Keep `Bool()` / `AnyBool` and record the short form as a deliberate exception - -Considered because `bool` is the near-universal spelling in modern C# while `Boolean` is rarely -hand-written, so `Any.Bool()` is marginally more familiar at the call site, and a one-line -README rationale could make the deviation read as chosen rather than accidental. - -Rejected because the same familiarity argument applies verbatim to `Int`/`Long`/`Short`/`Float`, -which the surface already declines; recording the exception preserves a rule that must then be -stated with a caveat and guarded with an allow-list entry, trading a one-time pre-release rename -for permanent asymmetry in the surface whose whole value is its uniformity. - -### Rename only one side — the builder to `AnyBoolean` while keeping `Bool()`, or the reverse - -Considered because it would touch fewer call sites. - -Rejected because it breaks the factory-name-equals-builder-name correspondence that holds -everywhere else (`Any.Int32()` → `AnyInt32`), replacing one deviation with another and losing -the single-mental-model benefit that motivates the change. - -### Offer both `Bool()` and `Boolean()`, one delegating to the other - -Considered because it would keep the familiar name while adding the conventional one. - -Rejected because two names for one generator double the discoverable surface, invite -inconsistent call sites, and still leave an off-convention public member to document and guard; -the pre-release window makes a single clean name available at no cost. - -## Consequences - -### Positive - -* The scalar factory surface follows one exception-free rule (`Any.X()` → `AnyX` → CLR `X`), - statable in a sentence and enforceable by a single reflection guard. -* The surface matches the BCL's own `Convert` / `BitConverter` per-primitive naming, lowering - surprise for consumers. -* The naming is settled before the first release, so no consumer ever migrates. - -### Negative - -* `Any.Boolean()` is more verbose than the near-universal `bool` keyword spelling; a consumer - expecting `Bool()` must reach for `Boolean()`. -* The rename touches the builder type, both entry points, the packaged-asset check, and the - tests at once — a mechanical, pre-release cost. - -### Risks - -* Without enforcement a future scalar type could reintroduce a keyword-short name (a second - `Bool`); mitigated by the factory-naming parity guard added with this decision, which fails - when a factory, its builder, or its `AnyContext` mirror departs from the CLR name. - -## Follow-up Actions - -* Rename `Any.Bool()` / `AnyContext.Bool()` / `AnyBool` to `Boolean` / `AnyBoolean`, and update - the tests, the `justdummies-check` packaged-asset probe, and the package README *(done in this - change)*. -* Add the factory-naming parity guard to `JustDummies.UnitTests` *(done in this change)*. - -## References - -* ADR-0005 — reserve the plain factory name for the Outcome-returning variant; naming-decision - precedent. -* ADR-0007 — name the binder terminals New and Create; naming-decision precedent. -* ADR-0020 — materialize dummies only through Generate(); shares the pre-1.0 "cheapest moment to - decide" framing. -* 2026-07-20 JustDummies architecture & design audit, §8.2 and §8.4 — surfaced the deviation. -* Issue #222. diff --git a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md deleted file mode 100644 index 57d1c136..00000000 --- a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md +++ /dev/null @@ -1,166 +0,0 @@ -# ADR-0032 | Tirer des valeurs arbitraires depuis un pool de choix explicite et de premier niveau - -🌍 🇬🇧 [English](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-21 -**Accepté :** 2026-07-21 -**Décideurs :** Reefact - -## Contexte - -`JustDummies` fournit des valeurs arbitraires mais valides depuis une source unique et seedable, de sorte que tout run est -reproductible à partir d'un seed reporté (ADR-0006). Un besoin récurrent est une valeur dont le domaine est un -**ensemble fermé que l'appelant détient déjà** — l'une des devises pour lesquelles un contexte est configuré, l'une des -commandes déjà présentes dans une fixture, l'un d'une poignée d'états métier sur lesquels le test ne porte aucune -assertion. La bibliothèque génère des formes structurelles (une longueur, un intervalle, un motif) ; elle ne peut pas -synthétiser un tel ensemble du monde réel, et c'est l'appelant qui possède les valeurs. - -Plusieurs faits existants cadrent le choix : - -* La façon la plus courante de tirer aujourd'hui dans un ensemble détenu par l'appelant est écrite à la main — - `pool[new Random().Next(pool.Count)]` — ce qui tire d'un `Random` neuf, ignore la source seedée ambiante et ne peut - donc pas être rejoué sous `Any.Reproducibly(...)` : précisément le piège que la bibliothèque existe pour supprimer. -* Les builders scalaires et d'enum exposent `OneOf(params T[])`, mais uniquement **au sein de leur propre domaine** — - il restreint l'intervalle ou le pool d'un scalaire et se contrôle vis-à-vis des autres contraintes. Il n'existe aucun - combinateur de premier niveau pour tirer dans un pool d'objets métier arbitraires. -* L'ADR-0030 a ajouté `Any.String().OneOf(...)` comme générateur d'ensemble de valeurs **terminal** chaîné sur le point - d'entrée des chaînes : il implémente `ICardinalityHint`, déduplique sous une comparaison ordinale, tire - uniformément et de façon reproductible, et **rejette un élément `null`**, en orientant l'appelant vers `OrNull()`. Il - est spécifique aux chaînes ; un pool d'objets métier agnostique au type n'a aucun builder typé sur lequel se chaîner. -* Les collections distinctes se gardent, au moment de la déclaration, sur la cardinalité annoncée par le générateur - d'éléments (ADR-0013), à travers l'interface interne `ICardinalityHint` ; un générateur qui annonce une - cardinalité répond aussi à l'appartenance. -* `OrNull()` est le décorateur orthogonal de nullabilité de la bibliothèque, pour les types valeur comme référence. -* Avec une méthode `params T[]` seule, passer une unique collection détenue lie le paramètre de type au **type de la - collection**, non à ses éléments ; et lorsque le type d'élément est lui-même énumérable, une surcharge acceptant - aussi `IEnumerable` rend `OneOf(collection)` ambigu entre « un pool contenant la collection » et « un pool de ses - éléments ». -* Une factory qui prend des valeurs brutes plutôt qu'un opérande `IAny<>` n'hérite d'aucun contexte aléatoire depuis un - opérande, donc — contrairement à `Combine`/`ListOf`/`SetOf` — elle doit exister à la fois sur `Any` (ambiant) et - `AnyContext` (seedé) ; la garde de parité de surface traite une telle factory comme une factory scalaire et exige le - miroir. -* L'audit d'architecture et de conception de JustDummies du 2026-07-20 (§10) classe cet ajout comme le Must-Have à plus - fort levier : chaque consommateur, presque chaque semaine. - -## Décision - -`Any.OneOf(params T[])` et `Any.ElementOf(IReadOnlyList)`/`Any.ElementOf(IEnumerable)` — reflétées sur -`AnyContext` — tirent une valeur uniformément dans un pool explicite fourni par l'appelant, en tant que générateur -terminal, en rejetant un pool vide et tout élément `null`, et en dédupliquant, dimensionnant et testant l'appartenance -du pool sous `EqualityComparer.Default`. - -## Justification - -* **Cela ferme le piège de reproductibilité qui est la raison d'être de la bibliothèque.** Un tirage de pool conscient - du seed remplace le `Random` écrit à la main, si bien que le choix se rejoue sous `Any.Reproducibly(...)` et - `Any.WithSeed(...)` comme tout autre tirage (ADR-0006). L'audit désigne cet ajout comme celui au plus fort levier. -* **Rejeter `null` garde la nullabilité orthogonale et la surface symétrique.** `OrNull()` est l'unique manière - d'exprimer une valeur optionnelle, aussi un membre de pool `null` réintroduirait-il l'ambiguïté « `null` est-il une - valeur ou une absence » que ce décorateur existe pour supprimer. Cela s'aligne aussi sur le générateur de chaînes - livré (ADR-0030) : les deux combinateurs d'ensemble de valeurs restent symétriques sur leur contrat `null` au lieu de - diverger — le type d'asymétrie contre lequel l'audit met en garde. Un appelant qui veut un `null` occasionnel écrit - toujours `OneOf(...).OrNull()`. -* **`EqualityComparer.Default` est l'analogue agnostique au type de la déduplication ordinale du générateur de - chaînes, et c'est le choix *sound* pour le contrat de cardinalité.** Une collection distincte en aval portant un - comparateur personnalisé plus grossier ne peut que *fusionner* des valeurs du pool, jamais en créer de nouvelles ; le - nombre distinct annoncé reste donc une borne supérieure conservatrice et l'appartenance ne revendique jamais une - valeur absente du pool — la collection continue de se garder correctement (ADR-0013). -* **Un générateur terminal qui annonce sa cardinalité compose gratuitement.** Le pool est la spécification tout entière - — il n'y a aucun domaine scalaire à restreindre — de sorte que le générateur n'expose aucune autre contrainte, tout - en circulant à travers `As(...)`, `OrNull()`, `Combine(...)` et les générateurs de collections comme tout `IAny`, - et une collection distincte au-dessus de lui se garde tôt comme les autres générateurs à domaine dénombrable. -* **Deux noms valent mieux qu'un seul nom surchargé.** `OneOf` prend des littéraux en ligne ; `ElementOf` prend une - collection détenue. La séparation supprime le piège d'inférence générique : les valeurs en ligne ne lient jamais le - paramètre de type à un conteneur, et une collection détenue n'est jamais confondue avec ses propres éléments. La - surcharge séquence d'`ElementOf` matérialise une fois, de sorte qu'une requête paresseuse n'est pas ré-énumérée à - chaque tirage. -* **Le miroir `AnyContext` est requis, pas optionnel.** Le pool ne porte aucun opérande d'où hériter d'un contexte - seedé, donc sans miroir la surface seedée présenterait un trou silencieux ; la garde de parité fait de l'omission un - test qui échoue. - -## Alternatives considérées - -### Autoriser `null` comme membre de pool - -Considérée parce qu'un objet métier `null` est sans doute un choix arbitraire valide, et parce qu'un membre `null` -(poids `1/n`) diffère, sur le plan de la distribution, du tirage au sort indépendant d'`OrNull()` — la direction que -l'issue déposée proposait d'abord. - -Rejetée parce qu'elle contredirait le `Any.String().OneOf(...)` livré (ADR-0030), réintroduirait l'ambiguïté -valeur-contre-absence qu'`OrNull()` supprime, et scinderait les deux combinateurs d'ensemble de valeurs sur leur -contrat `null` — exactement l'asymétrie de surface que l'audit signale. Le cas du `null` occasionnel reste servi par -`OneOf(...).OrNull()`. - -### Un unique `OneOf(params T[])` surchargé plus `OneOf(IEnumerable)`, sans `ElementOf` - -Considérée pour l'économie de surface, en miroir des deux surcharges du builder de chaînes. - -Rejetée parce que l'inférence générique en fait un piège : passer une collection détenue à la forme `params` met le -conteneur lui-même en pool, et lorsque le type d'élément est énumérable les deux surcharges rendent `OneOf(collection)` -ambigu entre le conteneur et ses éléments. Un nom `ElementOf` distinct rend l'intention non ambiguë au site d'appel. - -### Ajouter une surcharge `IEqualityComparer`, comme `SetOf` - -Considérée parce qu'un appelant pourrait vouloir que l'identité du pool soit décidée par un comparateur personnalisé. - -Rejetée comme inutile pour la v1 : le comparateur par défaut fournit déjà une borne de cardinalité *sound* sous -n'importe quel comparateur aval, et un comparateur spécifique au pool pourra être ajouté plus tard sur preuve de besoin -sans changer le contrat par défaut. - -### La chaîner sur un point d'entrée typé, comme le `OneOf` des chaînes - -Considérée pour la cohérence avec `Any.String().OneOf(...)`. - -Rejetée parce qu'un objet métier arbitraire n'a aucun builder `Any.X()` sur lequel se chaîner — tout l'intérêt est une -factory de premier niveau, agnostique au type — de sorte qu'une factory statique sur `Any`/`AnyContext` est la seule -forme qui convienne. - -## Conséquences - -### Positives - -* La lacune classée première par l'audit est comblée : tirer dans un ensemble détenu par l'appelant devient un dummy - d'une ligne, reproductible par seed, qui compose vers des objets valeur (`As`), des optionnels (`OrNull`) et des - collections comme tout autre générateur. -* Les combinateurs d'ensemble de valeurs des chaînes et génériques partagent désormais un seul contrat `null` (rejeté, - via `OrNull()`), supprimant une asymétrie au lieu d'en ajouter une. -* Une collection distincte au-dessus du pool est gardée tôt par sa cardinalité, en cohérence avec les autres - générateurs à domaine dénombrable. - -### Négatives - -* Un nouveau type public (`AnyOneOf`) et deux noms de point d'entrée (`OneOf`/`ElementOf`) à maintenir, documenter - et garder reflétés sur `AnyContext`. -* La bibliothèque ne vérifie pas que les valeurs du pool respectent un format externe : ce sont le contenu de - l'appelant, et un objet valeur a toujours besoin d'`As(...)` pour faire respecter son invariant. - -### Risques - -* Un appelant peut s'attendre à ce que `null` soit un membre de pool légal et être surpris qu'il soit refusé. Atténué - par le message d'exception qui pointe vers `OrNull()` — le même conseil que donne le générateur de chaînes. -* Un appelant peut passer une collection détenue à `OneOf` et obtenir un pool d'un seul élément. Atténué par `ElementOf` - qui est le chemin documenté pour une collection détenue, et par le résumé d'`OneOf` qui y oriente. - -## Actions de suivi - -* Documenter `OneOf`/`ElementOf` dans le guide utilisateur de JustDummies (`ArbitraryTestValues.en.md`) et sa traduction - française, ainsi que dans le README du package (`README.nuget.md`), avec un exemple. -* Garder le miroir `Any`↔`AnyContext` au vert (imposé par `SurfaceParityTests`). -* Envisager d'aligner les messages d'élément `null` du générateur de chaînes et du générateur générique lors de la - prochaine révision de la surface des chaînes. - -## Références - -* ADR-0030 — Tirer des chaînes arbitraires depuis un ensemble de valeurs explicite et terminal (le frère « chaînes » ; - le précédent générateur terminal et rejet du `null`). -* ADR-0013 — Garder les collections distinctes par cardinalité, sinon par un tirage borné (le contrat - `ICardinalityHint`). -* ADR-0006 — Fournir les valeurs de test arbitraires depuis une source unique et seedable (reproductibilité). -* ADR-0031 — Nommer les factories scalaires d'Any d'après leur type CLR (pourquoi `OneOf`/`ElementOf`, en tant que - combinateurs, sont exemptés). -* ADR-0020 — Ne matérialiser les dummies qu'à travers `Generate()`. -* Issue [#223](https://github.com/Reefact/first-class-errors/issues/223) et l'audit d'architecture et de conception de - JustDummies du 2026-07-20 (§10 Must-Have). -* Le type `AnyOneOf`, les factories `Any.OneOf`/`Any.ElementOf` et `AnyContext.OneOf`/`AnyContext.ElementOf`, et - leurs tests dans le projet `JustDummies` et `JustDummies.UnitTests`. diff --git a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md b/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md deleted file mode 100644 index 8d710a08..00000000 --- a/doc/handwritten/for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md +++ /dev/null @@ -1,153 +0,0 @@ -# ADR-0032 | Draw arbitrary values from an explicit, top-level choice pool - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-21 -**Accepted:** 2026-07-21 -**Decision Makers:** Reefact - -## Context - -`JustDummies` supplies arbitrary yet valid values from a single seedable source, so any run is reproducible from a -reported seed (ADR-0006). A recurring need is a value whose domain is a **closed set the caller already holds** — one -of the currencies a context is configured with, one of the orders already in a fixture, one of a handful of domain -states the test does not assert on. The library generates structural shapes (a length, an interval, a pattern); it -cannot synthesize such a real-world set, and the caller owns the values. - -Several existing facts frame the choice: - -* The most common way to pick from a caller-held set today is hand-rolled — `pool[new Random().Next(pool.Count)]` — - which draws from a fresh `Random`, ignores the ambient seeded source, and therefore cannot replay under - `Any.Reproducibly(...)`: exactly the trap the library exists to remove. -* The scalar and enum builders expose `OneOf(params T[])`, but only **within their own domain** — it narrows a - scalar's interval or pool and cross-validates against the other constraints. There is no top-level combinator to - draw from a pool of arbitrary domain objects. -* ADR-0030 added `Any.String().OneOf(...)` as a **terminal** value-set generator chained off the string entry point: - it implements `ICardinalityHint`, deduplicates under an ordinal comparison, draws uniformly and - reproducibly, and **rejects a `null` element**, directing the caller to `OrNull()`. It is string-specific; a - type-agnostic pool of domain objects has no typed builder to chain off. -* Distinct collections gate on an element generator's advertised cardinality at declaration time (ADR-0013), through - the internal `ICardinalityHint`; a generator that advertises a cardinality also answers membership. -* `OrNull()` is the library's orthogonal decorator for nullability, for both value and reference types. -* With a `params T[]`-only method, passing a single held collection binds the type parameter to the **collection - type**, not its elements; and when the element type is itself enumerable, an overload that also accepts - `IEnumerable` makes `OneOf(collection)` ambiguous between "a pool holding the collection" and "a pool of its - elements". -* A factory that takes raw values rather than an `IAny<>` operand does not inherit a random context from an operand, - so — unlike `Combine`/`ListOf`/`SetOf` — it must exist on both `Any` (ambient) and `AnyContext` (seeded); the - surface-parity guard treats such a factory as a scalar factory and requires the mirror. -* The 2026-07-20 JustDummies architecture & design audit (§10) ranks this the highest-leverage Must-Have: every consumer, - most weeks. - -## Decision - -`Any.OneOf(params T[])` and `Any.ElementOf(IReadOnlyList)`/`Any.ElementOf(IEnumerable)` — mirrored on -`AnyContext` — draw one value uniformly from an explicit, caller-supplied pool as a terminal generator, rejecting an -empty pool and any `null` element, and deduplicating, sizing and testing membership of the pool under -`EqualityComparer.Default`. - -## Rationale - -* **It closes the reproducibility trap that is the library's reason to exist.** A seed-aware pool draw replaces the - hand-rolled `Random`, so the choice replays under `Any.Reproducibly(...)` and `Any.WithSeed(...)` like every other - draw (ADR-0006). The audit names this the single highest-leverage addition. -* **Rejecting `null` keeps nullability orthogonal and the surface symmetric.** `OrNull()` is the one way to express an - optional value, so a `null` pool member would reintroduce the "is `null` a value or an absence" ambiguity that - decorator exists to remove. It also matches the shipped string generator (ADR-0030): the two value-set combinators - stay symmetric on their `null` contract instead of diverging — the kind of asymmetry the audit warns against. A - caller who wants an occasional `null` still writes `OneOf(...).OrNull()`. -* **`EqualityComparer.Default` is the type-agnostic analogue of the string generator's ordinal dedup, and it is the - sound choice for the cardinality contract.** A downstream distinct collection carrying a coarser custom comparer can - only *merge* pool values, never create new ones, so the advertised distinct count stays a conservative upper bound - and membership never claims a value the pool lacks — the collection keeps gating correctly (ADR-0013). -* **A terminal generator that advertises its cardinality composes for free.** The pool is the whole specification — - there is no scalar domain to narrow — so the generator exposes no further constraints, yet it flows through - `As(...)`, `OrNull()`, `Combine(...)` and the collection generators as any `IAny` does, and a distinct collection - over it gates eagerly like the other countable-domain generators. -* **Two names beat one overloaded name.** `OneOf` takes inline literals; `ElementOf` takes a held collection. The - split removes the generic-inference footgun: inline values never bind the type parameter to a container, and a held - collection is never confused with its own elements. `ElementOf`'s sequence overload materializes once so a lazy - query is not re-enumerated per draw. -* **The `AnyContext` mirror is required, not optional.** The pool carries no operand from which to inherit a seeded - context, so without a mirror the seeded surface would have a silent hole; the parity guard makes the omission a - failing test. - -## Alternatives Considered - -### Allow `null` as a pool member - -Considered because a `null` domain object is arguably a valid arbitrary choice, and a `null` member (weight `1/n`) is -distributionally different from `OrNull()`'s independent coin flip — the direction the filed issue first proposed. - -Rejected because it would contradict the shipped `Any.String().OneOf(...)` (ADR-0030), reintroduce the -value-versus-absence ambiguity `OrNull()` removes, and split the two value-set combinators on their `null` contract — -exactly the surface asymmetry the audit flags. The occasional-`null` case is still served by `OneOf(...).OrNull()`. - -### A single overloaded `OneOf(params T[])` plus `OneOf(IEnumerable)`, no `ElementOf` - -Considered for surface economy, mirroring the string builder's two overloads. - -Rejected because generic inference turns it into a footgun: passing a held collection to the `params` form pools the -container itself, and when the element type is enumerable the two overloads make `OneOf(collection)` ambiguous between -the container and its elements. A distinct `ElementOf` name makes the intent unambiguous at the call site. - -### Add an `IEqualityComparer` overload, as `SetOf` has - -Considered because a caller might want pool identity decided by a custom comparer. - -Rejected as unneeded for v1: the default comparer already yields a sound cardinality bound under any downstream -comparer, and a pool-specific comparer can be added later on evidence of need without changing the default contract. - -### Chain it off a typed entry point, like the string `OneOf` - -Considered for consistency with `Any.String().OneOf(...)`. - -Rejected because an arbitrary domain object has no `Any.X()` builder to chain from — the whole point is a -type-agnostic, top-level factory — so a static factory on `Any`/`AnyContext` is the only shape that fits. - -## Consequences - -### Positive - -* The audit's first-ranked gap is closed: picking from a caller-held set becomes a one-line, seed-reproducible dummy - that composes into value objects (`As`), optionals (`OrNull`) and collections like every other generator. -* The string and generic value-set combinators now share one `null` contract (rejected, via `OrNull()`), removing an - asymmetry rather than adding one. -* A distinct collection over the pool is gated eagerly by its cardinality, consistent with the other - countable-domain generators. - -### Negative - -* A new public type (`AnyOneOf`) and two entry-point names (`OneOf`/`ElementOf`) to maintain, document, and keep - mirrored on `AnyContext`. -* The library does not check that the pooled values meet any external format: they are the caller's content, and a - value object still needs `As(...)` to enforce its invariant. - -### Risks - -* A caller may expect `null` to be a legal pool member and be surprised it is refused. Mitigated by the exception - message pointing at `OrNull()` — the same guidance the string generator gives. -* A caller may pass a held collection to `OneOf` and get a pool of one element. Mitigated by `ElementOf` being the - documented path for a held collection, and by `OneOf`'s summary directing there. - -## Follow-up Actions - -* Document `OneOf`/`ElementOf` in the JustDummies user guide (`ArbitraryTestValues.en.md`) and its French translation, and - in the package README (`README.nuget.md`), with an example. -* Keep the `Any`↔`AnyContext` mirror green (enforced by `SurfaceParityTests`). -* Consider aligning the string generator's and the generic generator's `null`-element messages when the string - surface is next revised. - -## References - -* ADR-0030 — Draw arbitrary strings from an explicit, terminal value set (the string sibling; the terminal-generator - and `null`-rejection precedent). -* ADR-0013 — Gate distinct collections by cardinality, otherwise by a bounded draw (the `ICardinalityHint` contract). -* ADR-0006 — Supply arbitrary test values from a single seedable source (reproducibility). -* ADR-0031 — Name Any's scalar factories after their CLR type (why `OneOf`/`ElementOf`, as combinators, are exempt). -* ADR-0020 — Materialize dummies only through `Generate()`. -* Issue [#223](https://github.com/Reefact/first-class-errors/issues/223) and the 2026-07-20 JustDummies architecture & - design audit (§10 Must-Have). -* The `AnyOneOf` type, the `Any.OneOf`/`Any.ElementOf` and `AnyContext.OneOf`/`AnyContext.ElementOf` factories, and - their tests in the `JustDummies` project and `JustDummies.UnitTests`. diff --git a/doc/handwritten/for-maintainers/adr/0033-meet-string-exclusions-with-a-bounded-redraw.fr.md b/doc/handwritten/for-maintainers/adr/0033-meet-string-exclusions-with-a-bounded-redraw.fr.md deleted file mode 100644 index bae5ec99..00000000 --- a/doc/handwritten/for-maintainers/adr/0033-meet-string-exclusions-with-a-bounded-redraw.fr.md +++ /dev/null @@ -1,84 +0,0 @@ -# ADR-0033 | Traiter les exclusions de chaînes par un tirage borné - -🌍 🇬🇧 [English](0033-meet-string-exclusions-with-a-bounded-redraw.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-22 -**Accepté :** 2026-07-22 -**Décideurs :** Reefact - -## Contexte - -JustDummies construit un scalaire directement pour satisfaire ses contraintes — jamais généré-puis-filtré — et détecte les contradictions au moment de la déclaration, de sorte qu'un générateur scalaire qui existe peut toujours générer. Il évite aussi les boucles de nouvelles tentatives cachées et non bornées. - -Tous les builders scalaires sauf un exposent un trio d'exclusion (`OneOf`/`Except`/`DifferentFrom`). Pour les types à projection ordinale (entiers, types temporels, `char`, `Guid`), une exclusion est intégrée à la construction : le tirage est projeté sur le k-ième ordinal non exclu du domaine en une passe, et le fait que les exclusions laissent le domaine non vide se compte à bas coût à la déclaration. - -Les chaînes n'ont pas de projection ordinale. Un `AnyString` est assemblé par disposition — préfixe, remplissage, valeurs contenues, suffixe — sur un domaine effectivement non borné. Une valeur exclue ne peut pas être retirée de ce domaine par construction, et le fait qu'un ensemble d'exclusions laisse une forme satisfaisable n'est pas décidable à bas coût en général : c'est trivial pour une longueur fixe d'un caractère, mais cela croît de façon combinatoire avec la longueur et le jeu de caractères. - -`AnyString` était le seul builder scalaire sans contraintes d'exclusion, alors que « une valeur différente de celle que je détiens déjà » — tester un chemin d'inégalité avec un identifiant de type chaîne tout en préservant son format — est un besoin courant de chaîne factice (issue #224). L'écrire à la main avec une boucle de nouvelles tentatives oublie généralement la source seedée et casse la reproductibilité, précisément le piège que la bibliothèque existe pour éviter. - -La bibliothèque accepte déjà un endroit où une valeur qu'un appelant a déclarée peut malgré tout ne pas se matérialiser : une collection distincte sur un domaine non dénombrable tire-et-déduplique sous un budget borné et échoue à la génération, de manière reproductible, lorsqu'elle ne le peut pas (ADR-0013). `AnyString.OneOf` est un générateur terminal distinct qui ne se combine pas avec les autres contraintes (ADR-0030). - -## Décision - -`AnyString.DifferentFrom`/`Except` sont satisfaits par un nouveau tirage borné de la disposition constructive, et une exclusion qui rend la forme insatisfaisable échoue à la génération par une erreur reproductible portant la seed, plutôt qu'au moment de la déclaration. - -## Justification - -Comme une chaîne ne porte aucune projection ordinale, une exclusion ne peut pas être intégrée à la disposition comme pour les types ordinaux ; un nouveau tirage est donc la seule stratégie générale — la même échappatoire qu'une collection distincte utilise déjà lorsqu'elle ne sait pas compter son domaine. La borner garantit la terminaison et transforme une exclusion insatisfaisable en un échec diagnostiquable et reproductible plutôt qu'en blocage ; porter la seed maintient cet échec dans le contrat de reproductibilité de la bibliothèque. - -L'échec est différé plutôt qu'anticipé parce que la satisfaisabilité d'une chaîne sous exclusion n'est pas décidable à bas coût en général. Un contrôle complet au moment de la déclaration est donc irréalisable, et un contrôle partiel diagnostiquerait certaines specs insatisfaisables à la déclaration et d'autres seulement à la génération — une couture incohérente, pire qu'une règle unique et prévisible. Différer uniformément est le choix honnête, et cela confine l'écart aux seules exclusions : toute autre contrainte de chaîne reste constructive et validée par anticipation. - -Accepter cet écart est justifié car les alternatives sont pires : laisser le manque garde le builder le plus utilisé comme le seul scalaire incapable d'exclure et renvoie les utilisateurs vers des boucles de nouvelles tentatives qui cassent le seed, tandis qu'imposer un verdict anticipé exige une procédure de décision que le domaine n'admet pas à bas coût. Le coût — un unique cas, cerné et documenté, où un générateur de chaîne qui existe peut malgré tout échouer — est le compromis déjà accepté pour les collections distinctes, et les collisions attendues sont ≈ 0 pour toute forme non triviale, de sorte que le chemin rapide constructif est préservé en pratique. - -Le budget de nouveau tirage, le contenu de l'exception et la propagation de la seed relèvent de l'implémentation, documentés dans le code `JustDummies` (`StringSpec`) et la documentation utilisateur de JustDummies — pas ici. - -## Alternatives envisagées - -### Laisser `AnyString` sans contraintes d'exclusion - -Envisagé parce que cela préservait la règle purement constructive des scalaires et n'exigeait aucun nouveau canal d'échec. Rejeté parce que cela laissait le builder le plus utilisé comme le seul scalaire incapable d'exprimer une exclusion, forçant des boucles de nouvelles tentatives écrites à la main qui cassent silencieusement le seed. - -### Décider la satisfaisabilité par anticipation, comme les builders ordinaux - -Envisagé parce que le diagnostic au moment de la déclaration est la norme de la bibliothèque pour les contraintes contradictoires. Rejeté parce que la satisfaisabilité d'une chaîne sous exclusion n'est pas décidable à bas coût en général ; un contrôle anticipé partiel serait une couture incohérente, diagnostiquant certaines specs tôt et d'autres tard. - -### Tirer sans borne - -Envisagé car une exclusion satisfaisable finirait par aboutir. Rejeté parce qu'une exclusion insatisfaisable boucquerait indéfiniment, violant le principe d'absence de boucles cachées non bornées. - -### Évitement conscient de la spec à la disposition - -Envisagé parce que construire la chaîne pour esquiver l'ensemble exclu garderait l'exclusion constructive et anticipée. Rejeté comme disproportionné : éviter correctement un ensemble exclu arbitraire sur les positions libres de la disposition est complexe pour un chemin que le nouveau tirage n'emprunte pour ainsi dire jamais ; on pourra le réexaminer si les faits le justifient. - -## Conséquences - -### Positives - -* La paire d'exclusion est désormais uniforme sur tous les builders scalaires ; le besoin courant « identifiant différent, même forme » est servi, seedé et reproductible. -* Le chemin rapide constructif est inchangé pour toute spec sans exclusion, et en pratique pour les exclusions aussi (collisions ≈ 0). -* Une exclusion insatisfaisable échoue de manière sûre, reproductible, et nomme la seed à rejouer — cohérent avec l'ADR-0013. - -### Négatives - -* « Un `AnyString` qui existe peut toujours générer » ne tient plus sans condition : une exclusion trop serrée est le seul cas différé à la génération. -* Le moment de l'échec d'une exclusion de chaîne insatisfaisable diffère du diagnostic anticipé, au moment de la déclaration, que donnent les builders ordinaux. - -### Risques - -* Un budget mal calibré pourrait faire échouer une forme théoriquement satisfaisable mais extrêmement serrée. Mesure : documenter le budget et le réviser sur la base de faits, plutôt que de présenter l'échec comme impossible (la posture de l'ADR-0013). -* Les utilisateurs pourraient s'attendre à ce que l'exclusion de chaîne soit constructive comme pour les builders numériques. Mesure : énoncer explicitement le nouveau tirage et son échec différé dans la documentation du builder et le readme de JustDummies. - -## Actions de suivi - -* Documenter le nouveau tirage et l'échec différé portant la seed dans le readme de JustDummies et la documentation du builder (fait dans la pull request d'implémentation). -* Réexaminer le budget si l'usage réel révèle des épuisements indus. -* Envisager l'évitement conscient de la spec seulement si les faits montrent que le tirage borné est insuffisant. - -## Références - -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md) — le canal frère « tirage borné avec échec différé ». -* [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.fr.md) — `AnyString.OneOf` reste terminal et ne se combine pas avec les exclusions. -* [ADR-0020](0020-materialize-dummies-only-through-generate.fr.md) — les dummies se matérialisent uniquement via `Generate()`. -* `StringSpec` et `AnyString` dans le projet `JustDummies` ; le readme NuGet de JustDummies. -* Issue [#224](https://github.com/Reefact/first-class-errors/issues/224). diff --git a/doc/handwritten/for-maintainers/adr/0033-meet-string-exclusions-with-a-bounded-redraw.md b/doc/handwritten/for-maintainers/adr/0033-meet-string-exclusions-with-a-bounded-redraw.md deleted file mode 100644 index d3ccbd82..00000000 --- a/doc/handwritten/for-maintainers/adr/0033-meet-string-exclusions-with-a-bounded-redraw.md +++ /dev/null @@ -1,84 +0,0 @@ -# ADR-0033 | Meet string exclusions with a bounded redraw - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0033-meet-string-exclusions-with-a-bounded-redraw.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-22 -**Accepted:** 2026-07-22 -**Decision Makers:** Reefact - -## Context - -JustDummies builds a scalar directly to satisfy its constraints — never generated-then-filtered — and detects contradictions eagerly at declaration, so a scalar generator that exists can always generate. It also avoids hidden unbounded retry loops. - -Every scalar builder but one exposes an exclusion trio (`OneOf`/`Except`/`DifferentFrom`). For the ordinal-mapped types (integers, temporal types, `char`, `Guid`) an exclusion is built into construction: the draw is mapped onto the k-th non-excluded value of the domain in one pass, and whether the exclusions leave the domain non-empty is counted cheaply at declaration. - -Strings have no ordinal mapping. An `AnyString` is assembled by layout — prefix, filler, contained values, suffix — over an effectively unbounded domain. An excluded value cannot be projected out of that domain by construction, and whether a set of exclusions leaves a shape satisfiable is not cheaply decidable in general: it is trivial for a fixed one-character length, but grows combinatorially with length and character set. - -`AnyString` was the only scalar builder with no exclusion constraints, even though "a value different from the one I already hold" — testing an inequality path with a string identifier while keeping its format — is a common dummy-string need (issue #224). Hand-rolling it with a retry loop typically forgets the seeded source and breaks reproducibility, the exact trap the library exists to prevent. - -The library already accepts one place where a value that a caller declared may still fail to materialize: a distinct collection over an uncountable domain draws-and-deduplicates under a bounded budget and fails at generation, reproducibly, when it cannot (ADR-0013). `AnyString.OneOf` is a separate, terminal generator that does not combine with other constraints (ADR-0030). - -## Decision - -`AnyString.DifferentFrom`/`Except` are satisfied by a bounded redraw of the constructive layout, and an exclusion that leaves the shape unsatisfiable fails at generation with a reproducible, seed-bearing error rather than eagerly at declaration. - -## Rationale - -Because a string carries no ordinal mapping, an exclusion cannot be built into the layout the way it is for ordinal types, so a redraw is the only general strategy — the same escape a distinct collection already uses when it cannot count its domain. Bounding it preserves termination and turns an unsatisfiable exclusion into a diagnosable, reproducible failure rather than a hang; carrying the seed keeps that failure within the library's reproducibility contract. - -The failure is deferred rather than eager because string satisfiability under exclusion is not cheaply decidable in general. A complete declaration-time check is therefore infeasible, and a partial one would diagnose some unsatisfiable specs at declaration and others only at generation — an inconsistent seam worse than a single, predictable rule. Deferring uniformly is the honest choice, and it confines the departure to exclusions alone: every other string constraint stays constructive and eagerly validated. - -Accepting that departure is warranted because the alternatives are worse: leaving the gap keeps the most-used builder the only scalar that cannot exclude and pushes users back to seed-breaking retry loops, while forcing an eager verdict demands a decision procedure the domain does not cheaply admit. The cost — one narrowly-scoped, documented case where a string generator that exists may still fail — is the trade already accepted for distinct collections, and expected collisions are ≈ 0 for any non-trivial shape, so the constructive fast path is preserved in practice. - -The redraw budget, the exception payload, and the seed propagation are implementation, documented in the `JustDummies` code (`StringSpec`) and the JustDummies user documentation — not here. - -## Alternatives Considered - -### Leave `AnyString` without exclusion constraints - -Considered because it preserved the pure constructive rule for scalars and needed no new failure channel. Rejected because it left the most-used builder the only scalar that cannot express exclusion, forcing hand-rolled retry loops that silently break seeding. - -### Decide satisfiability eagerly, as the ordinal builders do - -Considered because declaration-time diagnosis is the library's norm for contradictory constraints. Rejected because string satisfiability under exclusion is not cheaply decidable in general; a partial eager check would be an inconsistent seam, diagnosing some specs early and others late. - -### Redraw without a bound - -Considered because a satisfiable exclusion would eventually succeed. Rejected because an unsatisfiable one would loop forever, violating the no-hidden-unbounded-loops principle. - -### Spec-aware layout avoidance - -Considered because constructing the string to dodge the excluded set would keep exclusion constructive and eager. Rejected as disproportionate: correctly avoiding an arbitrary excluded set across the layout's free positions is complex for a path the redraw takes virtually never; it can be revisited if evidence warrants. - -## Consequences - -### Positive - -* The exclusion pair is now uniform across every scalar builder; the common "different identifier, same shape" need is served, seeded and reproducible. -* The constructive fast path is unchanged for every spec without exclusions, and in practice for exclusions too (collisions ≈ 0). -* An unsatisfiable exclusion fails safely, reproducibly, and names the seed to replay — consistent with ADR-0013. - -### Negative - -* "An `AnyString` that exists can always generate" no longer holds unconditionally: an over-tight exclusion is the one case deferred to generation. -* Failure timing for an unsatisfiable string exclusion differs from the eager, declaration-time diagnosis the ordinal builders give. - -### Risks - -* A poorly tuned budget could fail a theoretically satisfiable but extremely tight shape. Mitigation: keep the budget documented and revise it on evidence, rather than describing failure as impossible (the posture of ADR-0013). -* Users may expect string exclusion to be constructive like the numeric builders. Mitigation: state the redraw and its deferred failure explicitly in the builder documentation and the JustDummies readme. - -## Follow-up Actions - -* Document the redraw and the deferred, seed-bearing failure in the JustDummies readme and the builder documentation (done in the implementing pull request). -* Revisit the budget if real usage reveals false exhaustion. -* Consider spec-aware avoidance only if evidence shows the bounded redraw is inadequate. - -## References - -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) — the sibling bounded-draw-with-deferred-failure channel. -* [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md) — `AnyString.OneOf` stays terminal and does not combine with exclusions. -* [ADR-0020](0020-materialize-dummies-only-through-generate.md) — dummies materialize only through `Generate()`. -* `StringSpec` and `AnyString` in the `JustDummies` project; the JustDummies NuGet readme. -* Issue [#224](https://github.com/Reefact/first-class-errors/issues/224). diff --git a/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.fr.md b/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.fr.md deleted file mode 100644 index 900304cf..00000000 --- a/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.fr.md +++ /dev/null @@ -1,157 +0,0 @@ -# ADR-0035 | Détecter les conflits structurels de Any à la compilation, ceux dépendant de la valeur à l'exécution - -🌍 🇬🇧 [English](0035-enforce-structural-any-conflicts-at-compile-time.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -* Chaque générateur du point d'entrée `Any` de `JustDummies` — et son miroir `AnyContext` — a été jusqu'ici un - **builder plat** : un type unique expose toutes les méthodes de contrainte, les méthodes s'enchaînent dans - n'importe quel ordre, et une combinaison incompatible est signalée à l'**exécution** par une - `ConflictingAnyConstraintException` dont le message nomme les deux côtés (« Cannot apply X because Y is already - defined »). Une spécification qui ne se révèle insatisfiable que pendant la production d'une valeur lève une - `AnyGenerationException`, qui porte la graine. Le système de types n'est jamais utilisé pour empêcher une - combinaison. -* Deux sortes d'incompatibilité surviennent sur cette surface. L'une est **structurelle** : elle vaut pour la - combinaison elle-même, pour toute valeur d'argument — sur `Any.String()`, un second jeu de caractères après un - premier est toujours fautif. L'autre **dépend de la valeur** : le même appel de méthode est licite ou illicite - selon la valeur d'exécution de son argument — `Any.String().Numeric().StartingWith("ORD-")` est en conflit - parce que les lettres du préfixe tombent hors du jeu numérique, tandis que - `Any.String().Numeric().StartingWith("123")` est valide ; le point d'appel et les types statiques sont - identiques dans les deux cas. -* `Any.Uri()` (issue #226) est le premier générateur dont l'espace se partitionne en **formes** structurellement - différentes : une URI web absolue, WebSocket, FTP ou mailto, ou une référence relative. Chaque forme admet un - ensemble de composants différent et fixé par la RFC — un mailto n'a ni port ni autorité (RFC 6068), une URI - WebSocket ni user-info ni fragment (RFC 6455), une URI FTP ni requête ni fragment, une référence relative ni - schéma ni autorité. Quels composants sont licites est fixé par la forme, non par une valeur. -* Une erreur de catégorie entre ces formes — un port sur un mailto, un fragment sur une URI WebSocket — est donc - structurelle au sens ci-dessus, et connue avant qu'aucune valeur ne soit tirée. -* C# sait rendre un membre indisponible sur un type. Un générateur qui retourne un **type différent par forme**, - chacun n'exposant que les composants de sa forme, transforme une erreur de catégorie en du code qui ne compile - pas, là où un unique `AnyUri` plat exposant tous les composants ne pourrait rejeter la même erreur qu'à - l'exécution. -* `JustDummies` est en pré-publication : aucun tag `dum-v*`, aucun consommateur externe, une section *Unreleased* de - changelog vide. La forme de sa surface de générateurs publique peut encore être fixée sans coût de migration. -* Le dépôt consigne sous forme d'ADR les décisions qui façonnent la surface publique `Any` — ADR-0020 - (matérialiser uniquement via `Generate()`), ADR-0031 (nommer les fabriques d'après leur type CLR), ADR-0006 - (une seule source graine). Une règle nouvelle et transverse sur la *manière* dont la surface signale une - combinaison illicite est une décision de cette même classe. - -## Décision - -Une combinaison de contraintes illicite sur la surface `Any` est rendue impossible à écrire à la compilation — -au moyen d'une progression typée qui retourne un builder propre à la forme n'exposant que les membres de cette -forme — lorsque l'illicéité est structurelle, et est sinon laissée au chemin d'exécution -`ConflictingAnyConstraintException` / `AnyGenerationException` lorsqu'elle dépend d'une valeur générée. - -## Justification - -* La ligne de partage est la décidabilité par le compilateur, et elle tombe exactement là où tombent les deux - sortes d'incompatibilité du Contexte. Une erreur structurelle est une propriété de la combinaison, donc le - système de types *peut* la porter ; une erreur dépendant de la valeur est une propriété d'un argument que le - compilateur ne voit jamais, donc le système de types *ne peut pas* la porter et une vérification à l'exécution - est la seule option. La règle suit le grain de ce que chaque point d'application est capable de savoir. -* Appliquer la progression typée au cas dépendant de la valeur n'est pas seulement inutile, c'est impossible : - aucun agencement de types ne distingue `StartingWith("ORD-")` de `StartingWith("123")`, puisqu'ils ne - diffèrent que par une valeur. Le patron plat à l'exécution n'y est donc pas un repli plus faible — c'est le - seul mécanisme capable d'exprimer la contrainte tout court. -* Inversement, laisser une erreur structurelle d'URI à l'exécution jette une garantie disponible gratuitement. - `Mailto().WithPort(...)` est fautif pour tout argument possible ; l'exposer comme une génération en échec, ou - même comme une `ConflictingAnyConstraintException` levée, reporte à l'exécution une erreur que le compilateur - attraperait sinon à la frappe, sans aucun gain. -* Rendre les erreurs de catégorie impossibles à écrire les retire aussi de la surface qu'un lecteur doit - apprendre : un builder propre à la forme qui n'offre jamais `WithPort` ne peut pas être mal employé ainsi, si - bien que la règle RFC « un mailto n'a pas de port » est enseignée par l'API plutôt que par un message - d'exécution. C'est le même raisonnement « rendre la règle impossible à enfreindre plutôt que seulement - vérifiée » que l'ADR-0031 a appliqué au nommage des fabriques. -* Le coût du chemin typé — plusieurs types builder publics pour une famille au lieu d'un seul — est le genre de - décision de surface unique que la fenêtre de pré-publication absorbe sans frais, et il est confiné aux - générateurs dont l'espace se scinde réellement en formes fixes ; le patron plat reste le défaut partout - ailleurs, si bien que la surface ne se fragmente pas builder par builder. - -## Alternatives envisagées - -### Garder chaque générateur plat et signaler tous les conflits à l'exécution - -Envisagée parce que c'est le patron établi de la bibliothèque, qu'elle donne un modèle mental uniforme -(« enchaîner librement, apprendre les conflits par les exceptions ») et qu'elle garde le plus petit nombre de -types publics — un unique `AnyUri` au lieu d'une famille. - -Rejetée parce qu'elle dépense une garantie qu'elle n'a pas à dépenser : une erreur de catégorie comme un port -sur un mailto est connaissable à la compilation, et une surface uniquement d'exécution peut au mieux lever pour -elle une fois que le code compile et tourne déjà. L'uniformité serait préservée au mauvais endroit — faisant se -comporter l'erreur décidable par le compilateur comme celle dépendant de la valeur, alors que seule la seconde -est réellement contrainte à l'exécution. - -### Faire de chaque générateur une progression typée - -Envisagée par symétrie — un seul modèle d'application sur toute la surface `Any` — et parce qu'elle déplacerait -davantage d'erreurs vers la compilation en général. - -Rejetée parce que la plupart des conflits de la surface dépendent de la valeur (préfixes, valeurs contenues, -exclusions, jeu des longueurs), ce qu'aucun agencement de types ne peut décider ; leur imposer des types ne peut -pas fonctionner, et multiplierait soit des types builder sans retirer une seule vérification d'exécution, soit -rétrécirait en silence la surface en deçà de ce que le générateur est censé exprimer. La progression typée ne -mérite son coût que là où un espace se scinde en formes fixes. - -### Appliquer les règles de catégorie d'URI par un analyseur Roslyn au-dessus d'un builder plat - -Envisagée parce que la bibliothèque livre déjà des analyseurs, si bien qu'un diagnostic pourrait signaler -`Mailto().WithPort()` sur un unique `AnyUri` plat tout en gardant un seul type. - -Rejetée parce qu'elle réintroduit, comme vérification externe, un invariant que le système de types peut tenir -intrinsèquement : un analyseur peut être supprimé, accuse un retard sur le compilateur, et doit être documenté et -testé comme sa propre surface, là où un membre absent ne peut tout simplement pas être écrit. Un analyseur est -le bon outil pour une odeur *dépendant de la valeur* que les types ne peuvent pas attraper, pas pour une règle -structurelle qu'ils peuvent porter. - -## Conséquences - -### Positives - -* Les erreurs de catégorie dans un générateur partitionné par forme deviennent des erreurs de compilation : - `Mailto().WithPort(...)` et `WebSocket().WithFragment(...)` ne compilent pas, au lieu d'échouer à l'exécution. -* L'ensemble des composants licites de chaque forme d'URI est enseigné par le builder de cette forme — l'API est - auto-documentée là où elle s'appuyait sur un message d'exécution. -* La règle énonce clairement quel point d'application un nouveau générateur doit employer, indexé sur une - propriété (structurelle vs dépendant de la valeur) qui est déjà la distinction pertinente sur la surface. - -### Négatives - -* La surface `Any` n'est plus à modèle unique : un contributeur doit reconnaître lequel des deux patrons appelle - un nouveau générateur, au lieu de toujours se tourner vers le builder plat. -* Un générateur partitionné par forme porte plusieurs types builder publics au lieu d'un seul, augmentant le - nombre de types et la référence d'API publique pour cette famille. - -### Risques - -* La ligne « structurel vs dépendant de la valeur » peut être mal jugée pour un générateur futur — typer quelque - chose dont les conflits dépendent en fait de la valeur (surface de type morte), ou laisser une scission - réellement structurelle à l'exécution (une garantie de compilation manquée) ; atténué en gardant le patron - plat à l'exécution comme défaut et en réservant la progression typée à un espace qui se scinde - démonstrativement en formes fixes. -* La progression typée pourrait être sur-appliquée par nouveauté, fragmentant la surface ; atténué en consignant - ici qu'elle est l'exception — justifiée par une partition en formes fixes — et non le nouveau défaut. - -## Actions de suivi - -* Aucune requise. `Any.Uri()` (issue #226, première application) réalise déjà le côté progression typée, et la - surface `AnyString` existante réalise déjà le côté exécution ; cet ADR consigne la règle qu'ils établissent - conjointement. -* Appliquer la règle lorsque l'espace d'un générateur futur se scinde en formes fixes ; sinon, garder le patron - plat à l'exécution. - -## Références - -* ADR-0020 — matérialiser les dummies uniquement via `Generate()` ; partage le sujet « forme de la surface - `Any` ». -* ADR-0031 — nommer les fabriques de Any d'après leur type CLR ; précédent du « rendre la règle impossible à - enfreindre plutôt que seulement vérifiée », et de la consignation des décisions de surface `Any` comme ADR. -* ADR-0006 — fournir les valeurs arbitraires depuis une seule source graine ; la graine portée par - `AnyGenerationException` sur le chemin d'exécution. -* PR #295 — ajouter la famille `Any.Uri()`, la première progression typée. -* Issue #226 — le backlog Nice-to-Have de JustDummies qui a motivé `Any.Uri()`. diff --git a/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.md b/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.md deleted file mode 100644 index ff53affc..00000000 --- a/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.md +++ /dev/null @@ -1,147 +0,0 @@ -# ADR-0035 | Enforce structural Any conflicts at compile time, value-dependent ones at run time - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0035-enforce-structural-any-conflicts-at-compile-time.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -* Every generator on `JustDummies`' `Any` entry point — and its `AnyContext` mirror — has until now been a - **flat builder**: a single type exposes every constraint method, the methods chain in any order, and an - incompatible combination is reported at **run time** by a `ConflictingAnyConstraintException` whose message - names both sides ("Cannot apply X because Y is already defined"). A spec that only proves unsatisfiable while - a value is being produced throws `AnyGenerationException`, which carries the seed. The type system is never - used to prevent a combination. -* Two different kinds of incompatibility occur on that surface. One is **structural**: it holds for the - combination itself, for every argument value — on `Any.String()`, a second character set after a first is - always wrong. The other is **value-dependent**: the same method call is legal or illegal according to its - argument's run-time value — `Any.String().Numeric().StartingWith("ORD-")` conflicts because the prefix's - letters fall outside the numeric set, while `Any.String().Numeric().StartingWith("123")` is valid; the call - site and the static types are identical in both. -* `Any.Uri()` (issue #226) is the first generator whose space is partitioned into structurally different - **shapes**: an absolute web, WebSocket, FTP or mailto URI, or a relative reference. Each shape admits a - different, RFC-fixed set of components — a mailto has no port or authority (RFC 6068), a WebSocket URI no - user-info or fragment (RFC 6455), an FTP URI no query or fragment, a relative reference no scheme or - authority. Which components are legal is fixed by the shape, not by any value. -* A category error across those shapes — a port on a mailto, a fragment on a WebSocket URI — is therefore - structural in the sense above, and known before any value is drawn. -* C# can make a member unavailable on a type. A generator that returns a **different type per shape**, each - exposing only that shape's components, turns a category error into code that does not compile, whereas a - single flat `AnyUri` exposing every component could only reject the same error at run time. -* `JustDummies` is pre-release: no `dum-v*` tag, no external consumers, an empty *Unreleased* changelog. The shape - of its public generator surface can still be set at no migration cost. -* The repository records decisions that shape the `Any` public surface as ADRs — ADR-0020 (materialize only - through `Generate()`), ADR-0031 (name factories after their CLR type), ADR-0006 (a single seeded source). A - new, cross-cutting rule for *how* the surface reports an illegal combination is a decision of that same class. - -## Decision - -An illegal constraint combination on the `Any` surface is made unrepresentable at compile time — through a -typed progression that returns a shape-specific builder exposing only that shape's members — when the -illegality is structural, and is otherwise left to the run-time `ConflictingAnyConstraintException` / -`AnyGenerationException` path when it depends on a generated value. - -## Rationale - -* The dividing line is decidability by the compiler, and it falls exactly where the two kinds of incompatibility - from Context already fall. A structural error is a property of the combination, so the type system *can* carry - it; a value-dependent error is a property of an argument the compiler never sees, so the type system *cannot* - carry it and a run-time check is the only option. The rule follows the grain of what each enforcement point is - able to know. -* Applying typed progression to the value-dependent case is not merely unhelpful, it is impossible: no - arrangement of types tells `StartingWith("ORD-")` from `StartingWith("123")`, because they differ only in a - value. The flat, run-time pattern is therefore not a weaker fallback there — it is the only mechanism that can - express the constraint at all. -* Conversely, leaving a structural URI error to run time throws away a guarantee that is freely available. - `Mailto().WithPort(...)` is wrong for every possible argument; surfacing it as a failed generation, or even as - a thrown `ConflictingAnyConstraintException`, defers to run time an error the compiler would otherwise catch at - the keystroke, for no gain. -* Making category errors unrepresentable also removes them from the surface a reader must learn: a shape-specific - builder that never offers `WithPort` cannot be misused that way, so the RFC rule "a mailto has no port" is - taught by the API rather than by a run-time message. This is the same "make the rule un-break-able rather than - merely checked" reasoning ADR-0031 applied to factory naming. -* The cost of the typed path — several public builder types for a family instead of one — is the kind of - one-time surface decision the pre-release window absorbs for free, and it is confined to generators whose space - genuinely splits into fixed shapes; the flat pattern stays the default everywhere else, so the surface does not - fragment builder by builder. - -## Alternatives Considered - -### Keep every generator flat and report all conflicts at run time - -Considered because it is the library's established pattern, gives one uniform mental model ("chain freely, learn -the conflicts from exceptions"), and keeps the smallest public type count — a single `AnyUri` instead of a -family. - -Rejected because it spends a guarantee it need not spend: a category error such as a port on a mailto is knowable -at compile time, and a run-time-only surface can at best throw for it after the code already builds and runs. -Uniformity would be preserved in the wrong place — making the compiler-decidable error behave like the -value-dependent one, when only the latter is genuinely forced to run time. - -### Make every generator a typed progression - -Considered for symmetry — one enforcement model across the whole `Any` surface — and because it would move more -errors to compile time in general. - -Rejected because most conflicts on the surface are value-dependent (prefixes, contained values, exclusions, -length interplay), which no type arrangement can decide; forcing types onto them cannot work, and would either -multiply builder types without removing a single run-time check or quietly narrow the surface below what the -generator is meant to express. Typed progression earns its cost only where a space splits into fixed shapes. - -### Enforce the URI category rules with a Roslyn analyzer over a flat builder - -Considered because the library already ships analyzers, so a diagnostic could flag `Mailto().WithPort()` on a -single flat `AnyUri` while keeping one type. - -Rejected because it reintroduces, as an external check, an invariant the type system can hold intrinsically: an -analyzer can be suppressed, lags the compiler, and must be documented and tested as its own surface, whereas an -absent member simply cannot be written. An analyzer is the right tool for a *value-dependent* smell the types -cannot catch, not for a structural rule they can. - -## Consequences - -### Positive - -* Category errors in a shape-partitioned generator become compile-time errors: `Mailto().WithPort(...)` and - `WebSocket().WithFragment(...)` do not build, rather than failing when run. -* The legal component set of each URI shape is taught by that shape's own builder — the API is self-documenting - where it used to rely on a run-time message. -* The rule states cleanly which enforcement point a new generator should use, keyed on a property (structural - vs value-dependent) that is already the meaningful distinction on the surface. - -### Negative - -* The `Any` surface is no longer single-model: a contributor must recognise which of the two patterns a new - generator calls for, instead of always reaching for the flat builder. -* A shape-partitioned generator carries several public builder types instead of one, enlarging the type count - and the public-API baseline for that family. - -### Risks - -* The "structural vs value-dependent" line can be misjudged for a future generator — typing something whose - conflicts are actually value-dependent (dead type surface), or leaving a genuinely structural split to run - time (a missed compile-time guarantee); mitigated by keeping the flat, run-time pattern the default and - reserving typed progression for a space that demonstrably splits into fixed shapes. -* Typed progression could be over-applied for its novelty, fragmenting the surface; mitigated by recording here - that it is the exception — justified by a fixed-shape partition — not the new default. - -## Follow-up Actions - -* None required. `Any.Uri()` (issue #226, first application) already realises the typed-progression side, and - the existing `AnyString` surface already realises the run-time side; this ADR records the rule they jointly - establish. -* Apply the rule when a future generator's space splits into fixed shapes; otherwise keep the flat, run-time - pattern. - -## References - -* ADR-0020 — materialize dummies only through `Generate()`; shares the "shape of the `Any` surface" subject. -* ADR-0031 — name Any's factories after their CLR type; precedent for "make the rule un-break-able rather than - merely checked", and for recording `Any`-surface decisions as ADRs. -* ADR-0006 — supply arbitrary values from a single seedable source; the seed carried by `AnyGenerationException` - on the run-time path. -* PR #295 — add the `Any.Uri()` family, the first typed progression. -* Issue #226 — the JustDummies Nice-to-Have backlog that prompted `Any.Uri()`. diff --git a/doc/handwritten/for-maintainers/adr/0036-draw-lattice-constrained-scalars-on-the-grid.fr.md b/doc/handwritten/for-maintainers/adr/0036-draw-lattice-constrained-scalars-on-the-grid.fr.md deleted file mode 100644 index b2acb55d..00000000 --- a/doc/handwritten/for-maintainers/adr/0036-draw-lattice-constrained-scalars-on-the-grid.fr.md +++ /dev/null @@ -1,85 +0,0 @@ -# ADR-0036 | Tirer les scalaires contraints à un réseau sur la grille - -🌍 🇬🇧 [English](0036-draw-lattice-constrained-scalars-on-the-grid.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -JustDummies construit un scalaire directement pour satisfaire ses contraintes — jamais généré-puis-filtré —, détecte les contradictions au moment de la déclaration, et évite les boucles de nouvelles tentatives cachées et non bornées. Un générateur scalaire qui existe peut toujours générer, en un seul tirage. Les types à projection ordinale (les entiers, les temporels) tirent le k-ième ordinal non exclu du domaine en une passe sur un espace ordinal affine et préservant l'ordre ; le `decimal` tire un candidat et le décale dans un budget borné. - -Un besoin récurrent est celui d'une valeur qui doit se situer sur une grille régulière : un multiple d'une unité (un montant en centimes entiers, une quantité à la douzaine), un `decimal` exprimable en un nombre fixe de décimales (un montant monétaire), ou un instant rond (une seconde pleine, un quart d'heure, un jour plein). Ce sont des invariants du code testé — un value object ou une précondition de contrat que la valeur doit respecter —, non ce que le test vérifie. - -Aujourd'hui, une telle valeur n'est atteignable qu'en projetant après coup une valeur contrainte, `As(x => x * k)`. La projection déforme la portée déclarée (une portée exprimée dans l'unité d'avant projection ne veut plus dire ce qu'elle affirme) et fait sortir la valeur de l'algèbre de contraintes : le générateur projeté ne peut plus exclure de valeurs, ne peut plus détecter de conflit, et ne porte aucun indice de cardinalité pour les collections distinctes. Les dummies temporels à précision de tick surprennent en outre les tests qui sérialisent via un format à la seconde ou au jour, où l'aller-retour perd silencieusement la précision. - -Parce que la projection ordinale est affine, les multiples d'un pas forment une progression arithmétique dans l'espace ordinal ; une grille est donc exprimable comme une dimension à part entière des moteurs d'intervalle sans quitter le modèle constructif. Les types à virgule flottante binaire n'ont pas de grille décimale (ni rationnelle générale) exacte — `0.1` n'y est pas représentable —, de sorte que la même construction ne peut y tenir. L'issue #226 recense `MultipleOf`/`WithScale` comme un ajout piloté par la demande ; le besoin de granularité temporelle y a été noté en parallèle. - -## Décision - -Une contrainte de réseau — `MultipleOf` sur les entiers, `WithScale` sur le `decimal`, `WithGranularity` sur les temporels — restreint un scalaire à une grille régulière tirée de manière constructive en une passe, se compose avec les bornes, exclusions et listes d'autorisation existantes, se déclare une seule fois par générateur, et est délibérément refusée aux types à virgule flottante binaire. - -## Justification - -Tirer sur la grille préserve l'invariant du tirage unique et sans nouvelle tentative : la projection ordinale affine fait des multiples d'un pas une progression arithmétique, de sorte que la grille devient une dimension de plus que le moteur d'intervalle échantillonne directement, plutôt qu'un post-filtre qui réintroduirait du rejet. Garder la valeur de première classe — au lieu d'une projection `As` — est tout l'enjeu : la portée déclarée conserve son sens, et les exclusions, les listes d'autorisation, la détection de conflit au moment de la déclaration et l'indice de cardinalité continuent de s'appliquer, si bien qu'une collection distincte sur une grille étroite échoue toujours par anticipation. - -`WithScale` est un réseau de *valeurs* — un multiple de `10⁻ⁿ` —, non un contrat de représentation qui compléterait les zéros de fin, car l'invariant dont les appelants ont réellement besoin est « une valeur que le domaine accepte » (une fabrique monétaire qui refuse une troisième décimale), ce qui est un fait sur la valeur, non sur le rendu. Une garantie de représentation ne se composerait pas avec l'égalité de valeurs et surprendrait quiconque compare `12.30` et `12.3`. - -Le réseau est refusé aux flottants binaires parce qu'une grille décimale n'y est pas exactement représentable ; l'offrir rendrait des valeurs hors grille sous une promesse que le type ne peut tenir. Il se déclare une seule fois — un second réseau différent entre en conflit plutôt que de s'intersecter silencieusement —, à l'image de la règle « déclaré une seule fois » qu'utilise déjà la liste d'autorisation, et cela épargne une combinaison par plus petit commun multiple que la demande ne justifie pas. Exposer une seule capacité du moteur comme `MultipleOf` sur les entiers et `WithGranularity` sur les temporels est ce qui permet à une seule dimension de servir les deux familles, de sorte qu'un correctif de la logique de grille atteint tous les types d'un coup. - -L'arithmétique du pas, le calage-et-décalage décimal et la formulation des messages de conflit relèvent de l'implémentation, documentée dans le code `JustDummies` (`OrdinalIntervalSpec`, `WideIntervalSpec`, `DecimalIntervalSpec`) et dans la documentation utilisateur de JustDummies — pas ici. - -## Alternatives envisagées - -### Conserver la projection `As(x => x * k)` comme seul moyen - -Envisagée parce qu'elle ne demande aucune nouvelle API et fonctionne déjà. Rejetée parce qu'elle déforme la portée déclarée, fait sortir la valeur de l'algèbre de contraintes (pas d'exclusion, pas de détection de conflit, pas d'indice de cardinalité) et — pour la précision temporelle — ne traite pas du tout la surprise de sérialisation. - -### Générer puis filtrer les tirages hors grille - -Envisagée parce que c'est la manière évidente d'honorer une grille arbitraire. Rejetée parce qu'elle réintroduit une boucle de nouvelles tentatives non bornée, en contradiction avec le modèle constructif et sans boucle cachée sur lequel la bibliothèque est bâtie. - -### Étendre le réseau aux types à virgule flottante binaire - -Envisagée pour la symétrie de surface avec les entiers et le `decimal`. Rejetée parce qu'une grille décimale (ou rationnelle générale) n'est pas exactement représentable en virgule flottante binaire ; la contrainte rendrait donc des valeurs hors grille — une fausse promesse pire qu'un manque délibéré et documenté. - -### Faire de `WithScale` un contrat de représentation - -Envisagée parce que le nom évoque `decimal.Scale` et une colonne de base de données `DECIMAL(p, s)`. Rejetée parce que l'invariant dont les appelants ont besoin est au niveau de la valeur, qu'une garantie de représentation ne se compose pas avec l'égalité de valeurs, et qu'elle surprendrait sur `12.30 == 12.3`. - -### Combiner les réseaux répétés par plus petit commun multiple - -Envisagée parce que « multiple de 4 et de 6 » est mathématiquement « multiple de 12 », non une contradiction. Rejetée comme disproportionnée : elle ouvre un cas limite propice au dépassement pour une combinaison que la demande ne montre pas, alors que « déclaré une seule fois » est simple, sûr et cohérent avec la liste d'autorisation. - -## Conséquences - -### Positives - -* L'invariant « valeur sur une grille » est exprimable de manière constructive : la portée déclarée reste honnête et la valeur conserve toute sa composition — bornes, exclusions, liste d'autorisation, conflit par anticipation, et l'indice de cardinalité qui laisse une collection distincte sur une grille étroite échouer par anticipation. -* Une seule capacité du moteur sert les entiers et les temporels (et le `decimal` par son propre moteur), de sorte qu'un correctif de la logique de grille atteint tous les types d'un coup. -* Le contournement `As(x => x * k)` et la surprise de sérialisation à la précision de tick disparaissent tous deux pour les types couverts. - -### Négatives - -* Une nouvelle dimension commutative vit désormais dans trois moteurs d'intervalle (ordinal, large, décimal) et doit y être maintenue de concert. -* La surface est délibérément asymétrique : les flottants binaires portent le vocabulaire de signe et de bornes mais aucun réseau — un manque que les utilisateurs doivent apprendre plutôt que déduire. - -### Risques - -* La distinction valeur-contre-représentation de `WithScale` peut surprendre les utilisateurs qui attendent une échelle complétée. Atténuation : l'énoncer comme un réseau de valeurs dans la documentation du builder et le readme. -* La grille décimale tire-et-cale au lieu d'énumérer, si bien que la masse sur les deux points de grille extrêmes est approximative. Atténuation : l'atteignabilité des deux bornes est préservée et testée, en cohérence avec le tirage décimal existant. - -## Actions de suivi - -* Documenter `MultipleOf`/`WithScale`/`WithGranularity` dans le readme de JustDummies et la documentation des builders (fait dans la pull request d'implémentation). -* N'ajouter le sucre temporel `WholeSeconds()`/`WholeDays()` que si la demande apparaît ; le `WithGranularity(TimeSpan)` général le couvre en attendant. -* Ne revisiter la combinaison par plus petit commun multiple des réseaux répétés que si l'usage réel montre que la règle « déclaré une seule fois » est trop stricte. - -## Références - -* Issue [#226](https://github.com/Reefact/first-class-errors/issues/226) — le backlog piloté par la demande qui recense `MultipleOf`/`WithScale` et la granularité temporelle. -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) — l'indice de cardinalité qu'un réseau alimente, et le frère du tirage borné. -* [ADR-0020](0020-materialize-dummies-only-through-generate.md) — les dummies ne se matérialisent qu'à travers `Generate()`. -* `OrdinalIntervalSpec`, `WideIntervalSpec`, `DecimalIntervalSpec` et les builders concernés dans le projet `JustDummies` ; le readme NuGet de JustDummies. diff --git a/doc/handwritten/for-maintainers/adr/0036-draw-lattice-constrained-scalars-on-the-grid.md b/doc/handwritten/for-maintainers/adr/0036-draw-lattice-constrained-scalars-on-the-grid.md deleted file mode 100644 index 04fdbde5..00000000 --- a/doc/handwritten/for-maintainers/adr/0036-draw-lattice-constrained-scalars-on-the-grid.md +++ /dev/null @@ -1,85 +0,0 @@ -# ADR-0036 | Draw lattice-constrained scalars on the grid - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0036-draw-lattice-constrained-scalars-on-the-grid.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -JustDummies builds a scalar directly to satisfy its constraints — never generated-then-filtered — detects contradictions eagerly at declaration, and avoids hidden unbounded retry loops. A scalar generator that exists can always generate, in one draw. The ordinal-mapped types (the integers, the temporals) draw the k-th non-excluded value of the domain in one pass over an order-preserving, affine ordinal space; `decimal` draws a candidate and nudges it within a bounded budget. - -A recurring dummy need is a value that must lie on a regular grid: a multiple of a unit (an amount in whole cents, a quantity in dozens), a `decimal` expressible in a fixed number of places (a currency amount), or a round instant (a whole second, a quarter-hour, a whole day). These are invariants of the code under test — a value object or contract precondition the value must satisfy — not what the test asserts. - -Today such a value can only be reached by projecting a constrained one after the fact, `As(x => x * k)`. The projection distorts the declared range (a range stated in the pre-projection unit no longer means what it says) and drops the value out of the constraint algebra: the projected generator can no longer exclude values, cannot conflict-check, and carries no cardinality hint for distinct collections. Tick-precision temporal dummies additionally surprise tests that serialize through a second- or day-granular format, where the round-trip silently drops precision. - -Because the ordinal map is affine, the multiples of a step form an arithmetic progression in ordinal space, so a grid is expressible as a first-class dimension of the interval engines without leaving the constructive model. Binary floating-point types have no exact base-ten (or general rational) grid — `0.1` is not representable — so the same construction cannot hold there. Issue #226 records `MultipleOf`/`WithScale` as a demand-driven addition; the temporal granularity need was noted alongside it. - -## Decision - -A lattice constraint — `MultipleOf` on the integers, `WithScale` on `decimal`, `WithGranularity` on the temporals — restricts a scalar to a regular grid drawn constructively in one pass, composes with the existing bounds, exclusions and allow-list, is declared once per generator, and is deliberately withheld from the binary floating-point types. - -## Rationale - -Drawing on the grid keeps the library's single-draw, no-retry invariant: the affine ordinal map makes the multiples of a step an arithmetic progression, so the grid becomes another dimension the interval engine samples directly rather than a post-filter that would reintroduce rejection. Keeping the value first-class — rather than an `As` projection — is the whole point: the declared range keeps its meaning, and exclusions, allow-lists, eager conflict detection and the cardinality hint continue to apply, so a distinct collection over a narrow grid still fails eagerly. - -`WithScale` is a *value* lattice — a multiple of `10⁻ⁿ` — not a representation contract that would pad trailing zeros, because the invariant callers actually need is "a value the domain accepts" (a money factory that rejects a third decimal place), which is a fact about value, not rendering. A representation guarantee would not compose with value equality and would surprise anyone comparing `12.30` with `12.3`. - -The lattice is withheld from the binary floats because a base-ten grid is not exactly representable there; offering it would hand back off-grid values under a promise the type cannot keep. It is declared once — a second, different grid conflicts rather than silently intersecting — mirroring the "declared once" rule the allow-list already uses and sparing a least-common-multiple combination the demand does not justify. Surfacing one engine capability as `MultipleOf` on integers and `WithGranularity` on temporals is what lets a single dimension serve both families, so a fix to the grid logic reaches every type at once. - -The step arithmetic, the decimal snap-and-nudge, and the conflict-message wording are implementation, documented in the `JustDummies` code (`OrdinalIntervalSpec`, `WideIntervalSpec`, `DecimalIntervalSpec`) and the JustDummies user documentation — not here. - -## Alternatives Considered - -### Keep the `As(x => x * k)` projection as the only way - -Considered because it needs no new API and already works. Rejected because it distorts the declared range, drops the value out of the constraint algebra (no exclusion, no conflict check, no cardinality hint), and — for temporal precision — does not address the serialization surprise at all. - -### Generate then filter off-grid draws - -Considered because it is the obvious way to honour an arbitrary grid. Rejected because it reintroduces an unbounded retry loop, contradicting the constructive, no-hidden-loops model the library is built on. - -### Extend the lattice to the binary floating-point types - -Considered for surface symmetry with the integers and `decimal`. Rejected because a base-ten (or general rational) grid is not exactly representable in binary floating point, so the constraint would return off-grid values — a false promise worse than a deliberate, documented gap. - -### Make `WithScale` a representation contract - -Considered because the name evokes `decimal.Scale` and a database `DECIMAL(p, s)` column. Rejected because the invariant callers need is value-level, a representation guarantee does not compose with value equality, and it would surprise on `12.30 == 12.3`. - -### Combine repeated lattices by least common multiple - -Considered because "multiple of 4 and of 6" is mathematically "multiple of 12", not a contradiction. Rejected as disproportionate: it opens an overflow-prone corner for a combination the demand does not show, whereas "declared once" is simple, safe, and consistent with the allow-list. - -## Consequences - -### Positive - -* The "value on a grid" invariant is expressible constructively, so the declared range stays honest and the value keeps full composition — bounds, exclusions, allow-list, eager conflict, and the cardinality hint that lets a distinct collection over a narrow grid fail eagerly. -* One engine capability serves the integers and the temporals (and `decimal` through its own engine), so a fix to the grid logic reaches every type at once. -* The `As(x => x * k)` workaround and the tick-precision serialization surprise both go away for the covered types. - -### Negative - -* A new commutative dimension now lives in three interval engines (ordinal, wide, decimal) and must be maintained in step across them. -* The surface is deliberately asymmetric: the binary floats carry the sign and bound vocabulary but no lattice — a gap users must learn rather than infer. - -### Risks - -* `WithScale`'s value-versus-representation distinction may surprise users expecting a padded scale. Mitigation: state it as a value lattice in the builder documentation and the readme. -* The decimal grid draws-and-snaps rather than enumerating, so the mass at the two extreme grid points is approximate. Mitigation: reachability of both bounds is preserved and tested, consistent with the existing decimal draw. - -## Follow-up Actions - -* Document `MultipleOf`/`WithScale`/`WithGranularity` in the JustDummies readme and the builder documentation (done in the implementing pull request). -* Ship the `WholeSeconds()`/`WholeDays()` temporal sugar only if demand appears; the general `WithGranularity(TimeSpan)` covers it in the meantime. -* Revisit least-common-multiple combination of repeated lattices only if real usage shows the "declared once" rule is too strict. - -## References - -* Issue [#226](https://github.com/Reefact/first-class-errors/issues/226) — the demand-driven backlog that lists `MultipleOf`/`WithScale` and temporal granularity. -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) — the cardinality hint a lattice feeds, and the bounded-draw sibling. -* [ADR-0020](0020-materialize-dummies-only-through-generate.md) — dummies materialize only through `Generate()`. -* `OrdinalIntervalSpec`, `WideIntervalSpec`, `DecimalIntervalSpec` and the affected builders in the `JustDummies` project; the JustDummies NuGet readme. diff --git a/doc/handwritten/for-maintainers/adr/0037-vary-the-datetimeoffset-offset-dimension.fr.md b/doc/handwritten/for-maintainers/adr/0037-vary-the-datetimeoffset-offset-dimension.fr.md deleted file mode 100644 index 68d2eb55..00000000 --- a/doc/handwritten/for-maintainers/adr/0037-vary-the-datetimeoffset-offset-dimension.fr.md +++ /dev/null @@ -1,75 +0,0 @@ -# ADR-0037 | Faire varier la dimension d'offset de DateTimeOffset - -🌍 🇬🇧 [English](0037-vary-the-datetimeoffset-offset-dimension.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Superseded par l'[ADR-0051](0051-filter-the-datetimeoffset-pool-by-the-declared-offset.fr.md) -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -Un `DateTimeOffset` porte deux dimensions : l'instant (son `UtcTicks`) et le décalage (offset) par rapport à UTC. Le décalage est la raison d'être du type face à un simple `DateTime`. `AnyDateTimeOffset` ne fait varier que l'instant et fixe le décalage à `TimeSpan.Zero`, une limitation que ses propres remarks documentent. Le code dont le comportement dépend du décalage — rendu local, arithmétique de décalage, égalité « même instant, décalage différent » — ne peut donc pas obtenir de JustDummies un décalage varié mais valide, et le bug latent courant « le code suppose un décalage nul » n'est jamais révélé par une valeur dummy. - -`DateTimeOffset` contraint son décalage à un nombre entier de minutes dans ±14:00, et exige que les ticks locaux (`UtcTicks + offset`) restent dans la plage `DateTime` ; aux extrêmes du domaine, tout décalage n'est pas valide pour un instant donné. JustDummies construit une valeur de manière constructive pour satisfaire ses contraintes, détecte les contradictions au moment de la déclaration, et ne retente jamais. La comparaison se fait par instant, et `OneOf` renvoie déjà les valeurs fournies telles quelles, décalage compris, car reconstruire à partir du seul instant normaliserait le décalage. L'issue #226 recense un tirage de décalage borné comme un ajout piloté par la demande ; l'issue #297 en assure le suivi. - -## Décision - -`AnyDateTimeOffset` acquiert une dimension de décalage optionnelle — `WithOffset` épingle un décalage en minutes entières et `WithOffsetBetween` en tire un borné —, tandis que le défaut non contraint reste `TimeSpan.Zero`, et l'instant est resserré à la déclaration de sorte que tout décalage admis produise une valeur valide. - -## Justification - -Atteindre le décalage fait de `AnyDateTimeOffset` un générateur fidèle à son propre type et révèle la classe de bugs « suppose un décalage UTC » qu'un générateur épinglé à zéro masque. Le garder optionnel — le défaut reste `TimeSpan.Zero` — rend l'ajout non cassant : les tests qui s'appuient aujourd'hui sur un décalage nul, ou qui sérialisent en `+00:00`, continuent de fonctionner. - -Resserrer l'instant à la déclaration, plutôt que de caler ou rejeter le décalage à chaque tirage, est ce qui préserve le modèle constructif, en un seul tirage et sans nouvelle tentative : dès que la fenêtre d'instant admet tous les décalages de la plage demandée, le décalage devient un tirage indépendant qui ne peut jamais produire une valeur hors plage. Cela réutilise aussi le resserrement de bornes du moteur d'intervalle, si bien qu'une fenêtre d'instant sans place pour le décalage demandé entre en conflit par anticipation en nommant les deux côtés — exactement comme toute autre contrainte. Offrir un épinglage et un tirage borné reprend l'idiome pin/`Between` déjà présent dans la bibliothèque, et la règle des minutes entières dans ±14:00 reprend celle de `DateTimeOffset`. `OneOf` continue de renvoyer ses valeurs telles quelles car c'est une énumération terminale de valeurs exactes, de sorte que la dimension de décalage ne régit que le tirage construit. - -L'arithmétique du décalage, les bornes de resserrement de l'instant et le tirage relèvent de l'implémentation, documentée dans le code `AnyDateTimeOffset` et dans la documentation utilisateur de JustDummies — pas ici. - -## Alternatives envisagées - -### Faire varier le décalage par défaut - -Envisagée parce que « n'importe quel `DateTimeOffset` valide » inclut sans doute n'importe quel décalage, faisant de l'épinglage actuel à zéro le choix le moins fidèle. Rejetée parce que c'est un changement de comportement cassant : les tests qui vérifient `Offset == TimeSpan.Zero`, ou qui sérialisent en `+00:00`, casseraient. L'option optionnelle livre la capacité de façon additive ; faire varier par défaut ne pourra être revu que dans une future version majeure. - -### Caler ou rejeter le décalage à chaque tirage près des bords - -Envisagée parce qu'elle laisse le domaine de l'instant intact. Rejetée parce qu'elle réintroduit soit un échec conditionnel à chaque tirage (contre le modèle sans nouvelle tentative), soit un rétrécissement silencieux du décalage difficile à raisonner. Resserrer l'instant une seule fois, en amont, est plus simple et toujours valide. - -### Ne livrer que `WithOffset` (épinglage), sans tirage borné - -Envisagée comme surface minimale. Rejetée parce que le cas d'usage moteur — exercer une logique sensible au décalage sur une plage de décalages — est précisément le tirage borné ; un épinglage seul ne le sert pas. - -### Laisser le manque - -Envisagée parce que la plupart du code traite un `DateTimeOffset` comme un instant. Rejetée parce qu'elle laisse `AnyDateTimeOffset` un générateur infidèle dont le décalage ne varie jamais, et pousse quiconque a besoin d'un décalage varié vers une construction faite à la main qui ignore généralement la graine. - -## Conséquences - -### Positives - -* Le code sensible au décalage devient exerçable, et le bug latent « suppose un décalage UTC » attrapable, avec une valeur qui reste valide par construction. -* L'ajout est non cassant : le défaut non contraint est inchangé. -* Une combinaison instant/décalage impossible est diagnostiquée par anticipation via le moteur existant, en nommant les deux contraintes. - -### Négatives - -* `AnyDateTimeOffset` porte désormais une seconde dimension et son propre état de décalage propagé à travers chaque transformation. -* La dimension de décalage est spécifique à `DateTimeOffset` — les autres générateurs temporels n'ont pas de décalage — une spécificité délibérée plutôt qu'une surface uniforme. - -### Risques - -* Un décalage épinglé près du bord du domaine resserre la fenêtre d'instant atteignable ; un utilisateur pourrait lire le conflit *eager* qui en résulte comme fallacieux. Atténuation : le conflit nomme les deux contraintes, et le comportement est documenté. -* `WithOffset` combiné à `OneOf` ne remplace pas le décalage propre d'une valeur `OneOf`. Atténuation : documenté, et cohérent avec la sémantique d'énumération terminale de `OneOf`. - -## Actions de suivi - -* Documenter `WithOffset`/`WithOffsetBetween` dans le readme de JustDummies et la documentation des builders (fait dans la pull request d'implémentation). -* N'envisager un raccourci pour « n'importe quel décalage valide » que si `WithOffsetBetween(-14h, +14h)` s'avère une friction en pratique. -* Ne revisiter la variation du décalage par défaut que dans une future version majeure. - -## Références - -* Issue [#297](https://github.com/Reefact/first-class-errors/issues/297) — l'issue dédiée à cette fonctionnalité. -* Issue [#226](https://github.com/Reefact/first-class-errors/issues/226) — le backlog Nice-to-Have dont elle a été détachée. -* [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md) — la sémantique d'énumération terminale que suit `OneOf`. -* `AnyDateTimeOffset` dans le projet `JustDummies` ; le readme NuGet de JustDummies. diff --git a/doc/handwritten/for-maintainers/adr/0037-vary-the-datetimeoffset-offset-dimension.md b/doc/handwritten/for-maintainers/adr/0037-vary-the-datetimeoffset-offset-dimension.md deleted file mode 100644 index a2ca9907..00000000 --- a/doc/handwritten/for-maintainers/adr/0037-vary-the-datetimeoffset-offset-dimension.md +++ /dev/null @@ -1,75 +0,0 @@ -# ADR-0037 | Vary the DateTimeOffset offset dimension - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0037-vary-the-datetimeoffset-offset-dimension.fr.md) - -**Status:** Superseded by [ADR-0051](0051-filter-the-datetimeoffset-pool-by-the-declared-offset.md) -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -A `DateTimeOffset` carries two dimensions: the instant (its `UtcTicks`) and the offset from UTC. The offset is the reason the type exists rather than a plain `DateTime`. `AnyDateTimeOffset` varies only the instant and pins the offset to `TimeSpan.Zero`, a limitation its own remarks document. Code whose behaviour depends on the offset — local rendering, offset arithmetic, "same instant, different offset" equality — therefore cannot obtain a varied-but-valid offset from JustDummies, and the common latent bug "the code assumes the offset is zero" is never surfaced by a dummy value. - -`DateTimeOffset` constrains its offset to a whole number of minutes within ±14:00, and requires that the local ticks (`UtcTicks + offset`) stay inside the `DateTime` range; near the extremes of the domain, not every offset is valid for a given instant. JustDummies builds a value constructively to satisfy its constraints, detects contradictions eagerly at declaration, and never retries. Comparison is by instant, and `OneOf` already returns the supplied values verbatim, offset included, because rebuilding from the instant alone would normalise the offset away. Issue #226 records a bounded offset draw as a demand-driven addition; issue #297 tracks it. - -## Decision - -`AnyDateTimeOffset` gains an opt-in offset dimension — `WithOffset` pins a whole-minute offset and `WithOffsetBetween` draws a bounded one — while the unconstrained default stays `TimeSpan.Zero`, and the instant is tightened at declaration so that every admitted offset yields a valid value. - -## Rationale - -Reaching the offset makes `AnyDateTimeOffset` a faithful generator of its own type and surfaces the "assumes UTC offset" bug class that a zero-pinned generator hides. Keeping it opt-in — the default stays `TimeSpan.Zero` — makes the addition non-breaking: tests that today rely on a zero offset, or serialise to `+00:00`, keep working. - -Tightening the instant at declaration, rather than clamping or rejecting the offset per draw, is what keeps the constructive, one-draw, no-retry model: once the instant window admits every offset in the requested range, the offset is an independent draw that can never produce an out-of-range value. It also reuses the interval engine's bound tightening, so an instant window with no room for the requested offset conflicts eagerly and names both sides — exactly as every other constraint does. Offering a pin and a bounded draw mirrors the library's existing pin/`Between` idiom, and the whole-minute ±14:00 rule mirrors `DateTimeOffset`'s own. `OneOf` keeps returning its values verbatim because it is a terminal enumeration of exact values, so the offset dimension governs only the constructed draw. - -The offset arithmetic, the instant-tightening bounds, and the draw are implementation, documented in the `AnyDateTimeOffset` code and the JustDummies user documentation — not here. - -## Alternatives Considered - -### Vary the offset by default - -Considered because "any valid `DateTimeOffset`" arguably includes any offset, making the current zero-pin the less faithful choice. Rejected because it is a behavioural breaking change: tests asserting `Offset == TimeSpan.Zero`, or serialising to a `+00:00` rendering, would break. Opt-in delivers the capability additively; varying by default can be revisited only under a future major version. - -### Clamp or reject the offset per draw near the edges - -Considered because it leaves the instant domain untouched. Rejected because it either reintroduces a per-draw conditional failure (against the no-retry model) or silently narrows the offset in a way that is hard to reason about. Tightening the instant once, up front, is simpler and always valid. - -### Ship only `WithOffset` (pin), no bounded draw - -Considered as the minimal surface. Rejected because the motivating use case — exercising offset-sensitive logic across a range of offsets — is exactly the bounded draw; a pin alone does not serve it. - -### Leave the gap - -Considered because most code treats a `DateTimeOffset` as an instant. Rejected because it keeps `AnyDateTimeOffset` an unfaithful generator whose offset never varies, and pushes anyone who needs a varied offset to a hand-rolled construction that typically ignores the seed. - -## Consequences - -### Positive - -* Offset-sensitive code becomes exercisable, and the "assumes UTC offset" latent bug is catchable, with a value that stays valid by construction. -* The addition is non-breaking: the unconstrained default is unchanged. -* An impossible instant/offset combination is diagnosed eagerly through the existing engine, naming both constraints. - -### Negative - -* `AnyDateTimeOffset` now carries a second dimension and its own offset state threaded through every transform. -* The offset dimension is `DateTimeOffset`-specific — the other temporal generators have no offset — a deliberate specificity rather than a uniform surface. - -### Risks - -* A pinned offset near the domain edge tightens the reachable instant window; a user could read the resulting eager conflict as spurious. Mitigation: the conflict names both constraints, and the behaviour is documented. -* `WithOffset` combined with `OneOf` does not override a `OneOf` value's own offset. Mitigation: documented, and consistent with `OneOf`'s terminal-enumeration semantics. - -## Follow-up Actions - -* Document `WithOffset`/`WithOffsetBetween` in the JustDummies readme and the builder documentation (done in the implementing pull request). -* Consider a shorthand for "any valid offset" only if `WithOffsetBetween(-14h, +14h)` proves a friction in practice. -* Revisit varying the offset by default only under a future major version. - -## References - -* Issue [#297](https://github.com/Reefact/first-class-errors/issues/297) — the dedicated issue for this feature. -* Issue [#226](https://github.com/Reefact/first-class-errors/issues/226) — the Nice-to-Have backlog it was split from. -* [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md) — the terminal-enumeration semantics `OneOf` follows. -* `AnyDateTimeOffset` in the `JustDummies` project; the JustDummies NuGet readme. diff --git a/doc/handwritten/for-maintainers/adr/0038-open-the-ambient-seed-scope-to-adapters.fr.md b/doc/handwritten/for-maintainers/adr/0038-open-the-ambient-seed-scope-to-adapters.fr.md deleted file mode 100644 index 3caeef43..00000000 --- a/doc/handwritten/for-maintainers/adr/0038-open-the-ambient-seed-scope-to-adapters.fr.md +++ /dev/null @@ -1,193 +0,0 @@ -# ADR-0038 | Ouvrir la portée de graine ambiante aux adaptateurs de framework de test - -🌍 🇬🇧 [English](0038-open-the-ambient-seed-scope-to-adapters.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -`JustDummies` tire chaque valeur arbitraire d'une source aléatoire. Les points -d'entrée statiques `Any` tirent d'une source **ambiante** qui suit le contexte -d'exécution, si bien qu'elle ne fuit jamais entre des tests exécutés en -parallèle. Le déterminisme sur cette source ambiante est optionnel, et -aujourd'hui seuls deux chemins publics y accèdent : - -* `Any.Reproducibly(...)`, qui fixe une graine pour la durée d'un **délégué dont - il est propriétaire**, exécute ce délégué et rapporte la graine si celui-ci - lève. L'ADR-0026 en a fait le récit unique de graine du dépôt. -* `Any.WithSeed(...)`, qui crée un contexte **isolé**. Les points d'entrée - statiques `Any` n'y tirent pas, donc il ne fixe rien pour du code qui les - utilise. - -La poignée qui ouvre et ferme une portée de graine ambiante existe, mais elle est -interne. - -Un adaptateur de framework de test — le package compagnon xUnit considéré -séparément, ou tout futur adaptateur pour un autre framework — ne possède pas de -délégué enveloppant le corps du test. La couture qu'offre un framework est une -paire de points d'accroche exécutés *avant* et *après* la méthode de test. Un -adaptateur doit donc ouvrir la portée ambiante dans l'un et la fermer dans -l'autre, ce qu'aucun chemin public ne permet. - -Deux faits supplémentaires pèsent sur la forme de cette ouverture. - -* **Les échecs de génération portent un extrait de rejeu.** Lorsqu'un - générateur échoue — typiquement une fabrique rejetant une valeur tirée — le - message d'exception ajoute une indication nommant le mécanisme qui rejoue - réellement l'exécution. Cette indication est choisie par type de source : la - source ambiante nomme l'exécuteur à délégué, un contexte isolé se nomme - lui-même, parce que les deux se rejouent différemment. Nommer un extrait - que le code de l'appelant ne contient pas est un diagnostic trompeur, et l'éviter - est la raison même pour laquelle l'indication varie. -* **Aucune des formulations existantes ne convient à une exécution fixée par un - adaptateur.** Un test dont la graine a été fixée par un adaptateur ne contient - aucun appel à l'exécuteur à délégué, et le rejouer signifie modifier ce que - l'adaptateur lit — un argument d'attribut, un réglage d'exécuteur — et non - ajouter un appel que le test n'a jamais eu. - -Le dépôt dispose déjà d'un idiome établi pour les surcharges locales au contexte, -consigné dans l'ADR-0006 et employé par les coutures d'horloge et d'identifiants -d'instance du package de test : la surcharge est ouverte par un appel `Use…` et -fermée en disposant ce qu'il retourne. - -`JustDummies` est en pré-1.0 et n'est pas encore publié sur NuGet (ADR-0011), donc sa -surface publique peut encore grandir sans cérémonie de compatibilité. L'identité -de la bibliothèque est de ne dépendre de rien au-delà de la bibliothèque -standard, une frontière qu'un test d'architecture vérifie sur son propre -assembly. - -Le chemin d'accès alternatif — accorder à un package compagnon nommé l'accès aux -membres internes de `JustDummies` — est disponible : la bibliothèque ne déclare -aucune autorisation de ce type aujourd'hui. - -## Décision - -`JustDummies` expose la portée de graine ambiante sous forme de poignée publique et -disposable, dont l'ouvreur peut fournir l'extrait de rejeu que les -diagnostics d'échec de génération nommeront. - -## Justification - -* **La forme d'un adaptateur est avant/après, pas autour d'un délégué.** - L'exécuteur à délégué ne peut pas servir un appelant qui n'a aucun délégué à - envelopper, et le contexte isolé est la mauvaise source — le code sous test - tire de la source ambiante. Une portée que l'appelant ouvre et ferme lui-même - est la seule forme qui épouse la couture qu'un framework de test offre - réellement. -* **C'est le caractère public, et non une autorisation d'accès aux internes, qui - garde tous les adaptateurs possibles.** Une autorisation d'accès privilégie un - compagnon nommé et exclut les autres : un adaptateur tiers, ou un adaptateur - interne pour un autre framework, exigerait chacun sa propre autorisation et sa - propre modification de `JustDummies`. Une poignée publique fait de « adapter un - autre framework plus tard » une décision additive ne touchant rien ici — ce qui - est précisément la propriété qui permet de prendre la décision xUnit de façon - étroite, sans trancher le reste. -* **Porter l'extrait de rejeu préserve un invariant que la bibliothèque - applique déjà.** Le diagnostic nomme le mécanisme qui s'applique ; c'est - d'ailleurs pourquoi l'indication varie selon la source. Un adaptateur introduit - une troisième manière de fixer la source ambiante, et sans moyen de le dire il - hériterait de la formulation de l'exécuteur à délégué — annonçant, à un - développeur dont le test ne contient aucun appel de ce genre, exactement - l'extrait trompeur que le mécanisme existe pour empêcher. Laisser - l'ouvreur nommer l'extrait prolonge ce design au lieu de le contourner. -* **La forme « portée disposable » est déjà l'idiome maison.** L'ADR-0006 l'a - établie pour les surcharges d'horloge et d'identifiants d'instance, si bien que - l'ajout se reconnaît comme la même chose plutôt que comme un second mécanisme - sans rapport. -* **C'est le moment le moins coûteux.** Le package est en pré-1.0 et non publié, - donc la surface peut être façonnée maintenant ; un paramètre ajouté plus tard à - un membre publié est plus perturbateur qu'un paramètre présent dès l'origine. - -## Alternatives considérées - -### Accorder au package compagnon l'accès aux membres internes de JustDummies - -Considérée parce qu'elle n'ajoute aucune surface publique : l'adaptateur -utiliserait la poignée interne existante telle quelle. Rejetée parce qu'elle -privilégie un compagnon nommé — tout autre adaptateur, interne ou tiers, aurait -besoin de sa propre autorisation et donc de sa propre modification de `JustDummies` — -et parce qu'elle couple les identités d'assembly des deux packages pour une -capacité qui n'est pas, en elle-même, privée. - -### Exposer la portée sans extrait de rejeu - -Considérée comme le plus petit ajout possible, reportant la question du -diagnostic jusqu'à l'existence d'un adaptateur. Rejetée parce qu'elle livre le -diagnostic trompeur que le mécanisme d'indication existe pour empêcher : toute -exécution fixée par un adaptateur dont la génération échoue dirait au -développeur d'utiliser un appel que son test ne contient pas. Elle reporte en -outre l'ajout d'un paramètre sur un membre publié, ce qui est l'ordre le plus -perturbateur. - -### Formuler l'indication ambiante de façon neutre, pour qu'elle ne soit jamais fausse - -Considérée parce qu'une indication ne nommant aucun mécanisme ne peut pas nommer -le mauvais. Rejetée parce qu'elle fait payer le cas rare par le cas dominant : -les utilisateurs de l'exécuteur à délégué perdraient un extrait actionnable -— l'appel exact à écrire — pour accommoder un appelant qui peut simplement -énoncer la sienne. - -### Laisser les adaptateurs réutiliser l'exécuteur à délégué - -Considérée parce qu'elle n'exige rien de nouveau. Rejetée parce que les points -d'accroche avant/après d'un framework ne donnent à un adaptateur aucun délégué à -passer : il observe le test, il ne l'invoque pas. - -## Conséquences - -### Positives - -* Tout framework de test peut être adapté sans accès privilégié à `JustDummies` et - sans modification supplémentaire de celui-ci, si bien que chaque adaptateur - supplémentaire est une décision indépendante et additive. -* Une exécution dont un adaptateur a fixé la graine rapporte un extrait de - rejeu correspondant au code de l'appelant, ce qui préserve la garantie qu'un - diagnostic ne nomme jamais un mécanisme que le lecteur n'utilise pas. -* L'ajout réutilise l'idiome établi de portée disposable au lieu d'introduire une - seconde forme pour le même concept. - -### Négatives - -* Une troisième manière publique de contrôler la graine, aux côtés de l'exécuteur - à délégué et du contexte isolé. La documentation doit garder les trois - distinctes et dire laquelle le lecteur cherche. -* L'extrait de rejeu est fourni par l'appelant et ne peut pas être validé - par `JustDummies` ; une formulation maladroite dégrade donc le diagnostic qu'elle - devait améliorer. - -### Risques - -* Un appelant qui ouvre la portée sans la fermer laisse fuir une graine fixée - vers ce qui s'exécute ensuite dans le même contexte d'exécution. Le risque est - borné par l'idiome — la propriété appartient à qui a ouvert la portée — et - c'est le même contrat que portent déjà les surcharges d'horloge et - d'identifiants d'instance. -* Une poignée publique invite à un usage hors adaptateur de framework de test, là - où l'exécuteur à délégué servirait mieux. C'est une affaire de documentation, - pas de justesse : la portée se comporte identiquement quelle que soit la - manière dont elle est ouverte. - -## Actions de suivi - -* Documenter l'ajout dans le guide utilisateur, en anglais et en français de - concert, en distinguant les trois manières de contrôler la graine et en - désignant le cas de l'adaptateur comme celui pour lequel cette poignée existe. -* Réexaminer la forme portant l'extrait si un second adaptateur montre - qu'elle reste habituellement inutilisée. - -## Références - -* ADR-0006 — Fournir les valeurs de test arbitraires depuis une source unique - semable : l'idiome de portée disposable que cet ajout réutilise, et le suivi - anticipant un adaptateur de framework de test. -* ADR-0011 — Héberger JustDummies comme package autonome : l'identité zéro-dépendance - et la latitude pré-1.0 sur lesquelles cette décision s'appuie. -* ADR-0026 — Rebaser les valeurs arbitraires du package de test sur JustDummies : le - récit unique de graine que cette source ambiante porte désormais. -* ADR-0039 — Adapter JustDummies à xUnit v3 via un package compagnon : le premier - consommateur de cette poignée. -* Issue #226 — le backlog des « nice-to-have » de JustDummies où l'adaptateur est - suivi. diff --git a/doc/handwritten/for-maintainers/adr/0038-open-the-ambient-seed-scope-to-adapters.md b/doc/handwritten/for-maintainers/adr/0038-open-the-ambient-seed-scope-to-adapters.md deleted file mode 100644 index d48161f8..00000000 --- a/doc/handwritten/for-maintainers/adr/0038-open-the-ambient-seed-scope-to-adapters.md +++ /dev/null @@ -1,175 +0,0 @@ -# ADR-0038 | Open the ambient seed scope to test-framework adapters - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0038-open-the-ambient-seed-scope-to-adapters.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -`JustDummies` draws every arbitrary value from a random source. The static `Any` -entry points draw from an **ambient** source that flows with the execution -context, so it never leaks across tests running in parallel. Determinism over -that ambient source is opt-in, and today only two public paths reach it: - -* `Any.Reproducibly(...)`, which pins a seed for the duration of a **delegate it - owns**, runs that delegate, and reports the seed if it throws. ADR-0026 made - this the repository's single seed story. -* `Any.WithSeed(...)`, which creates an **isolated** context. The static `Any` - entry points do not draw from it, so it pins nothing for code that uses them. - -The handle that opens and closes an ambient seed scope exists, but is internal. - -A test-framework adapter — the xUnit companion package considered separately, or -any future adapter for another framework — does not own a delegate wrapping the -test body. The seam a framework offers is a pair of hooks that run *before* and -*after* the test method. An adapter must therefore open the ambient scope in one -hook and close it in the other, which no public path allows. - -Two further facts bear on the shape of that opening. - -* **Generation failures carry a replay snippet.** When a generator fails — - typically a factory rejecting a drawn value — the exception message appends a - guidance naming the mechanism that actually replays the run. That guidance is chosen - per kind of source: the ambient source names the delegate runner, an isolated - context names itself, because the two are replayed differently. Naming an - snippet the caller's code does not contain is a misleading diagnostic, and - avoiding it is the reason the guidance varies at all. -* **Neither existing phrasing fits an adapter-pinned run.** A test whose seed was - pinned by an adapter contains no call to the delegate runner, and replaying it - means changing whatever the adapter reads — an attribute argument, a runner - setting — not adding a call the test never had. - -The repository already has an established idiom for context-local overrides, -recorded in ADR-0006 and used by the testing package's clock and instance-id -seams: the override is opened by a `Use…` call and closed by disposing what it -returns. - -`JustDummies` is pre-1.0 and not yet published to NuGet (ADR-0011), so its public -surface can still grow without a compatibility ceremony. The library's identity -is that it depends on nothing beyond the standard library, a boundary an -architecture test asserts over its own assembly. - -The alternative access path — granting a named companion package access to -`JustDummies`' internals — is available: the library declares no such grant today. - -## Decision - -`JustDummies` exposes the ambient seed scope as a public, disposable handle whose -opener may supply the replay snippet that generation-failure diagnostics will -name. - -## Rationale - -* **An adapter's shape is before/after, not around a delegate.** The delegate - runner cannot serve a caller that has no delegate to wrap, and the isolated - context is the wrong source — the code under test draws from the ambient one. - A scope the caller opens and closes itself is the only shape that fits the seam - a test framework actually offers. -* **Public, rather than an internals grant, is what keeps every adapter - possible.** An internals grant privileges one named companion and forecloses - the others: a third-party adapter, or a first-party one for another framework, - would each need their own grant and their own change to `JustDummies`. A public - handle makes "adapt another framework later" an additive decision that touches - nothing here — which is precisely the property that lets the xUnit decision be - taken narrowly, without deciding the rest. -* **Carrying the replay snippet preserves an invariant the library already - enforces.** The diagnostic names the mechanism that applies; that is why the - guidance varies by source in the first place. An adapter introduces a third way of - pinning the ambient source, and without a way to say so it would inherit the - delegate runner's phrasing — advertising, to a developer whose test contains no - such call, exactly the misleading snippet the mechanism exists to prevent. - Letting the opener name the snippet extends that design rather than - working around it. -* **The disposable-scope shape is already the house idiom.** ADR-0006 established - it for the clock and instance-id overrides, so the addition is recognizable as - the same thing rather than a second, unrelated mechanism. -* **This is the cheapest moment.** The package is pre-1.0 and unpublished, so the - surface can be shaped now; a parameter added to a published member later is - more disruptive than one present from the start. - -## Alternatives Considered - -### Grant the companion package access to JustDummies' internals - -Considered because it adds no public surface at all: the adapter would use the -existing internal handle unchanged. Rejected because it privileges one named -companion — every other adapter, first-party or third-party, would need its own -grant and therefore its own change to `JustDummies` — and because it couples the two -packages' assembly identities for a capability that is not, in itself, private. - -### Expose the scope without a replay snippet - -Considered as the smallest possible addition, deferring the diagnostic question -until an adapter exists. Rejected because it ships the misleading diagnostic the -guidance mechanism exists to prevent: every adapter-pinned run whose generation fails -would tell the developer to use a call their test does not contain. It also -defers a parameter onto a published member, which is the more disruptive order. - -### Phrase the ambient guidance neutrally, so it is never wrong - -Considered because guidance that names no mechanism cannot name the wrong one. -Rejected because it pays for the rarer case with the dominant one: the delegate -runner's users would lose an actionable snippet — the exact call to write — -to accommodate a caller that can simply state its own. - -### Let adapters reuse the delegate runner - -Considered because it needs nothing new. Rejected because a framework's -before/after hooks give an adapter no delegate to pass: it observes the test, it -does not invoke it. - -## Consequences - -### Positive - -* Any test framework can be adapted without privileged access to `JustDummies` and - without a further change to it, so each additional adapter is an independent, - additive decision. -* A run whose seed an adapter pinned reports a replay snippet that matches - the caller's own code, keeping the guarantee that a diagnostic never names a - mechanism the reader does not use. -* The addition reuses the established disposable-scope idiom instead of - introducing a second shape for the same concept. - -### Negative - -* A third public way to control seeding, alongside the delegate runner and the - isolated context. The documentation must keep the three distinct and say which - one a reader wants. -* The replay snippet is supplied by the caller and cannot be validated by - `JustDummies`, so a badly phrased one degrades the diagnostic it was meant to - improve. - -### Risks - -* A caller that opens the scope and fails to close it leaks a pinned seed into - whatever runs next in the same execution context. The risk is bounded by the - idiom — ownership belongs to whoever opened the scope — and is the same - contract the clock and instance-id overrides already carry. -* A public handle invites use outside a test-framework adapter, where the - delegate runner would serve better. This is a documentation matter, not a - correctness one: the scope behaves identically however it is opened. - -## Follow-up Actions - -* Document the addition in the user guide, in English and French in lockstep, - distinguishing the three ways to control seeding and naming the adapter case as - the one this handle exists for. -* Revisit the snippet-carrying form if a second adapter shows it is - habitually left unused. - -## References - -* ADR-0006 — Supply arbitrary test values from a single seedable source: the - disposable-scope idiom this addition reuses, and the follow-up anticipating a - test-framework adapter. -* ADR-0011 — Host JustDummies as a standalone package: the zero-dependency identity - and the pre-1.0 latitude this decision relies on. -* ADR-0026 — Rebase the testing package's arbitrary values on JustDummies: the single - seed story this ambient source now carries. -* ADR-0039 — Adapt JustDummies to xUnit v3 through a companion package: the first - consumer of this handle. -* Issue #226 — the JustDummies nice-to-have backlog where the adapter is tracked. diff --git a/doc/handwritten/for-maintainers/adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.fr.md b/doc/handwritten/for-maintainers/adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.fr.md deleted file mode 100644 index c462270a..00000000 --- a/doc/handwritten/for-maintainers/adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.fr.md +++ /dev/null @@ -1,214 +0,0 @@ -# ADR-0039 | Adapter JustDummies à xUnit v3 via un package compagnon - -🌍 🇬🇧 [English](0039-adapt-dummies-to-xunit-v3-through-a-companion-package.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -Un test qui tire des valeurs arbitraires n'est reproductible que si une graine -est fixée et rapportée. `JustDummies` fournit cela via un exécuteur qui fixe une -graine pour la durée d'un délégué et la rapporte lorsque ce délégué lève ; chaque -test sensible aux valeurs doit donc envelopper son corps dans ce délégué. La -cérémonie est reconstituée à la main dans chaque consommateur. - -Un adaptateur qui la supprime a été anticipé puis perdu. Les suivis de l'ADR-0006 -appelaient un adaptateur optionnel de framework de test « pour que la graine soit -exposée automatiquement, sans envelopper chaque corps » ; l'ADR-0026 a rebasé le -moteur de valeurs sur `JustDummies` sans reprendre ce suivi. La capacité est donc -anticipée par une ADR acceptée et remplacée par rien. L'audit d'architecture et -de conception de `JustDummies` du 2026-07-20 demande un oui ou un non explicite -plutôt qu'un silence prolongé, et place la décision dans le premier cycle stable. - -La capacité qu'un adaptateur doit fournir est étroite : fixer une graine pour la -durée d'un test, et exposer cette graine au développeur **uniquement lorsque le -test échoue**. Une graine rapportée à chaque exécution est du bruit ; une graine -jamais rapportée laisse un échec irreproductible. - -L'identité de `JustDummies` est de ne dépendre de rien au-delà de la bibliothèque -standard (ADR-0011), une frontière qu'un test d'architecture vérifie sur son -propre assembly. Elle ne peut donc pas référencer un framework de test, si bien -que tout adaptateur est un package compagnon distinct — l'arrangement que -`FirstClassErrors.Testing` établit déjà comme précédent dans ce dépôt. - -Les frameworks diffèrent par ce qu'expose leur extensibilité supportée, et la -différence est décisive pour la condition « uniquement en cas d'échec » : - -* **xUnit v3.** Son point d'accroche avant/après reçoit le test lui-même, et le - contexte de test ambiant expose l'issue du test terminé — succès ou échec, sa - cause d'échec et le détail de son exception — ainsi que le puits de sortie du - test. La condition « uniquement en cas d'échec » est donc exprimable dans - l'extensibilité documentée, sans aucune implication dans la découverte ni - l'exécution des tests. Le même point d'accroche est collecté depuis la méthode, - la classe et l'assembly, si bien qu'un attribut unique sert un test, une classe - entière ou une suite complète, et il s'exécute une fois par cas de théorie - plutôt qu'une fois par méthode de théorie. -* **xUnit v2.** Son point d'accroche équivalent ne reçoit que la méthode sous - test, et son assembly ne porte aucun contexte de test. Un attribut v2 ne peut - pas observer si le test a réussi ou échoué. Y exprimer la condition « uniquement - en cas d'échec » exige de remplacer la chaîne de découverte et d'exécution des - cas de test. - -Les deux versions livrent des assemblies et des espaces de noms distincts, si -bien qu'un seul assembly ne peut pas référencer les deux ; supporter chacune -signifierait de toute façon un package séparé. - -Les projets de test de ce dépôt s'exécutent déjà sur xUnit v3, si bien qu'un -adaptateur v3 est éprouvé par son auteur autant que documenté. - -L'exécuteur à délégué continue de fonctionner sur tous les frameworks et n'est -pas affecté par cette décision ; les utilisateurs de tout autre framework ne -perdent donc aucune capacité — ils conservent la forme qui existe aujourd'hui. - -L'ADR-0038 ouvre la portée de graine ambiante sous forme de poignée publique, si -bien qu'un adaptateur n'a besoin d'aucun accès privilégié à `JustDummies` et -qu'ajouter plus tard un adaptateur pour un autre framework n'exige aucune -modification de celui-ci. - -`JustDummies` est publié sur le train de release `dum`, qui ne porte actuellement -qu'un seul package. La bibliothèque a vocation à rejoindre son propre dépôt à -terme. - -## Décision - -`JustDummies` reçoit un package compagnon qui fixe et rapporte automatiquement la -graine pour les tests xUnit v3, et ne vise aucun autre framework de test. - -## Justification - -* **Le rapport « uniquement en cas d'échec » est toute la capacité, et seul v3 - sait l'exprimer.** Fixer une graine est facile partout ; décider s'il faut - l'exposer est ce qui sépare un adaptateur utile du bruit. xUnit v3 expose - l'issue du test terminé dans son extensibilité documentée, si bien que - l'adaptateur est une petite quantité de code au-dessus d'un contrat supporté. - En v2 la même condition est inatteignable depuis le point d'accroche - correspondant et exige de s'approprier la découverte et l'exécution sur une - surface semi-interne — un coût permanent et fragile, assumé pour un package - explicitement non destiné à être rouvert. -* **Un seul point d'accroche couvre toute la surface.** Parce que le framework - collecte le point d'accroche depuis la méthode, la classe et l'assembly et - l'exécute par cas de théorie, un attribut unique sert un test, une classe, une - suite entière et chaque cas d'une théorie. C'est toute la surface dont la - capacité a besoin, sans second type par sorte de test et sans toucher à la - manière dont les tests sont découverts. -* **Rien n'est retiré à personne d'autre.** L'exécuteur à délégué reste la forme - portable et continue de fonctionner sur tous les frameworks ; choisir un - framework pour l'adaptateur ne prive donc les utilisateurs des autres d'aucune - capacité, mais seulement de la commodité. -* **Un package compagnon est imposé par l'identité de la bibliothèque.** La - frontière zéro-dépendance rend impossible une référence à un framework de test - à l'intérieur de `JustDummies`, et le dépôt livre déjà un package compagnon pour - exactement cette raison. -* **Le choix est éprouvé par son auteur.** Les suites de ce dépôt s'exécutent sur - xUnit v3, si bien que l'adaptateur est utilisé là où il est maintenu plutôt que - livré sans usage. -* **L'étroitesse est délibérée et peu coûteuse à réexaminer.** Parce que - l'ADR-0038 rend la portée de graine publiquement atteignable, un adaptateur - pour un autre framework est une décision additive n'exigeant aucune - modification ici — décliner les autres maintenant n'exclut donc rien. - -## Alternatives considérées - -### Ne rien livrer et conserver l'exécuteur à délégué - -Considérée parce qu'elle ne coûte rien, fonctionne sur tous les frameworks et -correspond déjà à ce que font les consommateurs. Rejetée parce que c'est ce que -le silence a déjà produit une fois : le suivi consigné par l'ADR-0006 a été -abandonné lors du rebase et remplacé par rien, et l'audit demande que la question -soit tranchée plutôt que laissée ouverte. La cérémonie ainsi préservée est -reconstituée à la main dans chaque consommateur, ce qui est précisément le coût -que l'adaptateur existe pour supprimer. - -### Supporter aussi xUnit v2, dans un second package - -Considérée parce que v2 reste une base installée importante, et que « facilement -adoptable » plaide pour rejoindre les consommateurs là où ils sont. Rejetée parce -que la condition « uniquement en cas d'échec » n'est pas exprimable dans le point -d'accroche avant/après de v2 : la livrer signifie remplacer la chaîne de -découverte et d'exécution des cas de test et la maintenir indéfiniment contre une -surface semi-interne. C'est un coût disproportionné et permanent pour une -commodité dont l'absence laisse les utilisateurs v2 exactement où ils sont -aujourd'hui, avec l'exécuteur à délégué portable. - -### Dériver des attributs de fait et de théorie du framework - -Considérée parce qu'elle produit un attribut unique et auto-descriptif par sorte -de test, correspondant au nom qu'avait esquissé le suivi de l'ADR-0006. Rejetée -parce qu'elle coûte un type par sorte de test, ne se compose pas avec des -attributs de fait tiers, ne peut être appliquée ni à une classe ni à un assembly, -et achète une exposition aux internes de la découverte en échange d'aucune -capacité qui manquerait au point d'accroche avant/après. - -### Construire un adaptateur agnostique du framework - -Considérée parce qu'elle servirait tous les frameworks d'un coup et rendrait le -choix sans objet. Rejetée parce qu'il n'existe aucun point d'accroche -inter-frameworks sur lequel la bâtir : la capacité est définie par ce que chaque -framework expose d'un test terminé, et ces surfaces n'ont ni forme ni vocabulaire -communs. - -## Conséquences - -### Positives - -* La reproductibilité devient déclarative et optionnelle à la granularité que - l'auteur choisit — un test, une classe ou une suite entière — au lieu d'un - délégué enveloppant chaque corps sensible aux valeurs. -* Une exécution en échec nomme sa graine sans que l'auteur ait anticipé l'échec, - ce qui est le cas que l'exécuteur à délégué ne sert que lorsqu'il a été - appliqué à l'avance. -* L'adaptateur est éprouvé par les suites de ce dépôt. - -### Négatives - -* Un nouveau package publié, avec sa propre documentation en anglais et en - français, sa propre référence d'API publique et sa propre place dans la chaîne - de build et de release. -* La commodité n'atteint que les utilisateurs de xUnit v3 ; tout autre framework - conserve l'exécuteur à délégué. -* Le plancher de frameworks supportés du package est celui du framework de test, - au-dessus du plancher que `JustDummies` conserve — les deux ne peuvent donc pas - partager une même liste de cibles. - -### Risques - -* L'adaptateur dépend du contrat avant/après du framework et de son exposition de - l'issue d'un test terminé. Une future version majeure pourrait changer l'un ou - l'autre. L'exposition est bornée : l'adaptateur utilise l'extensibilité - documentée, pas les internes, si bien qu'un changement se manifesterait comme - un échec de compilation ou de comportement dans sa propre suite plutôt que - silencieusement. -* Une portée de graine ouverte avant un test doit être fermée même lorsque le - test lève, sans quoi la graine fixée fuit vers ce qui s'exécute ensuite dans le - même contexte d'exécution. - -## Actions de suivi - -* Publier le package sur le train `dum` existant dans un premier temps, pour - qu'il soit versionné avec `JustDummies`. **Lorsque `JustDummies` rejoindra son propre - dépôt, réexaminer si le package compagnon a besoin de son propre train** — un - train partagé n'est approprié que tant que les deux sont livrés depuis le même - endroit et à la même cadence. -* Documenter l'adaptateur dans le guide utilisateur et le readme du package, en - anglais et en français de concert, en présentant l'exécuteur à délégué comme la - forme portable et l'adaptateur comme la commodité xUnit v3. -* Ne réexaminer un adaptateur pour un autre framework que sur demande démontrée ; - l'ADR-0038 garde chacun additif. - -## Références - -* ADR-0038 — Ouvrir la portée de graine ambiante aux adaptateurs de framework de - test : la poignée publique dont ce package est le premier consommateur. -* ADR-0006 — Fournir les valeurs de test arbitraires depuis une source unique - semable : le suivi qui a anticipé cet adaptateur. -* ADR-0011 — Héberger JustDummies comme package autonome : la frontière - zéro-dépendance qui impose un package compagnon. -* ADR-0026 — Rebaser les valeurs arbitraires du package de test sur JustDummies : le - rebase dans lequel le suivi anticipé a été abandonné. -* `doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md` - — l'audit demandant une décision explicite. -* Issue #226 — le backlog des « nice-to-have » de JustDummies où l'adaptateur est - suivi. diff --git a/doc/handwritten/for-maintainers/adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.md b/doc/handwritten/for-maintainers/adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.md deleted file mode 100644 index a1bc80ad..00000000 --- a/doc/handwritten/for-maintainers/adr/0039-adapt-dummies-to-xunit-v3-through-a-companion-package.md +++ /dev/null @@ -1,196 +0,0 @@ -# ADR-0039 | Adapt JustDummies to xUnit v3 through a companion package - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0039-adapt-dummies-to-xunit-v3-through-a-companion-package.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -A test that draws arbitrary values is reproducible only if a seed is pinned and -reported. `JustDummies` supplies that through a runner that pins a seed for the -duration of a delegate and reports it when the delegate throws, so every -value-sensitive test must wrap its body in that delegate. The ceremony is -re-derived by hand in every consumer. - -An adapter that removes it was anticipated and then lost. ADR-0006's follow-ups -called for an optional test-framework adapter "so the seed is surfaced -automatically, without wrapping each body"; ADR-0026 rebased the value engine -onto `JustDummies` and did not carry that follow-up forward. The capability is -therefore anticipated by one accepted ADR and replaced by nothing. The -2026-07-20 `JustDummies` architecture and design audit asks for an explicit yes or no -on it rather than continued silence, and places the decision in the first stable -cycle. - -The capability an adapter must provide is narrow: pin a seed for the duration of -a test, and surface that seed to the developer **only when the test fails**. A -seed reported on every run is noise; a seed never reported leaves a failure -unreproducible. - -`JustDummies`' identity is that it depends on nothing beyond the standard library -(ADR-0011), a boundary an architecture test asserts over its own assembly. It -therefore cannot reference a test framework, so any adapter is a separate, -companion package — the arrangement `FirstClassErrors.Testing` already -establishes as a precedent in this repository. - -The frameworks differ in what their supported extensibility exposes, and the -difference is decisive for the failure-only condition: - -* **xUnit v3.** Its before/after test hook receives the test itself, and the - ambient test context exposes the finished test's outcome — pass or fail, its - failure cause and its exception detail — together with the test's output sink. - The failure-only condition is therefore expressible in documented - extensibility, with no involvement in test discovery or execution. The same - hook is collected from the method, the class and the assembly, so a single - attribute serves one test, a whole class or an entire suite, and it runs once - per theory case rather than once per theory method. -* **xUnit v2.** Its equivalent hook receives only the method under test, and its - assembly carries no test context at all. A v2 attribute cannot observe whether - the test passed or failed. Expressing the failure-only condition there requires - replacing the test-case discovery and execution chain. - -The two versions ship distinct assemblies and namespaces, so one assembly cannot -reference both; supporting each would mean a separate package either way. - -This repository's own test projects already run on xUnit v3, so a v3 adapter is -exercised by its author as well as documented. - -The delegate runner keeps working on every framework and is unaffected by this -decision, so users of any other framework lose no capability — they keep the -form that exists today. - -ADR-0038 opens the ambient seed scope as a public handle, so an adapter needs no -privileged access to `JustDummies` and adding an adapter for another framework later -requires no change to it. - -`JustDummies` is published on the `dum` release train, which currently carries a -single package. The library is expected to move to its own repository in time. - -## Decision - -`JustDummies` gains a companion package that pins and reports the seed automatically -for xUnit v3 tests, and targets no other test framework. - -## Rationale - -* **The failure-only report is the whole capability, and only v3 can express - it.** Pinning a seed is easy everywhere; deciding whether to surface it is what - separates a useful adapter from noise. xUnit v3 exposes the finished test's - outcome in its documented extensibility, so the adapter is a small amount of - code over a supported contract. In v2 the same condition is unreachable from - the corresponding hook and requires owning discovery and execution over - semi-internal surface — a permanent, fragile cost, taken on for a package - explicitly not expected to be revisited. -* **One hook covers the whole surface.** Because the framework collects the hook - from method, class and assembly and runs it per theory case, a single attribute - serves a test, a class, a whole suite, and every case of a theory. That is the - full surface the capability needs, without a second type per kind of test and - without touching how tests are discovered. -* **Nothing is taken away from anyone else.** The delegate runner remains the - portable form and keeps working on every framework, so choosing one framework - for the adapter withholds no capability from users of the others; it withholds - only the convenience. -* **A companion package is forced by the library's identity.** The zero-dependency - boundary makes a test-framework reference impossible inside `JustDummies`, and the - repository already ships a companion package for exactly this reason. -* **The choice is dogfooded.** The repository's own suites run on xUnit v3, so - the adapter is used where it is maintained rather than shipped untried. -* **Narrowness is deliberate and cheap to revisit.** Because ADR-0038 makes the - seed scope publicly reachable, an adapter for another framework is an additive - decision requiring no change here — so declining the others now forecloses - nothing. - -## Alternatives Considered - -### Ship nothing and keep the delegate runner - -Considered because it costs nothing, works on every framework, and is already -what consumers do. Rejected because it is what silence has already produced once: -the follow-up ADR-0006 recorded was dropped in the rebase and replaced by -nothing, and the audit asks for the question to be answered rather than left -open. The ceremony it preserves is re-derived by hand in every consumer, which is -the cost the adapter exists to remove. - -### Support xUnit v2 as well, in a second package - -Considered because v2 remains a large installed base, and "easily adopted" argues -for meeting consumers where they are. Rejected because the failure-only condition -is not expressible in v2's before/after hook: delivering it means replacing the -test-case discovery and execution chain and maintaining that against -semi-internal surface indefinitely. That is a disproportionate, permanent cost -for a convenience whose absence leaves v2 users exactly where they are today, -with the portable delegate runner. - -### Derive from the framework's fact and theory attributes - -Considered because it yields a single self-describing attribute per kind of test, -matching the name ADR-0006's follow-up had sketched. Rejected because it costs one -type per kind of test, does not compose with third-party fact attributes, cannot -be applied to a class or an assembly, and buys exposure to the discovery -internals in exchange for no capability the before/after hook lacks. - -### Build one framework-agnostic adapter - -Considered because it would serve every framework at once and make the choice -moot. Rejected because there is no cross-framework hook to build it on: the -capability is defined by what each framework exposes about a finished test, and -those surfaces have neither a common shape nor a common vocabulary. - -## Consequences - -### Positive - -* Reproducibility becomes declarative and opt-in at the granularity the author - chooses — a test, a class, or an entire suite — instead of a delegate wrapped - around every value-sensitive body. -* A failing run names its seed without the author having anticipated the failure, - which is the case the delegate runner serves only when it was applied in - advance. -* The adapter is exercised by this repository's own suites. - -### Negative - -* A new published package, with its own documentation in English and French, its - own public-API baseline, and its own place in the build and release pipeline. -* The convenience reaches xUnit v3 users only; every other framework keeps the - delegate runner. -* The package's supported-framework floor is the test framework's, which is above - the floor `JustDummies` itself keeps — so the two cannot share one target list. - -### Risks - -* The adapter depends on the framework's before/after contract and on its - exposure of a finished test's outcome. A future major version could change - either. The exposure is bounded: the adapter uses documented extensibility, not - internals, so a change would surface as a compilation or behavioural failure in - the adapter's own suite rather than silently. -* A seed scope opened before a test must be closed even when the test throws, or - the pinned seed leaks into whatever runs next in the same execution context. - -## Follow-up Actions - -* Publish the package on the existing `dum` train initially, so it versions with - `JustDummies`. **When `JustDummies` moves to its own repository, revisit whether the - companion package needs a train of its own** — a shared train is only - appropriate while the two ship from the same place and cadence. -* Document the adapter in the user guide and the package readme, in English and - French in lockstep, presenting the delegate runner as the portable form and the - adapter as the xUnit v3 convenience. -* Revisit an adapter for another framework only on demonstrated demand; ADR-0038 - keeps each one additive. - -## References - -* ADR-0038 — Open the ambient seed scope to test-framework adapters: the public - handle this package is the first consumer of. -* ADR-0006 — Supply arbitrary test values from a single seedable source: the - follow-up that anticipated this adapter. -* ADR-0011 — Host JustDummies as a standalone package: the zero-dependency boundary - that forces a companion package. -* ADR-0026 — Rebase the testing package's arbitrary values on JustDummies: the rebase - in which the anticipated follow-up was dropped. -* `doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md` - — the audit asking for an explicit decision. -* Issue #226 — the JustDummies nice-to-have backlog where the adapter is tracked. diff --git a/doc/handwritten/for-maintainers/adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md b/doc/handwritten/for-maintainers/adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md deleted file mode 100644 index 15f7811f..00000000 --- a/doc/handwritten/for-maintainers/adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md +++ /dev/null @@ -1,200 +0,0 @@ -# ADR-0040 | Répartir le banc de test de JustDummies entre une suite par l'exemple et une suite par propriétés - -🌍 🇬🇧 [English](0040-split-the-justdummies-test-bed-between-example-and-property-suites.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -`JustDummies` construit des valeurs arbitraires qui satisfont les contraintes -déclarées. Son affirmation de correction est donc quantifiée universellement : -*chaque* valeur produite par un générateur satisfait *chacune* des contraintes -déclarées sur lui, pour *chaque* combinaison légale d'arguments de contrainte. - -Jusqu'ici cette affirmation était prouvée par une seule suite, -`JustDummies.UnitTests`, qui l'établit par échantillonnage. Un test fixe une -contrainte — `Between(10, 20)`, `WithLength(12)`, `WithCount(4)` — puis tire -quelques centaines de valeurs et vérifie l'invariant sur chacune. Les valeurs -tirées varient ; les **arguments de contrainte, non**. La suite instancie donc -l'affirmation universelle en une poignée de points choisis à la main et ne prouve -rien sur le reste de l'espace des contraintes. - -Des défauts ont déjà été trouvés dans cet espace non prouvé. L'issue #206 était un -générateur d'intervalle décimal dont les tirages ne franchissaient jamais le milieu -de la plage demandée : tous les candidats tombaient dans la moitié basse. Il a été -trouvé à la main et figé en régression sur un seul intervalle, `[0, 100]`. Le bug -vivait dans la relation entre des bornes arbitraires et la valeur produite — -précisément la dimension qu'un argument fixe ne peut pas faire varier. - -La suite tire par ailleurs ses propres valeurs échantillonnées depuis -`JustDummies`, si bien que le composant sous test participe à décider avec quoi il -est testé. - -Le dépôt exploite déjà des suites par propriétés. `FirstClassErrors.PropertyTests` -et `FirstClassErrors.RequestBinder.PropertyTests` utilisent FsCheck, portent le -segment du plancher .NET Framework 4.7.2, et sont ce que le contrôle *Fuzzing* de -l'OpenSSF Scorecard lit pour créditer le projet. Aucune des deux n'a été introduite -par un ADR : un projet frère `*.PropertyTests` est ici une pratique établie, non un -mouvement architectural nouveau. - -Tous les contrats de la bibliothèque ne sont pas quantifiés universellement. Un -conflit doit lever `ConflictingAnyConstraintException` avec un message nommant *les -deux* contraintes fautives ; un argument nul doit lever `ArgumentNullException` ; le -miroir entre `Any` et `AnyContext`, la convention de nommage des fabriques et la -frontière d'assemblage autonome de la bibliothèque sont des faits structurels -vérifiés par réflexion. Ce sont des cas spécifiques et nommés, et leur formulation -est délibérément sensible au sens de l'application — une propriété qui les -quantifierait affirmerait moins, et moins lisiblement. - -L'audit d'architecture de juillet 2026 a relevé que l'ADR-0025 cite « a property -test » contre le vrai moteur d'expressions régulières, alors que ce qui existe est -un test-oracle à graine fixe et corpus fixe dans le projet de tests unitaires, et a -demandé que le texte dise ce qu'est réellement le filet de sécurité. - -## Décision - -`JustDummies` est testé par deux suites sœurs sous une frontière unique : -`JustDummies.PropertyTests` porte tout invariant quantifiable sur des **arguments -de contrainte générés**, et `JustDummies.UnitTests` porte tout contrat dont le sujet -est un cas spécifique et nommé — contenu des messages, validation des arguments, -conventions structurelles et régressions datées. - -## Justification - -L'affirmation de la bibliothèque est une quantification universelle : le test dont -la forme lui correspond est donc celui qui quantifie. Générer les arguments de -contrainte — bornes, longueurs, cardinalités, viviers et graines qu'un appelant -déclare — fait passer chaque test du statut d'instance de l'affirmation à celui de -l'affirmation elle-même, et déplace la recherche vers l'espace où #206 vivait -réellement. Tirer davantage de valeurs derrière un `Between(10, 20)` fixe n'en -explore rien. - -Un cadre de propriétés rapporte aussi les échecs différemment. Le rétrécissement -réduit un contre-exemple à sa forme minimale : un défaut arrive donc sous la forme -du plus petit intervalle et de la plus petite valeur qui le déclenchent, plutôt que -comme un tirage opaque parmi quelques centaines. Pour un composant dont les échecs -sont des cas limites arithmétiques, c'est la différence entre un diagnostic et un -point de départ. - -Tirer les contraintes depuis un générateur indépendant brise la circularité relevée -dans le Contexte : la suite n'utilise plus `JustDummies` pour décider avec quoi -tester `JustDummies`. Le biais propre de FsCheck vers les petites valeurs est -compensé en y mêlant explicitement les bords du domaine, sans quoi un décalage d'une -unité à `int.MaxValue` ne serait pour ainsi dire jamais tiré. - -La frontière est tracée là où chaque style est réellement le plus fort, non par -nature de code. Le contenu des messages, le traitement des nuls et les gardes de -convention par réflexion ne sont pas des affirmations universelles ; les exprimer en -propriétés ajouterait une quantification sur des entrées qui ne varient pas, -brouillerait ce qui est affirmé, et rendrait les assertions sur le libellé exact plus -difficiles à lire. Les garder dans la suite par l'exemple laisse chaque suite dire ce -qu'elle dit le mieux, et fait que la localisation d'un échec indique déjà quelle -classe de contrat a cédé. - -Les régressions datées restent avec les exemples pour la même raison. Une régression -fige un défaut qui a réellement eu lieu, aux coordonnées où il a eu lieu ; cette -spécificité est sa valeur, et une propriété couvrant le même terrain ne la retire pas. - -Deux projets frères plutôt qu'un projet mixte suit la convention que le dépôt applique -déjà deux fois, garde la dépendance FsCheck hors de la suite qui n'en a pas besoin, et -laisse chaque projet énoncer sa propre histoire de plancher applicatif. - -## Alternatives considérées - -### Élargir les boucles d'échantillonnage de la suite existante - -Augmenter le nombre d'échantillons et ajouter davantage d'intervalles choisis à la -main est le changement le moins coûteux et ne demande aucun nouveau projet. - -Rejeté : cela multiplie les tirages à l'intérieur des mêmes arguments de contrainte -fixes. La dimension laissée inexplorée — la relation entre une borne arbitraire et la -valeur produite — le reste, de sorte que la classe de défaut à laquelle #206 -appartenait demeure invisible. On achète du temps d'exécution, pas de l'information. - -### Convertir tout le banc de test en propriétés - -Une suite unique est plus simple à expliquer, et les tests de forme invariante -gagneraient tous à être quantifiés. - -Rejeté : les contrats décrits dans le Contexte ne sont pas quantifiés -universellement. Une propriété affirmant qu'un message de conflit nomme les deux -contraintes est un moins bon test par l'exemple — elle ne quantifie sur rien tout en -rendant l'assertion plus difficile à lire — et les gardes de convention par réflexion -n'ont aucun espace d'entrée. - -### Héberger les propriétés dans `JustDummies.UnitTests` - -Un seul projet, c'est une seule chose à construire, exécuter et configurer. - -Rejeté : cela place deux styles d'assertion et deux jeux de dépendances dans un même -assemblage, et perd le signal que le nom d'un projet en échec porte déjà. Cela -s'écarterait aussi de la convention de projet frère que le dépôt a appliquée à -`FirstClassErrors` et `FirstClassErrors.RequestBinder`. - -### Générer les arguments de contrainte avec `JustDummies` lui-même - -La bibliothèque est un générateur de valeurs : elle pourrait fournir ses propres -entrées de test et éviter une dépendance. - -Rejeté : cela approfondit la circularité au lieu de la briser. Un défaut du générateur -serait alors libre de biaiser les entrées mêmes censées l'exposer, et aucun échec ne -pourrait être attribué avec confiance. - -## Conséquences - -### Positives - -* L'affirmation universelle de la bibliothèque est prouvée sur un espace de contraintes - plutôt qu'en une poignée de points, et les défauts de la classe de #206 deviennent - atteignables par la suite. -* Une propriété en échec arrive rétrécie à un contre-exemple minimal. -* Chaque suite énonce une seule sorte de contrat : la localisation d'un échec le classe - déjà. -* La suite par propriétés porte le segment du plancher .NET Framework 4.7.2 comme ses - sœurs, de sorte que les invariants sont prouvés contre l'asset `netstandard2.0` que - les consommateurs chargent réellement. -* L'aller-retour sur les expressions régulières est prouvé par une véritable propriété, - ce qui permet de reformuler exactement l'affirmation de l'ADR-0025 : le constat - d'audit qui a motivé ce point est clos par construction plutôt que par une réécriture. - -### Négatives - -* Deux suites sont à garder en tête quand un générateur change, et la frontière doit - être appliquée délibérément plutôt que par habitude. -* Un invariant déjà prouvé par une propriété peut malgré tout être réaffirmé par un - exemple qui paraît redondant lu isolément ; la redondance est voulue quand l'exemple - est une régression datée, et non voulue sinon. -* Les générateurs par défaut de FsCheck demandent un biaisage explicite vers les bords - pour être utiles ici, ce qui fait du code de support de test supplémentaire à maintenir. - -### Risques - -* Une propriété dont les arguments générés chevauchent une frontière de légalité - dépendante de la valeur (ADR-0035) peut être écrite de façon à échouer par - intermittence plutôt que de manière déterministe. Une propriété doit décider du - résultat attendu à partir de la valeur générée, non de la forme de l'appel. -* Les propriétés statistiques — qu'une plage soit atteinte, que les deux branches d'un - tirage à pile ou face soient observées — sont probabilistes, non universelles. Écrites - sans soin elles deviennent instables ; elles relèvent d'une graine figée et doivent - être signalées comme gardes statistiques. -* Élaguer la suite par l'exemple à mesure que les propriétés arrivent peut réduire - silencieusement la couverture si l'on retire un exemple dont la propriété ne subsume - pas réellement l'invariant. - -## Actions de suivi - -* Réexaminer la formulation « property test » de l'ADR-0025, que l'audit a signalée comme - inexacte, maintenant qu'une propriété d'aller-retour existe. Seul `@reefact` peut - amender ou remplacer un ADR accepté. - -## Références - -* [Écrire les tests de JustDummies](../WritingJustDummiesTests.fr.md) — comment cette frontière s'applique au moment d'ajouter un test -* [ADR-0025](0025-generate-strings-from-a-home-grown-regular-subset.fr.md) — le sous-ensemble régulier dont cette suite prouve l'aller-retour -* [ADR-0035](0035-enforce-structural-any-conflicts-at-compile-time.fr.md) — conflits structurels contre conflits dépendants de la valeur, qui décident de la façon dont une propriété doit se ramifier -* [ADR-0036](0036-draw-lattice-constrained-scalars-on-the-grid.fr.md) — les contraintes de treillis, dont l'invariant de grille est quantifié par la suite par propriétés -* [Audit d'architecture et de conception de JustDummies, 2026-07-20](../audit/2026-07-20-dummies-architecture-and-design-audit.fr.md) — le constat « non property-based » sur l'ADR-0025 -* Issue #206 — le défaut d'intervalle décimal qui a motivé la quantification sur les bornes diff --git a/doc/handwritten/for-maintainers/adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.md b/doc/handwritten/for-maintainers/adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.md deleted file mode 100644 index fe4efadf..00000000 --- a/doc/handwritten/for-maintainers/adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.md +++ /dev/null @@ -1,186 +0,0 @@ -# ADR-0040 | Split the JustDummies test bed between an example suite and a property suite - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -`JustDummies` builds arbitrary values that satisfy declared constraints. Its -correctness claim is therefore universally quantified: *every* value a generator -produces satisfies *every* constraint declared on it, for *every* legal -combination of constraint arguments. - -Until now that claim was proven by a single suite, `JustDummies.UnitTests`, which -establishes it by sampling. A test pins a constraint — `Between(10, 20)`, -`WithLength(12)`, `WithCount(4)` — then draws a few hundred values and asserts the -invariant on each. The drawn values vary; the **constraint arguments do not**. The -suite therefore instantiates the universal claim at a handful of hand-picked -points and proves nothing about the rest of the constraint space. - -Defects have already been found in that unproven space. Issue #206 was a decimal -interval generator whose draws never crossed the midpoint of the requested range: -every candidate fell in the lower half. It was found by hand and pinned as a -seeded regression over one interval, `[0, 100]`. The bug lived in the relation -between arbitrary bounds and the produced value — precisely the dimension a fixed -argument cannot vary. - -The suite also draws its own sampled values from `JustDummies`, so the component -under test participates in deciding what it is tested with. - -The repository already operates property-based suites. `FirstClassErrors.PropertyTests` -and `FirstClassErrors.RequestBinder.PropertyTests` run FsCheck, carry the .NET -Framework 4.7.2 floor leg, and are what OpenSSF Scorecard's Fuzzing check reads to -credit the project. Neither was introduced by an ADR: a sibling `*.PropertyTests` -project is established practice here, not a new architectural move. - -Not every contract of the library is universally quantified. A conflict must raise -`ConflictingAnyConstraintException` with a message naming *both* offending -constraints; a null argument must raise `ArgumentNullException`; the mirror between -`Any` and `AnyContext`, the factory naming convention, and the library's standalone -assembly boundary are structural facts checked by reflection. These are specific, -named cases, and their wording is deliberately direction-aware — a property that -quantified over them would assert less, less readably. - -The July 2026 architecture audit recorded that ADR-0025 cites "a property test" -against the real regular-expression engine, whereas what exists is a fixed-seed, -fixed-corpus oracle test in the unit-test project, and asked that the text say what -the safety net actually is. - -## Decision - -`JustDummies` is tested by two sibling suites under one boundary: `JustDummies.PropertyTests` -owns every invariant that can be quantified over **generated constraint arguments**, -and `JustDummies.UnitTests` owns every contract whose subject is a specific, named -case — message content, argument validation, structural conventions, and dated -regressions. - -## Rationale - -The library's claim is a universal quantification, so the test that matches its -shape is one that quantifies. Generating the constraint arguments — the bounds, -lengths, counts, pools and seeds a caller declares — turns each test from an -instance of the claim into the claim itself, and moves the search into the space -where #206 actually lived. Sampling more values behind a fixed `Between(10, 20)` -explores none of it. - -A property framework also reports failures differently. Shrinking reduces a -counter-example to its minimal form, so a defect arrives as the smallest interval -and value that break it rather than as one opaque draw out of a few hundred. For a -component whose failures are arithmetic edge cases, that is the difference between -a diagnosis and a starting point. - -Drawing the constraints from an independent generator breaks the circularity noted -in Context: the suite no longer uses `JustDummies` to decide what to test -`JustDummies` with. FsCheck's own bias toward small values is compensated by -explicitly mixing in domain edges, since an off-by-one at `int.MaxValue` is -otherwise almost never drawn. - -The boundary is drawn where each style is genuinely stronger, not by kind of code. -Message content, null handling and reflection-driven conventions are not universal -claims; expressing them as properties would add quantification over inputs that do -not vary, obscure what is being asserted, and make the exact-wording assertions -harder to read. Keeping them in the example suite leaves each suite saying the kind -of thing it says best, and lets a failure's location already indicate what class of -contract broke. - -Dated regressions stay with the examples for the same reason. A regression pins a -defect that actually occurred, at the coordinates where it occurred; that specificity -is its value, and a property covering the same ground does not retire it. - -Two sibling projects rather than one mixed project follows the convention the -repository already applies twice, keeps the FsCheck dependency out of the suite that -does not need it, and lets each project state its own framework-floor story. - -## Alternatives Considered - -### Widen the sampled loops in the existing suite - -Raising the sample count and adding more hand-picked intervals is the cheapest -change and needs no new project. - -Rejected: it multiplies draws inside the same fixed constraint arguments. The -dimension left unexplored — the relation between an arbitrary bound and the produced -value — stays unexplored, so the class of defect that #206 belonged to remains -invisible. It buys runtime, not information. - -### Convert the whole test bed to properties - -A single suite is simpler to explain, and the invariant-shaped tests would all gain -from quantification. - -Rejected: the contracts described in Context are not universally quantified. A -property asserting that a conflict message names both constraints is a worse example -test — it quantifies over nothing while making the assertion harder to read — and the -reflection-driven convention guards have no input space at all. - -### Host the properties inside `JustDummies.UnitTests` - -One project is one thing to build, run and configure. - -Rejected: it puts two assertion styles and two dependency sets in one assembly, and -loses the signal that a failing project name already carries. It would also depart -from the sibling-project convention the repository has applied to `FirstClassErrors` -and `FirstClassErrors.RequestBinder`. - -### Generate the constraint arguments with `JustDummies` itself - -The library is a value generator, so it could supply its own test inputs and avoid a -dependency. - -Rejected: it deepens the circularity rather than breaking it. A generator defect -would then be free to bias the very inputs meant to expose it, and no failure could -be attributed with confidence. - -## Consequences - -### Positive - -* The library's universal claim is proven over a constraint space rather than at a - handful of points, and defects of the #206 class become reachable by the suite. -* A failing property arrives shrunk to a minimal counter-example. -* Each suite states one kind of contract, so a failure's location already classifies it. -* The property suite carries the .NET Framework 4.7.2 floor leg like its siblings, so - the invariants are proven against the `netstandard2.0` asset consumers actually load. -* The regular-expression round-trip is proven by an actual property, which lets ADR-0025's - claim be restated accurately — the audit finding that prompted this is closed by - construction rather than by editing the text. - -### Negative - -* Two suites must be kept in mind when a generator changes, and the boundary has to be - applied deliberately rather than by habit. -* An invariant already proven by a property may still be re-asserted by an example that - looks redundant when read in isolation; the redundancy is intended where the example - is a dated regression, and unintended otherwise. -* FsCheck's default generators need explicit edge-biasing to be useful here, which is - additional test-support code to maintain. - -### Risks - -* A property whose generated arguments straddle a value-dependent legality boundary - (ADR-0035) can be written so that it fails intermittently rather than deterministically. - A property must decide the expected outcome from the generated value, not from the call - shape. -* Statistical properties — that a range is reached, that both branches of a coin flip are - observed — are probabilistic, not universal. Written carelessly they flake; they belong - under a pinned seed and must be labelled as statistical guards. -* Pruning the example suite as properties land can silently reduce coverage if an example - is removed whose invariant the property does not in fact subsume. - -## Follow-up Actions - -* Revisit ADR-0025's "property test" wording, which the audit flagged as inaccurate, now that - a round-trip property exists. Only `@reefact` may amend or supersede an accepted ADR. - -## References - -* [Writing JustDummies tests](../WritingJustDummiesTests.en.md) — how this boundary is applied when adding a test -* [ADR-0025](0025-generate-strings-from-a-home-grown-regular-subset.md) — the regular subset whose round-trip this suite proves -* [ADR-0035](0035-enforce-structural-any-conflicts-at-compile-time.md) — structural versus value-dependent conflicts, which decides how a property must branch -* [ADR-0036](0036-draw-lattice-constrained-scalars-on-the-grid.md) — lattice constraints, whose grid invariant is quantified by the property suite -* [JustDummies architecture and design audit, 2026-07-20](../audit/2026-07-20-dummies-architecture-and-design-audit.md) — the "not property-based" finding on ADR-0025 -* Issue #206 — the decimal interval defect that motivated quantifying over bounds diff --git a/doc/handwritten/for-maintainers/adr/0041-draw-flag-enum-combinations-behind-an-opt-in.fr.md b/doc/handwritten/for-maintainers/adr/0041-draw-flag-enum-combinations-behind-an-opt-in.fr.md deleted file mode 100644 index 83842716..00000000 --- a/doc/handwritten/for-maintainers/adr/0041-draw-flag-enum-combinations-behind-an-opt-in.fr.md +++ /dev/null @@ -1,98 +0,0 @@ -# ADR-0041 | Tirer les combinaisons d'enums de drapeaux derrière un opt-in - -🌍 🇬🇧 [English](0041-draw-flag-enum-combinations-behind-an-opt-in.md) · 🇫🇷 Français (ce fichier) - -**Statut :** Accepté -**Proposé :** 2026-07-26 -**Accepté :** 2026-07-26 -**Décideurs :** Reefact - -## Contexte - -Un enum marqué `[Flags]` déclare des bits destinés à être combinés : ses membres ne sont pas des alternatives mais les parties d'un ensemble. Ses valeurs **valides** sont donc les combinaisons, alors que les valeurs qu'il **déclare** n'en sont que les parties — `Read | Write` est une valeur que le type est conçu pour porter et qu'il ne nomme jamais. La BCL le confirme deux fois : `Enum.GetValues` ne renvoie que les membres déclarés, et `Enum.IsDefined` répond `false` pour une combinaison. - -`AnyEnum` tire uniformément parmi les membres déclarés, un contrat que ses propres remarks énoncent. Pour un enum de drapeaux, cela signifie qu'un dummy porte au plus un bit : une branche qui en lit deux — la forme ordinaire du code consommant des drapeaux — n'est donc jamais exercée par une valeur JustDummies. C'est l'inverse de la raison d'être de la bibliothèque : la surface de contraintes existe pour faire remonter les hypothèses cachées, et ici le générateur en installe silencieusement une à son tour (« cette valeur a zéro ou un bit »). C'est la forme même d'un défaut d'atteignabilité, atteint par conception. - -Le générateur est tenu par trois règles permanentes de la bibliothèque. Il construit ses valeurs de manière constructive, en un tirage, sans jamais générer-puis-filtrer. Il détecte les contraintes contradictoires au moment de l'appel fluide qui les cause, en nommant les deux côtés. Et il annonce une cardinalité distincte via `ICardinalityHint`, ce qui permet à une collection distincte d'enums d'échouer à la déclaration plutôt qu'à la génération — la taille du domaine de tirage fait donc partie du contrat public, pas du détail d'implémentation. - -Deux propriétés des vrais enums de drapeaux comptent pour la forme du domaine. Un enum de drapeaux n'est pas tenu de déclarer un membre nul, et celui qui n'en déclare pas n'a aucune valeur « aucun drapeau » à rendre. Et un enum de drapeaux peut déclarer des **composites** — `ReadWrite = Read | Write`, `All = 7` — qui sont déjà des combinaisons, si bien que plusieurs sous-ensembles des membres déclarés retombent sur la même valeur. - -JustDummies n'a jamais été publié : le sens du tirage non contraint est donc encore libre d'être figé. L'audit du 2026-07-20 a recensé les combinaisons de drapeaux comme un ajout piloté par la demande (issue #226). - -## Décision - -`AnyEnum` continue de tirer parmi les membres déclarés par défaut et acquiert `AllowingCombinations()`, une contrainte explicite élargissant le tirage à la clôture par OU des membres déclarés — plus la valeur nulle lorsque l'enum en déclare une —, refusée sur un enum qui n'est pas `[Flags]` et sur un enum comptant plus de membres non nuls qu'il n'est possible d'énumérer. - -## Justification - -**Le défaut ne peut pas dépendre de l'attribut.** Faire que `Any.Enum()` se comporte différemment parce que le type porte `[Flags]` rendrait le tirage fonction des métadonnées d'un type plutôt que de ce que le test a écrit, c'est-à-dire exactement la classe de comportement implicite et à distance qu'ADR-0020 a retirée de cette bibliothèque en supprimant les conversions implicites. « Membres déclarés uniquement » est aussi le seul défaut *valide* pour les deux familles d'enums : un membre déclaré est toujours une valeur légitime, alors qu'une combinaison ne l'est que pour un enum de drapeaux. Le conserver coûte un appel à l'utilisateur de drapeaux et ne coûte rien à tous les autres. - -**En faire une contrainte, et non une seconde factory,** place le choix là où le lecteur cherche déjà la forme d'une valeur. `Any.Enum().AllowingCombinations()` se lit comme un élargissement du même générateur, se compose avec `OneOf`/`Except`/`DifferentFrom` à travers le pool existant, et ne demande aucun miroir sur `AnyContext` — la factory est inchangée, donc la surface miroir maintenue à la main ne grandit pas. - -**L'univers est la clôture par OU des membres déclarés, non celle des bits individuels.** Prendre les membres déclarés comme ensemble générateur absorbe un composite déclaré sans avoir à décider quels membres « sont » des bits : `ReadWrite = Read | Write` n'ajoute rien, et un enum dont les membres ne sont pas tous des puissances de deux ne demande aucun cas particulier. N'ajouter la valeur nulle que lorsqu'un membre nul est déclaré préserve la promesse que toute valeur tirée est une valeur définie par le type : un enum ne déclarant que `Left` et `Right` n'a pas de nom pour l'ensemble vide, et l'inventer serait précisément la valeur non déclarée que le défaut refuse. - -**Les exclusions continuent de comparer par égalité.** `Except(Read)` interdit la valeur `Read` et laisse `Read | Write` tirable. Lire le même appel comme un masque de bits sous l'opt-in ferait qu'une méthode signifie deux choses selon qu'une autre contrainte a été déclarée — la même implicité que le défaut rejette — et supprimerait silencieusement la majeure partie de l'univers. La bibliothèque distingue déjà les quasi-synonymes par le nom quand l'intention diffère : une exclusion au niveau du bit, si elle est un jour souhaitée, sera une contrainte nommée à part plutôt qu'une mutation de celle-ci. - -**Énumérer l'univers est ce qui préserve les deux garanties permanentes.** Un tirage indépendant par membre serait moins coûteux et sans plafond, mais il est uniforme sur les *sous-ensembles*, pas sur les *valeurs* : en présence d'un composite déclaré, plusieurs sous-ensembles retombent sur une même valeur, qui sort alors bien plus souvent que les autres — et un dummy biaisé est un échec plus grave qu'une contrainte refusée, parce que rien ne le révèle. Matérialiser la clôture garde aussi `ICardinalityHint` exact, ce qui préserve le conflit anticipé sur une collection distincte demandant plus de valeurs qu'il n'en existe. Le prix est que la clôture est exponentielle en nombre de membres : il lui faut un plafond. - -**Au-delà du plafond, la contrainte est refusée, pas dégradée.** Un repli silencieux scinderait le générateur en deux régimes — l'un uniforme et vérifié à la déclaration, l'autre ni l'un ni l'autre — que seul le comptage des membres d'un enum permettrait de distinguer. Refuser en nommant la cause, et pointer vers la liste explicite qui sert le cas, est la réponse qu'ADR-0025 a déjà donnée pour les constructions hors du sous-ensemble supporté : une erreur claire vaut mieux qu'une valeur dont l'appelant ne peut prévoir les propriétés. Un enum de drapeaux assez large pour atteindre le plafond est très loin des formes que le vrai code déclare. - -## Alternatives envisagées - -### Faire des combinaisons le défaut pour les enums `[Flags]` - -Envisagée parce qu'elle ne demande aucune API nouvelle et donne à l'utilisateur de drapeaux le bon domaine sans qu'il ait à le demander : « arbitraire mais valide » signifie sans doute déjà les combinaisons pour un type conçu pour les porter. - -Rejetée parce que le tirage dépendrait alors des métadonnées du type et non du texte du test : ajouter `[Flags]` à un enum existant changerait silencieusement tout dummy tiré de lui — et, avant cela, toute séquence seedée. Elle élargit aussi le domaine pour les nombreux enums de drapeaux dont les consommateurs ne passent jamais que des membres simples, où un dummy à deux bits est une surprise plutôt qu'une révélation. L'appel explicite coûte une ligne et rend l'élargissement lisible au point d'appel. - -### Tirer chaque membre par un pile-ou-face indépendant - -Envisagée parce qu'elle tient en quelques lignes, n'a pas de plafond et ne demande aucune énumération : on OR un sous-ensemble aléatoire des membres et le résultat est une combinaison valide par construction. - -Rejetée parce qu'elle est uniforme sur les sous-ensembles et non sur les valeurs, si bien que tout composite déclaré biaise fortement la distribution vers la valeur télescopée, et parce qu'elle ne peut annoncer aucune cardinalité distincte — ce qui ferait silencieusement passer les collections distinctes d'enums d'un conflit anticipé à un tirage borné, une régression par rapport au comportement actuel. - -### Exposer les combinaisons par une factory séparée - -Envisagée parce qu'un point d'entrée distinct énoncerait l'intention encore plus fort et pourrait porter sa propre surface de contraintes. - -Rejetée parce qu'elle duplique toute l'algèbre de contraintes des enums pour un seul élargissement, et parce qu'il faudrait la mirrorer sur `AnyContext`, faisant grandir la surface miroir que les gardes de parité existent pour surveiller. Une contrainte sur le builder existant se compose avec tout ce qui y est déjà. - -### Lire `Except` comme un masque de bits sous l'opt-in - -Envisagée parce que « aucune valeur portant le bit Read » est une demande plausible, et que réutiliser `Except` n'exigerait aucun nom nouveau. - -Rejetée parce qu'elle fait qu'une méthode signifie deux choses différentes selon qu'une autre contrainte a été déclarée, et parce qu'elle est la plus destructrice des deux lectures : exclure un bit retirerait la moitié de l'univers, ce qu'un appelant écrivant `Except` sur un enum n'a aucune raison d'attendre. Un nom distinct reste disponible pour ce besoin. - -## Conséquences - -### Positives - -* Un dummy de drapeaux peut porter les combinaisons que le type existe pour porter : une branche lisant deux bits est exercée. -* Le défaut est inchangé : aucun tirage existant, aucune séquence seedée, aucun comportement documenté ne bouge. -* Le domaine élargi passe par le hint de cardinalité existant, donc une collection distincte de combinaisons continue d'échouer à la déclaration plutôt qu'à la génération. -* Les refus — pas `[Flags]`, trop de membres — ont lieu à la déclaration et nomment leur cause, comme le reste de la surface de contraintes. - -### Négatives - -* L'utilisateur de drapeaux doit savoir que l'appel existe ; un générateur tirant des membres simples reste le défaut qu'il rencontre d'abord. -* L'univers est matérialisé : un enum proche du plafond coûte de la mémoire et un calcul unique proportionnels à son nombre de combinaisons. -* L'opt-in est sensible à l'ordre face à une liste d'autorisation nommant des combinaisons : appliqué après `OneOf`, il élargit un univers que la liste a déjà épinglé, et ne change donc rien. - -### Risques - -* Un enum assez large pour être refusé est un type supporté dont les combinaisons ne peuvent pas être tirées du tout. Mitigation : le message nomme le plafond et pointe vers la liste explicite ; la forme est très loin de ce que le vrai code déclare, et le plafond peut être relevé par une décision ultérieure sur preuves. -* La sensibilité à l'ordre avec `OneOf` pourrait se lire comme un no-op silencieux. Mitigation : documentée sur les deux membres, et l'ordre inverse — une liste nommant une combinaison avant l'opt-in — échoue avec un message nommant la contrainte manquante plutôt que de l'accepter. - -## Actions de suivi - -* Si une exclusion au niveau du bit est demandée, l'introduire sous son propre nom plutôt qu'en élargissant `Except`. -* Revisiter le plafond d'énumération si un vrai enum de drapeaux est un jour signalé contre lui. - -## Références - -* [ADR-0020](0020-materialize-dummies-only-through-generate.fr.md) — la suppression du comportement implicite piloté par les métadonnées dont le défaut reprend le raisonnement, et l'argument de calendrier pré-1.0 réutilisé ici. -* [ADR-0025](0025-generate-strings-from-a-home-grown-regular-subset.fr.md) — la règle « un refus clair vaut mieux qu'une valeur imprévisible » que le plafond applique. -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.fr.md) — le contrat du hint de cardinalité que l'univers matérialisé garde exact. -* [ADR-0032](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.fr.md) — le tirage sur pool explicite vers lequel pointe le message du plafond. -* [ADR-0037](0037-vary-the-datetimeoffset-offset-dimension.fr.md) — le précédent d'une dimension optionnelle dont le défaut reste intouché, y compris la même interaction d'énumération terminale avec `OneOf`. -* Issue [#226](https://github.com/Reefact/first-class-errors/issues/226) — l'entrée de backlog que ceci résout. diff --git a/doc/handwritten/for-maintainers/adr/0041-draw-flag-enum-combinations-behind-an-opt-in.md b/doc/handwritten/for-maintainers/adr/0041-draw-flag-enum-combinations-behind-an-opt-in.md deleted file mode 100644 index f2aaf780..00000000 --- a/doc/handwritten/for-maintainers/adr/0041-draw-flag-enum-combinations-behind-an-opt-in.md +++ /dev/null @@ -1,98 +0,0 @@ -# ADR-0041 | Draw flag-enum combinations behind an opt-in - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0041-draw-flag-enum-combinations-behind-an-opt-in.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-26 -**Accepted:** 2026-07-26 -**Decision Makers:** Reefact - -## Context - -An enum marked `[Flags]` declares bits meant to be combined: its members are not alternatives but the parts of a set. Its **valid** values are therefore the combinations, while the values it **declares** are only the parts — `Read | Write` is a value the type is designed to hold and never names. The BCL agrees on both counts: `Enum.GetValues` returns the declared members only, and `Enum.IsDefined` answers `false` for a combination. - -`AnyEnum` draws uniformly from the declared members, a contract its own remarks state. For a flags enum this means a dummy carries at most one bit, so a branch reading two — the ordinary shape of flag-consuming code — is never exercised by a JustDummies value. That is the inverse of what the library exists for: the constraint surface is meant to surface hidden assumptions, and here the generator silently installs one of its own ("this value has zero or one bit"). It is the same shape as a reachability defect, except reached by design. - -The generator is bound by three of the library's standing rules. It builds values constructively in one draw and never generates-then-filters. It detects contradictory constraints eagerly, at the fluent call that caused them, naming both sides. And it advertises a distinct cardinality through `ICardinalityHint`, which is what lets a distinct collection over an enum fail at declaration rather than at generation — so the size of the draw domain is part of the public contract, not an implementation detail. - -Two properties of real flags enums matter for the domain's shape. A flags enum need not declare a zero member, and one that does not has no "no flags" value to yield. And a flags enum may declare **composites** — `ReadWrite = Read | Write`, `All = 7` — which are combinations already, so several subsets of the declared members collapse onto the same value. - -JustDummies has never been released, so the meaning of the unconstrained draw is still free to be fixed. The audit of 2026-07-20 recorded flag combinations as a demand-driven addition (issue #226). - -## Decision - -`AnyEnum` keeps drawing from the declared members by default and gains `AllowingCombinations()`, an explicit constraint widening the draw to the OR-closure of the declared members — plus the zero value when the enum declares one — refused on an enum that is not `[Flags]` and on one with more non-zero members than can be enumerated. - -## Rationale - -**The default cannot depend on the attribute.** Making `Any.Enum()` behave differently because the type carries `[Flags]` would make the draw a function of a type's metadata rather than of what the test wrote, which is the class of implicit, action-at-a-distance behaviour ADR-0020 removed from this library when it deleted the implicit conversions. Declared-members-only is also the sole default that is *valid* for both enum families: a declared member is always a legitimate value, whereas a combination is legitimate only for a flags enum. Keeping it costs the flags user one call and costs everyone else nothing. - -**Making it a constraint, not a second factory,** puts the choice where the reader already looks for the shape of a value. `Any.Enum().AllowingCombinations()` reads as a widening of the same generator, composes with `OneOf`/`Except`/`DifferentFrom` through the existing pool, and needs no mirror on `AnyContext` — the factory is unchanged, so the hand-mirrored surface does not grow. - -**The universe is the OR-closure of the declared members, not of the individual bits.** Taking the declared members as the generating set absorbs a declared composite without having to decide which members "are" bits: `ReadWrite = Read | Write` contributes nothing new, and an enum whose members are not all powers of two needs no special case. Adding the zero value only when a zero member is declared keeps the promise that every drawn value is one the type defines: an enum declaring `Left` and `Right` alone has no name for the empty set, and inventing it would be exactly the undeclared value the declared-members default refuses. - -**Exclusions keep comparing by equality.** `Except(Read)` forbids the value `Read` and leaves `Read | Write` drawable. Reading the same call as a bit mask under the opt-in would make one method mean two things depending on another constraint — the same implicitness the default rejects — and would silently delete most of the universe. The library already distinguishes near-synonyms by name when the intent differs, so a bit-level exclusion, if it is ever wanted, is a separate named constraint rather than a mutation of this one. - -**Enumerating the universe is what keeps the two standing guarantees.** A per-member coin flip would be cheaper and unbounded, but it is uniform over *subsets*, not over *values*: with a declared composite, several subsets collapse onto one value, and that value is then drawn far more often than the others — a biased dummy is a worse failure than a refused constraint, because nothing reveals it. Materializing the closure also keeps `ICardinalityHint` exact, which is what preserves the eager conflict on a distinct collection asking for more values than exist. The cost is that the closure is exponential in the number of members, so it needs a ceiling. - -**Beyond the ceiling the constraint is refused, not degraded.** A silent fallback would split the generator into two regimes — one uniform and eagerly-checked, one neither — distinguishable only by counting an enum's members. Refusing by name, and pointing at the explicit allow-list that serves the case, is the answer ADR-0025 already gave for constructs outside the supported subset: a clear error beats a value whose properties the caller cannot predict. A flags enum wide enough to hit the ceiling is far outside the shapes real code declares. - -## Alternatives Considered - -### Make combinations the default for `[Flags]` enums - -Considered because it needs no new API and gives the flags user the right domain without asking: arguably "arbitrary yet valid" already means the combinations for a type designed to hold them. - -Rejected because the draw would then depend on the type's metadata rather than on the test's text, so adding `[Flags]` to an existing enum would silently change every dummy drawn from it — and, before that, change every seeded sequence. It also widens the domain for the many flags enums whose consumers only ever pass single members, where a two-bit dummy is a surprise rather than a revelation. The explicit call costs one line and makes the widening legible at the call site. - -### Draw each member with an independent coin flip - -Considered because it is a few lines, has no ceiling, and needs no enumeration at all: OR a random subset of the members and the result is a valid combination by construction. - -Rejected because it is uniform over subsets rather than over values, so any declared composite skews the distribution heavily towards the collapsed value, and because it cannot report a distinct cardinality — which would silently drop distinct collections over an enum from an eager conflict to a bounded draw, a regression against today's behaviour. - -### Expose the combinations through a separate factory - -Considered because a distinct entry point would state the intent even more loudly and could carry its own constraint surface. - -Rejected because it duplicates the whole enum constraint algebra for one widening, and because it would have to be mirrored on `AnyContext`, growing the hand-mirrored surface the parity guards exist to police. A constraint on the existing builder composes with everything already there. - -### Read `Except` as a bit mask under the opt-in - -Considered because "no value carrying the Read bit" is a plausible thing a test wants, and reusing `Except` would need no new name. - -Rejected because it makes one method mean two different things depending on whether another constraint was declared, and because it is the more destructive of the two readings: excluding one bit would remove half the universe, which a caller writing `Except` on an enum has no reason to expect. A distinct name remains available for that need. - -## Consequences - -### Positive - -* A flags dummy can carry the combinations the type exists to hold, so a branch reading two bits is exercised. -* The default is unchanged, so no existing draw, seeded sequence, or documented behaviour moves. -* The widened domain flows through the existing cardinality hint, so a distinct collection over combinations keeps failing eagerly rather than at generation. -* The refusals — not `[Flags]`, too many members — are declaration-time and name their cause, consistent with the rest of the constraint surface. - -### Negative - -* The flags user must know the call exists; a generator drawing single members remains the default they meet first. -* The universe is materialized, so an enum near the ceiling costs memory and one-off computation proportional to its combination count. -* The opt-in is order-sensitive with respect to an allow-list naming combinations: applied after `OneOf`, it widens a universe the allow-list has already pinned, so it changes nothing. - -### Risks - -* An enum wide enough to be refused is a supported type whose combinations cannot be drawn at all. Mitigation: the message names the ceiling and points at the explicit allow-list; the shape is far outside what real code declares, and the ceiling can be raised by a later decision on evidence. -* The order-sensitivity with `OneOf` could read as a silent no-op. Mitigation: documented on both members, and the reverse order — an allow-list naming a combination before the opt-in — fails with a message naming the missing constraint rather than accepting it. - -## Follow-up Actions - -* Should a bit-level exclusion be requested, introduce it under its own name rather than by widening `Except`. -* Revisit the enumeration ceiling if a real flags enum is ever reported against it. - -## References - -* [ADR-0020](0020-materialize-dummies-only-through-generate.md) — the removal of implicit, metadata-driven behaviour whose reasoning the default follows, and the pre-1.0 timing argument reused here. -* [ADR-0025](0025-generate-strings-from-a-home-grown-regular-subset.md) — the "a clear refusal beats an unpredictable value" rule the ceiling applies. -* [ADR-0013](0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) — the cardinality-hint contract the materialized universe keeps exact. -* [ADR-0032](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) — the explicit-pool draw the ceiling's message points at. -* [ADR-0037](0037-vary-the-datetimeoffset-offset-dimension.md) — the precedent for an opt-in extra dimension whose default is left untouched, including the same `OneOf` terminal-enumeration interaction. -* Issue [#226](https://github.com/Reefact/first-class-errors/issues/226) — the backlog entry this resolves. diff --git a/doc/handwritten/for-maintainers/adr/0042-serialize-draws-on-a-random-source.fr.md b/doc/handwritten/for-maintainers/adr/0042-serialize-draws-on-a-random-source.fr.md deleted file mode 100644 index 7c5100fd..00000000 --- a/doc/handwritten/for-maintainers/adr/0042-serialize-draws-on-a-random-source.fr.md +++ /dev/null @@ -1,91 +0,0 @@ -# ADR-0042 | Sérialiser les tirages sur une source aléatoire, et borner la reproductibilité à la séquence de tirages - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0042-serialize-draws-on-a-random-source.md) - -**Statut :** Accepté -**Proposé :** 2026-07-27 -**Accepté :** 2026-07-27 -**Décideurs :** Reefact - -## Contexte - -`JustDummies` tire chaque valeur arbitraire d'une `RandomSource`, qui possède un `System.Random`. Il existe deux sources : l'ambiante, derrière les points d'entrée statiques `Any`, dont l'état vit dans un `AsyncLocal`, et la source fixe que possède un `AnyContext` issu de `Any.WithSeed`. - -`System.Random` n'est pas thread-safe. Son implémentation semée mute un tableau et deux index à chaque tirage sans aucune synchronisation ; sous contention, les deux index peuvent converger, après quoi le générateur retourne zéro définitivement. Rien ne le réinitialise. Comme la couche des valeurs projette un tirage nul sur le bas de la plage déclarée, chaque générateur se fige alors sur le minimum de son propre domaine — `0`, `""`, `Guid.Empty`, `int.MinValue` — pour toute la durée de vie restante de cette source, et aucune exception n'est levée. - -Une source atteint plusieurs threads par deux voies ordinaires, dont aucune n'est un mésusage. - -* Un `AsyncLocal` **descend** dans les tâches et les threads que son propriétaire démarre. Dès qu'une portée de graine est installée — ce que `Any.Reproducibly` et `Any.UseSeed` font toujours — un `Parallel.For` ou un `Task.WhenAll` à l'intérieur du test remet la même source à chaque worker. Hors portée de graine, l'état ambiant est créé paresseusement : chaque worker écrit son propre emplacement et obtient son propre générateur. Le chemin non semé est donc indemne, et c'est l'épinglage d'une graine qui crée le partage. -* Un `AnyContext` est un objet ; qui le détient peut le partager. - -L'ADR-0006 a enregistré la décision d'origine et a nommé ce danger exactement — *« a single shared, mutable `System.Random` is not thread-safe and would produce cross-test interference and non-reproducible values »* — puis a retenu la localité de contexte par `AsyncLocal` comme remède. Ce remède traite l'axe **inter-tests** : deux tests en parallèle ne voient jamais la graine l'un de l'autre, ce qui est vrai et fait l'objet d'un garde-fou distinct. Il ne traite pas l'axe **intra-test**, que l'ADR n'envisage pas ; et le mécanisme qu'il choisit est précisément ce qui propage l'instance partagée. L'ADR-0006 est remplacé par l'ADR-0026, qui rebase `FirstClassErrors.Testing` sur `JustDummies` sans rouvrir la question. - -Deux propriétés de la bibliothèque pèsent sur le remède. Les tirages se situent sur des chemins d'erreur et d'arrangement, jamais dans des boucles chaudes. Et `Any.UseSeed` est public depuis l'ADR-0038 — ouvert pour les adaptateurs de framework de test, mais utilisable par n'importe quel appelant, y compris dans le corps d'une boucle parallèle, où l'emplacement `AsyncLocal` propre à chaque worker rend la portée privée à cette itération. - -Les promesses affichées du paquet sont que les valeurs sont arbitraires mais valides, et qu'une exécution est reproductible à partir d'une graine rapportée. La documentation utilisateur indique que la source est *« safe under parallel tests »* et explique l'`AsyncLocal` ; les remarques d'`AnyContext` indiquent qu'il est *« not thread-safe »* sans que rien ne le fasse respecter. Aucune des deux sources n'est protégée, et les deux disent des choses différentes. - -`JustDummies` est pré-1.0 et non publié (ADR-0011) : le contrat peut encore être posé plutôt que corrigé. - -## Décision - -Chaque tirage sur une source aléatoire est sérialisé sur le verrou propre à cette source, et la promesse de reproductibilité est bornée à une séquence de tirages pris un à la fois — une exécution parallèle ne rejoue que si chaque unité de travail ouvre sa propre portée de graine. - -## Justification - -* **Le défaut est un danger de mutation, non de portée : le remède appartient donc là où se trouve la mutation.** L'`AsyncLocal` répond à *quelle* source est en vigueur ; il n'a jamais pu répondre à *comment* on y touche. Ajouter un verrou laisse intacte la décision de portée de l'ADR-0006 et fournit la propriété qu'on lui prêtait à tort. Retirer l'`AsyncLocal` casserait l'isolation inter-tests qu'il assure réellement, et le remplacer par un stockage lié au thread perdrait la graine au premier `await`. -* **Sérialiser ne coûte rien qui compte, et préserve toutes les exécutions existantes.** Un verrou non contendu ne change pas l'ordre dans lequel un thread unique consomme le flux : une graine épinglée rejoue donc à l'identique bit à bit — la propriété qui permet de livrer ceci sans invalider un seul test, ici comme chez un consommateur. Sur des chemins qui relèvent de l'arrangement et non du calcul, le coût du verrou est négligeable. -* **Le générateur doit être l'unique porte, pour qu'un contournement ne compile pas.** Livrer le `Random` sous-jacent derrière une façade synchronisée laisserait silencieusement non protégé tout membre que la façade ne redéfinit pas, et le prochain tirage ajouté déciderait par accident s'il est sûr. Garder l'instance privée transforme cela en erreur de compilation. C'est le raisonnement que les ADR-0031 et ADR-0035 appliquent ailleurs : rendre la règle incassable plutôt que seulement vérifiée. -* **La promesse doit se réduire à ce que la sérialisation apporte réellement.** Le verrou est pris par tirage primitif, et une seule valeur générée peut en consommer beaucoup — une chaîne tire un caractère à la fois — de sorte que deux threads s'entrelacent *à l'intérieur* d'une même génération. Ni la séquence ni le multiensemble des valeurs générées ne sont donc stables sous parallélisme, et une promesse de reproductibilité parallèle serait fausse. Énoncer la garantie la plus étroite est ce qui maintient la fiabilité du rapport de graine — la même exigence que la bibliothèque applique déjà lorsqu'elle retient sa promesse de rejeu complet face à un générateur étranger. -* **La promesse réduite ne coûte rien à l'utilisateur, car la plus large est déjà atteignable.** Une portée ouverte dans le corps d'une boucle parallèle est privée à son worker : dériver une graine par unité de travail à partir de celle de l'exécution fait rejouer l'ensemble. Ce mécanisme est déjà public, donc la décision n'ajoute aucune surface : elle documente une capacité au lieu de la construire. -* **Une seule règle pour les deux sources supprime une contradiction.** Verrouiller le point de passage commun protège d'un coup la source ambiante et `AnyContext`, ce qui permet de remplacer la remarque « not thread-safe » non appliquée de ce dernier par le contrat désormais vrai pour les deux. - -## Alternatives envisagées - -### Donner à chaque thread son propre générateur - -Supprime la corruption sans verrou, en dérivant un générateur par thread à partir de la graine de l'exécution. Rejetée parce qu'elle détruit la propriété pour laquelle la bibliothèque existe : la correspondance thread → sous-flux est fixée par l'ordonnanceur, donc la même graine produit des valeurs différentes d'une exécution à l'autre. Dans une bibliothèque semée, le nombre de générateurs et leur propriété *sont* le contrat de reproductibilité, pas un détail d'implémentation — la raison même pour laquelle ceci ne peut pas être traité comme un choix local de sûreté d'accès. - -### Lever une exception sur usage concurrent d'une source - -La branche « interdire explicitement », et la plus conforme à l'habitude de la bibliothèque d'échouer vite sur une contradiction. Rejetée pour deux motifs. Un tirage concurrent n'est pas une contradiction : un test qui parallélise sans avoir besoin d'un rejeu appel par appel est légitime, et sous verrou il fonctionne. Et la détection n'est pas fiable dans la forme qui compte — un test `async` reprend légitimement sur un autre thread sans la moindre concurrence, donc toute vérification d'affinité de thread rejetterait du code correct, tandis qu'un vrai détecteur de chevauchement coûte ce que coûte un verrou en apportant moins. - -### Utiliser un générateur thread-safe de la plateforme - -`Random.Shared` est thread-safe et sans verrou. Rejetée parce qu'il ne peut pas être semé, ce qui condamne toute la surface de reproductibilité, et parce qu'il n'existe pas sur la cible `netstandard2.0` sur laquelle la bibliothèque plancher (ADR-0022). - -### Ne rien faire et documenter la limitation - -Rejetée parce que la défaillance est silencieuse et que son résultat est indiscernable d'une valeur légitime : un dummy devenu `0`, `""` ou `Guid.Empty` est exactement la valeur la plus susceptible de faire passer une assertion pour la mauvaise raison. Une limitation qu'un utilisateur ne peut ni observer ni détecter n'est pas une limitation que la documentation peut solder. - -## Conséquences - -### Positives - -* Des tirages concurrents ne peuvent plus dégrader une source, ni sur le chemin ambiant ni sur celui du contexte, et une source reste utilisable pour les tirages séquentiels pris après une section parallèle. -* Les exécutions semées existantes sont inchangées : les séquences mono-thread sont identiques bit à bit. -* Les deux sources énoncent un seul contrat au lieu de deux contradictoires. -* La génération parallèle reproductible devient une recette exprimable et documentée, au lieu d'une impossibilité tue. - -### Négatives - -* Chaque tirage passe par un verrou, y compris l'immense majorité qui est mono-thread et ne peut pas contendre. -* La promesse de reproductibilité est désormais explicitement conditionnelle, ce qui est une phrase plus faible à écrire dans la documentation que celle qu'un lecteur aurait pu supposer. -* Les appelants de la source interne passent désormais par ses méthodes plutôt que par un `Random` : un futur tirage primitif devra être ajouté à ce type avant de pouvoir être utilisé. - -### Risques - -* Un utilisateur qui parallélise en attendant que la graine seule rejoue l'exécution constatera que non. Atténuation : la condition est énoncée dans la documentation XML du tirage et des deux points d'entrée de graine, et la recette par unité de travail est documentée dans le guide utilisateur. -* La sérialisation rend un arrangement *pathologiquement* parallèle plus lent qu'il ne le serait autrement. Accepté : la génération de dummies n'est pas un chemin chaud, et l'alternative est une corruption silencieuse. - -## Actions de suivi - -* À revisiter seulement si une charge mesurée montre que le verrou est significatif, ce qui reviendrait à rouvrir l'idée des sous-flux par unité de travail comme couture *publique* plutôt que comme substitution interne. - -## Références - -* Issue [#310](https://github.com/Reefact/first-class-errors/issues/310) — le défaut et ses mesures. -* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.fr.md) — la décision d'origine sur la source semable, qui nomme le danger et ne traite que l'axe inter-tests. -* [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.fr.md) — remplace l'ADR-0006 sans rouvrir la question. -* [ADR-0038](0038-open-the-ambient-seed-scope-to-adapters.fr.md) — rend publique la portée de graine ambiante, ce qui met la recette par unité de travail à portée. -* [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.fr.md) — le plancher `netstandard2.0` qui écarte `Random.Shared`. -* [ADR-0031](0031-name-any-factories-after-their-clr-type.fr.md), [ADR-0035](0035-enforce-structural-any-conflicts-at-compile-time.fr.md) — le précédent « rendre la règle incassable plutôt que seulement vérifiée ». diff --git a/doc/handwritten/for-maintainers/adr/0042-serialize-draws-on-a-random-source.md b/doc/handwritten/for-maintainers/adr/0042-serialize-draws-on-a-random-source.md deleted file mode 100644 index 49ab9699..00000000 --- a/doc/handwritten/for-maintainers/adr/0042-serialize-draws-on-a-random-source.md +++ /dev/null @@ -1,91 +0,0 @@ -# ADR-0042 | Serialize draws on a random source, and scope reproducibility to the draw sequence - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0042-serialize-draws-on-a-random-source.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-27 -**Accepted:** 2026-07-27 -**Decision Makers:** Reefact - -## Context - -`JustDummies` draws every arbitrary value from a `RandomSource`, which owns one `System.Random`. Two sources exist: the ambient one behind the static `Any` entry points, whose state lives in an `AsyncLocal`, and the fixed one owned by an `AnyContext` from `Any.WithSeed`. - -`System.Random` is not thread-safe. Its seeded implementation mutates an array and two indices on every draw with no synchronisation; under contention the two indices can converge, after which the generator returns zero permanently. Nothing resets it. Because the value layer maps a zero draw onto the bottom of whatever range was declared, every generator then settles on the minimum of its own domain — `0`, `""`, `Guid.Empty`, `int.MinValue` — for the remaining life of that source, and no exception is raised. - -A source reaches several threads by two ordinary routes, neither of which is a misuse. - -* An `AsyncLocal` **flows into** the tasks and threads its owner starts. Once a seed scope is installed — which `Any.Reproducibly` and `Any.UseSeed` always do — a `Parallel.For` or a `Task.WhenAll` inside the test hands the same source to every worker. Outside a seed scope the ambient state is created lazily, so each worker writes its own slot and gets its own generator: the unseeded path is unaffected, and pinning a seed is what creates the sharing. -* An `AnyContext` is an object; whoever holds it can share it. - -ADR-0006 recorded the original decision and named this hazard exactly — *"a single shared, mutable `System.Random` is not thread-safe and would produce cross-test interference and non-reproducible values"* — then chose `AsyncLocal` context-locality as the remedy. That remedy addresses the **cross-test** axis: two tests running in parallel never see each other's seed, which holds and is separately guarded. It does not address the **intra-test** axis, which the ADR does not consider; and the mechanism it selects is what propagates the shared instance. ADR-0006 is superseded by ADR-0026, which rebases `FirstClassErrors.Testing` onto `JustDummies` and does not revisit the question. - -Two properties of the library bear on the remedy. Draws sit on error and arrangement paths, never in hot loops. And `Any.UseSeed` is public since ADR-0038 — opened for test-framework adapters, but usable by any caller, including inside a parallel loop body, where each worker's own `AsyncLocal` slot makes the scope private to that iteration. - -The package's stated promises are that values are arbitrary yet valid, and that a run is reproducible from a reported seed. The user documentation states that the source is *"safe under parallel tests"* and explains the `AsyncLocal`; `AnyContext`'s remarks state it is *"not thread-safe"* without anything enforcing it. Neither source is protected, and the two say different things. - -`JustDummies` is pre-1.0 and unpublished (ADR-0011), so the contract can still be set rather than corrected. - -## Decision - -Every draw on a random source is serialized on that source's own lock, and the reproducibility promise is scoped to a sequence of draws taken one at a time — a parallel run replays only when each unit of work opens its own seed scope. - -## Rationale - -* **The defect is a mutation hazard, not a scoping one, so the remedy belongs where the mutation is.** `AsyncLocal` answers *which* source is in effect; it was never able to answer *how* that source is touched. Adding a lock leaves the scoping decision of ADR-0006 intact and supplies the property it was mistakenly believed to provide. Removing the `AsyncLocal` would break the cross-test isolation it does provide, and replacing it with thread-affine storage would lose the seed at the first `await`. -* **Serializing costs nothing that matters, and preserves every existing run.** An uncontended lock does not change the order in which a single thread consumes the stream, so a pinned seed replays bit-identically — the property that lets this ship without invalidating any test, in this repository or in a consumer's. On paths that are arrangement rather than computation, the cost of the lock is immaterial. -* **The generator must be the only door, so that bypassing it cannot compile.** Handing out the underlying `Random` behind a synchronized façade would leave every member the façade does not override silently unprotected, and the next draw added would decide by accident whether it is safe. Keeping the instance private turns that into a compile error. This is the same reasoning ADR-0031 and ADR-0035 apply elsewhere: make the rule un-break-able rather than merely checked. -* **The promise has to shrink to what serialization actually delivers.** The lock is taken per primitive draw, and a single generated value may consume many — a string draws once per character — so two threads interleave *inside* one generation. Neither the sequence nor the multiset of generated values is therefore stable under parallelism, and a promise of parallel reproducibility would be false. Stating the narrower guarantee is what keeps the seed report trustworthy, which is the same standard the library already applies when it withholds a full-replay claim for a foreign generator. -* **The narrower promise costs the user nothing, because the wider one is already reachable.** A scope opened inside a parallel loop body is private to its worker, so deriving one seed per unit of work from the run's seed makes the whole run replay. That mechanism is already public, so the decision adds no surface: it documents a capability rather than building one. -* **One rule for both sources removes a contradiction.** Locking the shared choke point protects the ambient source and `AnyContext` alike, which lets the latter's unenforced "not thread-safe" remark be replaced by the contract that now holds for both. - -## Alternatives Considered - -### Give each thread its own generator - -Removes the corruption without a lock, by deriving a per-thread generator from the run's seed. Rejected because it destroys the property the library exists to provide: the mapping from thread to sub-stream is set by the scheduler, so the same seed yields different values from one run to the next. In a seeded library the number of generators and their ownership *is* the reproducibility contract, not an implementation detail — the very reason this cannot be treated as a local choice about thread safety. - -### Throw when a source is drawn from concurrently - -The "forbid it explicitly" branch, and the one most aligned with the library's habit of failing fast on a contradiction. Rejected on two counts. A concurrent draw is not a contradiction: a test that parallelises without needing a per-call replay is legitimate, and under a lock it works. And the detection is unsound in the shape that matters — an `async` test legitimately resumes on another thread with no concurrency at all, so any thread-affinity check would reject correct code, while a true overlap detector costs what a lock costs and delivers less. - -### Use a thread-safe generator from the platform - -`Random.Shared` is thread-safe and lock-free. Rejected because it cannot be seeded, which forecloses the entire reproducibility surface, and because it does not exist on the `netstandard2.0` target the library floors on (ADR-0022). - -### Leave it and document the limitation - -Rejected because the failure is silent and its result is indistinguishable from a legitimate value: a dummy that becomes `0`, `""` or `Guid.Empty` is exactly the value most likely to make an assertion pass for the wrong reason. A limitation a user can neither observe nor detect is not one documentation can discharge. - -## Consequences - -### Positive - -* Concurrent draws can no longer degrade a source, on either the ambient or the context path, and a source stays usable for the sequential draws taken after a parallel section. -* Existing seeded runs are unaffected: single-threaded sequences are bit-identical. -* The two sources state one contract instead of two contradictory ones. -* Reproducible parallel generation becomes an expressible, documented recipe rather than an unstated impossibility. - -### Negative - -* Every draw goes through a lock, including the overwhelming majority that are single-threaded and cannot contend. -* The reproducibility promise is now explicitly conditional, which is a weaker sentence to write in the documentation than the one a reader might have assumed. -* Callers of the internal source now go through its methods rather than a `Random`, so a future draw primitive must be added to that type before it can be used. - -### Risks - -* A user who parallelises and expects the seed alone to replay the run will find it does not. Mitigation: the condition is stated in the XML documentation of the draw and of both seeding entry points, and the per-work-item recipe is documented in the user guide. -* Serialization makes a *pathologically* parallel arrangement slower than it would otherwise be. Accepted: dummy generation is not a hot path, and the alternative is silent corruption. - -## Follow-up Actions - -* Revisit only if a measured workload shows the lock to be material, which would mean reopening the per-work-item sub-stream idea as a *public* seam rather than as an internal substitution. - -## References - -* Issue [#310](https://github.com/Reefact/first-class-errors/issues/310) — the defect and its measurements. -* [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) — the original seedable-source decision, which named the hazard and addressed the cross-test axis only. -* [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.md) — supersedes ADR-0006 without revisiting the question. -* [ADR-0038](0038-open-the-ambient-seed-scope-to-adapters.md) — makes the ambient seed scope public, which is what puts the per-work-item recipe within reach. -* [ADR-0022](0022-floor-the-library-on-net-framework-4-7-2.md) — the `netstandard2.0` floor that rules out `Random.Shared`. -* [ADR-0031](0031-name-any-factories-after-their-clr-type.md), [ADR-0035](0035-enforce-structural-any-conflicts-at-compile-time.md) — the "make the rule un-break-able rather than merely checked" precedent. diff --git a/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.fr.md b/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.fr.md index cb3331a9..dbf57d33 100644 --- a/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.fr.md +++ b/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.fr.md @@ -315,7 +315,7 @@ snapshots et tests lanceurs de processus compris. * [ADR-0001](0001-lock-the-analyzer-roslyn-floor.fr.md) — le précédent en matière d'épinglage d'une version d'outil qui, sinon, déplacerait seule un résultat mesuré. -* [ADR-0040](0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md) +* [just-dummies ADR-0019](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0019-split-the-justdummies-test-bed-between-example-and-property-suites.md) — le découpage du banc de tests dont les deux suites alimentent ce barrage. * [stryker-net#3117](https://github.com/stryker-mutator/stryker-net/issues/3117) — le signalement amont du runner VSTest de Stryker face à xUnit v3. diff --git a/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.md b/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.md index 825dc6f2..40d89af1 100644 --- a/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.md +++ b/doc/handwritten/for-maintainers/adr/0043-gate-pull-requests-on-the-mutation-score-of-the-diff.md @@ -289,7 +289,7 @@ of them — snapshot suites and process-spawning tests included. is implemented, and the knobs it exposes. * [ADR-0001](0001-lock-the-analyzer-roslyn-floor.md) — the precedent for pinning a tool version that would otherwise move a measured result on its own. -* [ADR-0040](0040-split-the-justdummies-test-bed-between-example-and-property-suites.md) +* [just-dummies ADR-0019](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0019-split-the-justdummies-test-bed-between-example-and-property-suites.md) — the test-bed split whose two suites both feed this gate. * [stryker-net#3117](https://github.com/stryker-mutator/stryker-net/issues/3117) — the upstream report of Stryker's VSTest runner mishandling xUnit v3. diff --git a/doc/handwritten/for-maintainers/adr/0044-ship-justdummies-analyzers.fr.md b/doc/handwritten/for-maintainers/adr/0044-ship-justdummies-analyzers.fr.md deleted file mode 100644 index 38fc5121..00000000 --- a/doc/handwritten/for-maintainers/adr/0044-ship-justdummies-analyzers.fr.md +++ /dev/null @@ -1,138 +0,0 @@ -# ADR-0044 | Fournir des analyseurs JustDummies de première partie, et garder avec eux la surface asynchrone reproductible - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0044-ship-justdummies-analyzers.md) - -**Statut :** Accepté -**Proposé :** 2026-07-27 -**Accepté :** 2026-07-27 -**Décideurs :** Reefact - -## Contexte - -* `JustDummies` est une bibliothèque de support de test : toute sa valeur tient à ce qu'un arrangement cassé - *échoue*. `Any.Reproducibly` exécute un corps de test sous une graine épinglée et la rapporte en cas d'échec. Elle - surchargeait sur un seul nom une `Action` synchrone et un `Func` asynchrone. -* Ce jeu de surcharges cachait un piège à échec silencieux. Une lambda `async` est une meilleure conversion vers - `Func` que vers `Action`, donc `Any.Reproducibly(async () => { ... })` se liait à la surcharge asynchrone, qui - retournait un `Task`. Une méthode de test est en général un `void` synchrone : le `Task` retourné était jeté ; les - assertions du corps s'exécutaient sur une continuation après le retour de la méthode, et l'échec ne surgissait — au - mieux — que plus tard sous forme d'`UnobservedTaskException`. **Le test passait au vert.** Le `CS4014` natif du - compilateur ne se déclenche pas dans une méthode synchrone : rien n'avertissait. -* Renommer la surcharge asynchrone en `ReproduciblyAsync`, conforme au TAP, corrige le nommage, mais seul il *rouvre* - le piège de l'autre côté : `Reproducibly` n'ayant plus que des surcharges `Action`, une lambda `async` se lie à - `Action` en **`async void`**, dont l'exception d'après le premier `await` échappe entièrement au `try/catch` de la - portée reproductible. -* C# n'offre aucun `Task` non-jetable, ni aucun moyen d'interdire une conversion lambda-`async`→`Action`. Les deux - erreurs résiduelles — passer un corps async à `Reproducibly`, et jeter un `Task` de `ReproduciblyAsync` — ne sont - donc pas exprimables dans le système de types. -* `FirstClassErrors` fournit déjà des analyseurs Roslyn (`FCE001`…`FCE022`) dans son propre package NuGet. - `JustDummies` n'en fournissait aucun, et c'est une bibliothèque **autonome, agnostique des erreurs** (un ADR le - garde : elle ne doit jamais dépendre de FirstClassErrors). Une règle propre à JustDummies ne peut pas vivre dans - `FirstClassErrors.Analyzers` — cet assembly est livré dans le package FirstClassErrors et porte l'identité FCE — un - consommateur de JustDummies ne la recevrait jamais. -* D'autres gardes ont été envisagées et rejetées : une surcharge « poison » `[Obsolete(error: true)]` (un membre - déprécié dans une 1.0 toute neuve est un contresens qui salit la surface livrée), et une surcharge asynchrone qui - bloque avec `GetAwaiter().GetResult()` (le sync-over-async risque le *deadlock* sous un `SynchronizationContext` - capturé — l'anti-pattern que l'async existe pour éviter). - -## Décision - -`JustDummies` fournit ses propres analyseurs Roslyn de première partie, dans un nouveau projet `JustDummies.Analyzers` -empaqueté dans le package NuGet `JustDummies` (`analyzers/dotnet/cs`), agnostique des erreurs et indépendant de -`FirstClassErrors`, sous un schéma d'identifiants de diagnostic propre à JustDummies (`JDxxx`, en miroir de `FCExxx`). - -La première application rend la surface asynchrone reproductible non-abusable : le point d'entrée asynchrone est -`Any.ReproduciblyAsync(Func)` (nommé TAP, retourne un `Task` que l'on `await`), le synchrone reste -`Any.Reproducibly(Action)`, et deux analyseurs de sévérité *error* ferment ce que les types ne peuvent pas — **JD001**, -une lambda `async` passée à `Any.Reproducibly`, et **JD002**, un `Task` de `Any.ReproduciblyAsync` jeté. - -## Justification - -* Le défaut est invisible là où ça compte le plus — une compilation qui passe sur un test qui échoue — donc une erreur - de compilation est la seule contrainte assez forte. Un avertissement, ou de la documentation, laisse le vert vert. -* Le choix de la contrainte suit ce que chaque mécanisme peut porter (le même grain qu'ADR-0035). Le système de types - *ne peut pas* exprimer « ce `Task` doit être attendu » ni « cette lambda async ne doit pas se lier ici », donc un - analyseur est l'outil légitime — pas un pis-aller, le seul mécanisme disponible. Là où le langage *peut* porter la - règle, on le préfère ; ici il ne peut pas. -* Un analyseur de première partie n'est pas exotique pour ce dépôt — il en livre et en teste déjà, avec un contrat de - chargement Roslyn épinglé au plancher et des règles à suivi de version. Étendre cette discipline à JustDummies - réutilise un patron éprouvé plutôt que d'en inventer un, et garde les règles JustDummies dans le package JustDummies, - là où est leur public. -* Les alternatives rejetées échangent chacune le vert-silencieux contre un échec pire ou plus laid : la surcharge - poison livre un membre déprécié dès le premier jour ; la surcharge bloquante échange un vert-silencieux contre un - *deadlock* possible. L'analyseur laisse la surface publique propre (deux méthodes honnêtes) et l'échec bruyant (une - erreur de compilation). -* Séparer `Reproducibly`/`ReproduciblyAsync` par le nom — plutôt que de garder un seul nom surchargé — est ce qui rend - JD001 et JD002 précis : chaque règle vise une seule méthode, donc aucune ne se déclenche à tort sur l'usage correct - de l'autre. - -## Alternatives considérées - -### Garder `Reproducibly(Func)` surchargé et n'ajouter qu'un analyseur « ne pas jeter » - -Envisagé car c'est le plus petit changement, une seule règle. Rejeté car il laisse une méthode qui retourne un `Task` -sans le suffixe `Async` (violation TAP et smell de nommage durable, figé à 1.0), et car la surcharge qui retourne un -`Task` jetable est précisément la forme qu'exploite le piège — le choix de nommage et le choix de sûreté se font mieux -ensemble. - -### Surcharge poison — `[Obsolete("Use ReproduciblyAsync", error: true)] Reproducibly(Func)` - -Envisagé car il ferme le piège `async void` purement au niveau langage, sans analyseur. Rejeté car `[Obsolete]` -signifie « déprécié depuis une version antérieure », dont une 1.0 neuve n'a aucune ; il livre un membre à jamais -non-appelable dans la toute première surface publique, ce qui se lit comme une erreur plutôt qu'un design. - -### Surcharge asynchrone bloquante — `void Reproducibly(Func)` qui exécute le corps via `GetAwaiter().GetResult()` - -Envisagé car il n'expose aucun `Task` à jeter et ne nécessite aucun analyseur. Rejeté car il impose du -sync-over-async à tout corps de test asynchrone, ce qui peut *deadlocker* sous un `SynchronizationContext` capturé ; -échanger un vert-silencieux contre un gel intermittent n'est pas un progrès pour un outil de test. - -### Mettre la règle JustDummies dans `FirstClassErrors.Analyzers` - -Envisagé car le projet d'analyseur existe déjà. Rejeté car cet assembly est livré dans le package FirstClassErrors et -porte l'identité FCE : un consommateur de JustDummies seul ne recevrait jamais la règle, et faire transiter une règle -JustDummies par la bibliothèque d'erreurs casse la frontière d'autonomie que garde le test d'architecture. - -## Conséquences - -### Positives - -* Le piège du vert-silencieux devient une erreur de compilation : `Any.Reproducibly(async …)` (JD001) et un - `Any.ReproduciblyAsync(…)` jeté (JD002) font tous deux échouer la build, avec un message pointant vers la correction. -* Le point d'entrée asynchrone est nommé TAP (`ReproduciblyAsync`), donc il se lit correctement et `CS4014` couvre - gratuitement le cas `await`-dans-une-méthode-async. -* JustDummies acquiert une histoire d'analyseurs de première partie extensible à de futures règles, dans son propre - package, sans couplage à FirstClassErrors. - -### Négatives - -* Le renommage est un changement cassant de la surface publique (pré-version, non livrée) : `Reproducibly(Func)` - devient `ReproduciblyAsync`. Acceptable seulement dans la fenêtre pré-1.0, sans coût de migration puisqu'il n'y a - aucun consommateur. -* Un deuxième projet d'analyseur, une cible d'empaquetage et un schéma d'ID de diagnostic alourdissent le dépôt et la - build du package JustDummies. - -### Risques - -* Le contrat de chargement de `JustDummies.Analyzers` doit rester épinglé au plancher Roslyn, comme - `FirstClassErrors.Analyzers`, sinon l'analyseur échoue silencieusement à se charger (CS8032) sur des SDK plus - anciens ; atténué en épinglant `Microsoft.CodeAnalysis.CSharp` à `$(RoslynFloorVersion)`. -* JD001/JD002 détectent l'invocation par le nom de métadonnées `JustDummies.Any` et le nom de méthode ; un futur - renommage de ces membres désactiverait silencieusement les règles, donc leurs noms font désormais partie du contrat - de diagnostic. - -## Actions de suivi - -* Aucune requise pour la surface reproductible. Appliquer le même patron d'analyseur de première partie quand une - future erreur JustDummies n'est exprimable qu'à la compilation. -* La provenance des messages de conflit d'`AnyEnum` / `AnyGuid` (issue #314) est sans rapport et non affectée. - -## Références - -* ADR-0035 — imposer les conflits Any structurels à la compilation, ceux dépendant de la valeur à l'exécution ; le - grain « les types là où ils peuvent porter la règle, des vérifications là où ils ne peuvent pas » que suit cette - décision. -* ADR-0031 — nommer les fabriques d'Any d'après leur type CLR ; précédent pour « rendre la règle in-cassable plutôt - que seulement vérifiée », et pour la discipline de nommage TAP sur la surface. -* ADR-0042 — sérialiser les tirages sur une source aléatoire ; le correctif de reproductibilité frère (#310 / #311). -* Issue #317 — le piège du vert-silencieux que cet ADR résout. diff --git a/doc/handwritten/for-maintainers/adr/0044-ship-justdummies-analyzers.md b/doc/handwritten/for-maintainers/adr/0044-ship-justdummies-analyzers.md deleted file mode 100644 index 2a372ed1..00000000 --- a/doc/handwritten/for-maintainers/adr/0044-ship-justdummies-analyzers.md +++ /dev/null @@ -1,130 +0,0 @@ -# ADR-0044 | Ship first-party JustDummies analyzers, and guard the reproducible async surface with them - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0044-ship-justdummies-analyzers.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-27 -**Accepted:** 2026-07-27 -**Decision Makers:** Reefact - -## Context - -* `JustDummies` is a test-support library: its whole value is that a broken arrangement *fails*. `Any.Reproducibly` - runs a test body under a pinned seed and reports it on failure. It overloaded a synchronous `Action` and an - asynchronous `Func` on one name. -* That overload set had a silent-failure footgun. An `async` lambda is a better conversion to `Func` than to - `Action`, so `Any.Reproducibly(async () => { ... })` bound to the async overload, which returned a `Task`. A test - method is usually a synchronous `void`, so the returned task was discarded; the body's assertions ran on a - continuation after the method had already returned, and the failure surfaced — if at all — as a later - `UnobservedTaskException`. **The test passed green.** The compiler's own `CS4014` does not fire in a synchronous - method, so nothing warned. -* Renaming the async overload to a TAP-conventional `ReproduciblyAsync` fixes the naming, but on its own it *reopens* - the hazard from the other side: with only `Action` overloads left on `Reproducibly`, an `async` lambda binds to - `Action` as **`async void`**, whose post-await exception escapes the reproducible scope's `try/catch` entirely. -* C# offers no non-droppable `Task`, and no way to forbid an `async`-lambda→`Action` conversion. The two residual - mistakes — passing an async body to `Reproducibly`, and discarding a `ReproduciblyAsync` task — are therefore not - expressible in the type system. -* `FirstClassErrors` already ships Roslyn analyzers (`FCE001`…`FCE022`) inside its own NuGet package. `JustDummies` - shipped none, and is a **standalone, error-agnostic** library (ADR guards it: it must never depend on - FirstClassErrors). A JustDummies-specific rule cannot live in `FirstClassErrors.Analyzers` — that assembly ships in - the FirstClassErrors package and carries the FCE identity — so a JustDummies consumer would never receive it. -* Alternative guards were considered and rejected in the design discussion: an `[Obsolete(error: true)]` "poison" - overload (a member deprecated in a brand-new 1.0 is a contradiction that clutters the shipped surface), and an - async overload that blocks with `GetAwaiter().GetResult()` (sync-over-async risks deadlock under a captured - `SynchronizationContext` — the anti-pattern async exists to avoid). - -## Decision - -`JustDummies` ships its own first-party Roslyn analyzers, in a new `JustDummies.Analyzers` project packaged inside the -`JustDummies` NuGet package (`analyzers/dotnet/cs`), error-agnostic and independent of `FirstClassErrors`, under a -JustDummies-owned diagnostic-id scheme (`JDxxx`, mirroring `FCExxx`). - -The first application makes the reproducible async surface un-misusable: the asynchronous entry point is -`Any.ReproduciblyAsync(Func)` (TAP-named, returns an awaited `Task`), the synchronous one stays -`Any.Reproducibly(Action)`, and two error-severity analyzers close the mistakes the types cannot — **JD001**, an -`async` lambda passed to `Any.Reproducibly`, and **JD002**, a discarded `Any.ReproduciblyAsync` task. - -## Rationale - -* The defect is invisible where it matters most — a passing build over a failing test — so a build-time error is the - only enforcement strong enough. A warning, or documentation, leaves the green build green. -* The choice of enforcement follows what each mechanism can carry (the same grain as ADR-0035). The type system - *cannot* express "this `Task` must be awaited" or "this async lambda must not bind here", so a run-time or - compile-time analyzer is the legitimate tool — not a fallback, the only mechanism available. Where the language - *can* carry the rule, it is preferred; here it cannot. -* A first-party analyzer is not exotic for this repository — it already ships and tests analyzers, with a floor-pinned - Roslyn load contract and release-tracked rules. Extending that discipline to JustDummies reuses a proven pattern - rather than inventing one, and keeps the JustDummies rules in the JustDummies package where their audience is. -* The rejected alternatives each trade the silent-green for a worse or uglier failure: the poison overload ships a - deprecated member on day one; the blocking overload trades a silent green for a possible deadlock. The analyzer - leaves the public surface clean (two honest methods) and the failure mode loud (a compile error). -* Splitting `Reproducibly`/`ReproduciblyAsync` by name — rather than keeping one overloaded name — is what lets JD001 - and JD002 be precise: each rule targets one method, so neither over-reports on the other's correct use. - -## Alternatives Considered - -### Keep the overloaded `Reproducibly(Func)` and add only a "don't discard" analyzer - -Considered because it is the smallest change and needs a single rule. Rejected because it leaves a `Task`-returning -method without the `Async` suffix (a TAP violation and a lasting naming smell frozen at 1.0), and because the overload -that returns a droppable `Task` is exactly the shape the footgun exploits — the naming choice and the safety choice -are better made together. - -### Poison overload — `[Obsolete("Use ReproduciblyAsync", error: true)] Reproducibly(Func)` - -Considered because it closes the `async void` hazard purely in the language, with no analyzer. Rejected because -`[Obsolete]` means "deprecated since an earlier version", of which a fresh 1.0 has none; it ships a permanently -un-callable member in the very first public surface, which reads as an error rather than a design. - -### Blocking async overload — `void Reproducibly(Func)` that runs the body with `GetAwaiter().GetResult()` - -Considered because it exposes no `Task` to drop and needs no analyzer. Rejected because it forces sync-over-async on -every asynchronous test body, which can deadlock under a captured `SynchronizationContext`; trading a silent green for -an intermittent hang is not an improvement for a test tool. - -### Put the JustDummies rule in `FirstClassErrors.Analyzers` - -Considered because the analyzer project already exists. Rejected because that assembly ships inside the -FirstClassErrors package and carries the FCE identity: a JustDummies-only consumer would never receive the rule, and -routing a JustDummies rule through the error library breaks the standalone boundary the architecture test guards. - -## Consequences - -### Positive - -* The silent-green footgun becomes a compile error: `Any.Reproducibly(async …)` (JD001) and a discarded - `Any.ReproduciblyAsync(…)` (JD002) both fail the build, with a message pointing at the fix. -* The async entry point is TAP-named (`ReproduciblyAsync`), so it reads correctly and `CS4014` covers the - await-in-async-method case for free. -* JustDummies gains a first-party analyzer story it can extend to future rules, in its own package, without coupling - to FirstClassErrors. - -### Negative - -* The rename is a breaking change to the (pre-release, unshipped) public surface: `Reproducibly(Func)` becomes - `ReproduciblyAsync`. Acceptable only in the pre-1.0 window, at no migration cost since there are no consumers. -* A second analyzer project, package-embedding target, and diagnostic-id scheme enlarge the repository and the - JustDummies package's build. - -### Risks - -* The `JustDummies.Analyzers` load contract must stay pinned to the Roslyn floor, like `FirstClassErrors.Analyzers`, - or the analyzer silently fails to load (CS8032) on older SDKs; mitigated by pinning `Microsoft.CodeAnalysis.CSharp` - to `$(RoslynFloorVersion)`. -* JD001/JD002 detect the invocation by the `JustDummies.Any` metadata name and the method name; a future rename of - those members would silently disable the rules, so their names are now part of the diagnostic contract. - -## Follow-up Actions - -* None required for the reproducible surface. Apply the same first-party-analyzer pattern when a future JustDummies - mistake is expressible only at compile time. -* `AnyEnum` / `AnyGuid` conflict-message provenance (issue #314) is unrelated and unaffected. - -## References - -* ADR-0035 — enforce structural Any conflicts at compile time, value-dependent ones at run time; the "types where - they can carry the rule, checks where they cannot" grain this decision follows. -* ADR-0031 — name Any's factories after their CLR type; precedent for "make the rule un-break-able rather than merely - checked", and for TAP-style naming discipline on the surface. -* ADR-0042 — serialize draws on a random source; the sibling reproducibility fix (#310 / #311). -* Issue #317 — the silent-green footgun this ADR resolves. diff --git a/doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.fr.md b/doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.fr.md deleted file mode 100644 index 77992014..00000000 --- a/doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.fr.md +++ /dev/null @@ -1,136 +0,0 @@ -# ADR-0045 | Garder contre le null les arguments publics et internes, imposé par une convention par réflexion - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0045-guard-public-and-internal-arguments-against-null.md) - -**Statut :** Remplacé par [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.fr.md) -**Proposé :** 2026-07-27 -**Accepté :** 2026-07-27 -**Décideurs :** Reefact - -## Contexte - -* `JustDummies` fait des invariants la raison d'être de la bibliothèque : un arrangement erroné doit *échouer*, au - plus près de sa cause. Ses objets-valeurs et ses résultats sont des classes validantes (la règle « class, jamais - struct » du dépôt), dont toute la garantie est qu'aucune instance n'existe sans avoir franchi un point d'entrée - validant. -* Les types référence nullables ne sont qu'une annotation **à la compilation**. Un appelant dont l'analyse nullable - est désactivée, un `null!`, la réflexion ou un `default` peuvent toujours faire passer un `null` par un paramètre - typé non-nullable à l'exécution. La bibliothèque raisonne déjà à partir de ce fait — c'est la raison affichée pour - laquelle les objets-valeurs sont des classes, pas des structs. -* Avant ce changement, beaucoup de membres ne validaient pas leurs arguments référence. Le manque était le plus large - à la **frontière interne** : les fabriques `Create(RandomSource)` et les constructeurs internes, là où les - dépendances d'une classe (la source aléatoire, les specs d'intervalle/de chaîne/d'URI) entrent en elle pour la - première fois. L'API publique ne peut jamais y router un `null` ; un `null` qui les atteindrait ne pourrait venir que - d'une erreur de câblage interne — et surgirait plus tard en `NullReferenceException`, loin de sa cause. -* La suite de contrats était strictement **boîte noire** : aucun `InternalsVisibleTo` n'existait, si bien que chaque - test n'exerçait que la surface publique. L'audit d'architecture du 2026-07-20 (§9.3) l'a consigné comme un choix - délibéré — il prouve que l'API publique suffit à spécifier la bibliothèque, et rend les refactors du moteur - transparents aux tests. -* Construire une exception se produit sur le chemin de gestion d'erreur et de journalisation. `System.Exception` - tolère un message et une exception interne `null`. -* La bibliothèque a pour plancher **.NET Standard 2.0** (donc `ArgumentNullException.ThrowIfNull`, une API .NET 6+, - est indisponible), et les suites de contrats tournent en plus sur le plancher de support .NET Framework 4.7.2. Les - métadonnées de nullabilité par réflexion (`NullabilityInfoContext`) sont une API .NET 6+. - -## Décision - -Tout membre `public` ou `internal` de `JustDummies` — constructeur ou méthode — rejette un argument de type référence -non-nullable `null` par une `ArgumentNullException` nommant le paramètre, à l'exception des constructeurs de types -d'exception ; un test-convention piloté par réflexion l'impose sur toute la surface, et les internes de la bibliothèque -sont ouverts à la suite de contrats pour qu'il le puisse. - -## Rationale - -* **La classe, pas l'assembly, est la frontière de confiance.** Un membre ne peut pas supposer ses appelants - corrects, et « appelant » inclut une autre classe du même assembly. Valider ce qui franchit la frontière — et là - seulement, en faisant confiance à ce qu'un membre validant a déjà accepté — est ce qui empêche un `null` de voyager - loin de l'erreur qui l'a produit, sans re-vérification redondante à l'intérieur de la classe. -* **Les annotations nullables ne sont pas une application.** Comme elles disparaissent à l'exécution, le seul mécanisme - qui rejette réellement un `null` est une garde à l'exécution. C'est le raisonnement que le dépôt accepte déjà pour - faire des objets-valeurs des classes plutôt que des structs ; l'appliquer à la validation d'arguments est cohérent, - pas nouveau. -* **La frontière interne est la garde la plus utile et la plus difficile à tester.** C'est là que les dépendances - entrent dans une classe, donc là qu'une erreur de câblage interne est attrapée — pourtant l'API publique ne peut - jamais y conduire un `null`, si bien qu'une convention limitée au public laisserait justement cette garde non - vérifiée. La vérifier est ce qui rend l'ouverture des internes rentable. -* **Seule la réflexion rend la convention auto-entretenue.** La convention doit valoir pour chaque membre existant et - chaque membre ajouté ensuite ; un test qui découvre les membres par réflexion y soumet automatiquement un nouveau - générateur, une nouvelle fabrique ou une nouvelle méthode fluide, sans rien à ajouter. Des tests écrits à la main, - un par paramètre, oublient précisément le nouveau membre que la convention existe pour attraper. -* **Relâcher la boîte noire est le prix de la vérification de la frontière interne, et il est borné.** Puisqu'un - `null` ne peut pas atteindre les membres internes via l'API publique, vérifier leurs gardes exige un accès interne. - Les suites comportementales gardent leur posture boîte noire et ses bénéfices ; l'unique test qui a besoin des - internes ne nomme aucun membre — il est générique par réflexion — donc il reste transparent aux refactors. Ce à quoi - on renonce, c'est seulement la propriété que *tous* les tests ne touchent que la surface publique. -* **Les exceptions sont exemptées car une garde y irait contre son propre but.** Leurs constructeurs s'exécutent - pendant qu'une erreur est gérée ou journalisée ; lever une `ArgumentNullException` sur un message `null` masquerait - l'échec d'origine, et le type de base le tolère déjà. - -## Alternatives considérées - -### Garder la posture boîte noire : imposer la convention sur la seule surface publique - -Considérée parce qu'elle préserve la posture délibérée que l'audit consigne, sans ouvrir aucun interne. Rejetée parce -qu'elle laisse non vérifiées les gardes de la frontière interne — les fabriques `Create` et les constructeurs internes -où l'API publique ne peut jamais router un `null` —, c'est-à-dire la couverture dont la convention a le plus besoin ; et -elle ne correspond pas à la portée de la décision elle-même, qui est *public ou internal*. - -### Exercer les internes par réflexion sans `InternalsVisibleTo` - -Considérée parce qu'elle garde vrai le fait littéral « aucun `InternalsVisibleTo` ». Rejetée parce qu'elle exerce -malgré tout les internes — donc relâche exactement la même posture — tout en forçant le test à atteindre, par la seule -réflexion, des types qu'il n'a pas le droit de nommer ; si l'on relâche la posture, le faire explicitement est plus -clair et pas davantage un écart. - -### Tests écrits à la main, un par paramètre - -Considérée comme l'option la plus compatible avec la boîte noire. Rejetée parce qu'elle ne s'auto-entretient pas : -chaque nouveau membre exige un nouveau test, et une garde oubliée sur un nouveau membre — le défaut même que la -convention existe pour empêcher — est justement ce qu'une suite écrite à la main oublie aussi. - -### S'appuyer sur les annotations de référence nullable, ou un analyseur, plutôt que sur des gardes à l'exécution - -Considérée parce que les annotations documentent l'intention à la compilation et qu'un analyseur pourrait signaler les -gardes manquantes. Rejetée parce que ni l'un ni l'autre ne rejette un `null` à l'exécution, qui est la garantie -recherchée ; les consommateurs en aval peuvent compiler avec l'analyse nullable désactivée, et la propre règle -« class, jamais struct » de la bibliothèque repose déjà sur le fait que l'application à l'exécution est la seule -réelle. - -## Conséquences - -### Positives - -* Un argument `null` échoue vite à la frontière, en `ArgumentNullException` nommant le paramètre, au lieu de surgir - plus tard en `NullReferenceException` loin de la cause. -* La convention s'auto-impose : un nouveau membre `public`/`internal` y est soumis automatiquement, sans test à écrire. -* La frontière interne — jusqu'ici hors d'atteinte de tout test — est désormais vérifiée. - -### Négatives - -* La posture de test boîte noire délibérée est relâchée : les internes de la bibliothèque sont visibles pour la suite - de contrats. -* Un petit volume permanent de code de garde est réparti sur la surface publique et interne. -* Le test-convention utilise des métadonnées de nullabilité par réflexion .NET 6+, donc il ne tourne que sur la patte - moderne et est exclu du build du plancher net472 ; les gardes qu'il impose sont, elles, en netstandard2.0. - -### Risques - -* Le test-convention ne peut vérifier qu'un membre pour lequel il sait construire des arguments valides. Atténuation : - un membre qu'il ne peut pas exercer est signalé comme *non couvert* et fait échouer le test (échec bruyant), jamais - ignoré en silence — un trou de couverture apparaît en test rouge, à combler par un échantillon ou un test explicite. -* Des internes ouverts pourraient tenter de futurs tests vers un couplage boîte blanche. Atténuation : le - test-convention ne nomme aucun membre, et les suites comportementales restent boîte noire. - -## Actions de suivi - -* Aucune nécessaire pour que la convention tienne : les membres futurs sont couverts automatiquement. Garder le - test-convention au vert. -* L'observation §9.3 de l'audit du 2026-07-20 selon laquelle « aucun `InternalsVisibleTo` n'existe » cesse - délibérément d'être vraie à partir de cette décision. - -## Références - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) — JustDummies est un paquet autonome et agnostique aux erreurs. -* [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.md) — le paquet Testing rebase ses valeurs arbitraires sur JustDummies. -* Audit d'architecture et de conception JustDummies du 2026-07-20, §9.3 (stratégie de test — la posture boîte noire). -* `CLAUDE.md` — la règle « class, jamais struct » des objets-valeurs (application des invariants à l'exécution). diff --git a/doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.md b/doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.md deleted file mode 100644 index a7539e38..00000000 --- a/doc/handwritten/for-maintainers/adr/0045-guard-public-and-internal-arguments-against-null.md +++ /dev/null @@ -1,128 +0,0 @@ -# ADR-0045 | Guard public and internal arguments against null, enforced by a reflection convention - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0045-guard-public-and-internal-arguments-against-null.fr.md) - -**Status:** Superseded by [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md) -**Proposed:** 2026-07-27 -**Accepted:** 2026-07-27 -**Decision Makers:** Reefact - -## Context - -* `JustDummies` treats invariants as the point of the library: a broken arrangement must *fail*, close to its cause. - Its value objects and results are validating classes (the repository's class-not-struct rule), whose whole guarantee - is that no instance exists without having passed a validating entry point. -* Nullable reference types are a **compile-time** annotation only. A caller with nullable analysis disabled, a - `null!`, reflection, or `default` can still route a `null` through a parameter typed as non-nullable at run time. The - library already reasons from this fact — it is the stated reason value objects are classes, not structs. -* Before this change, many members did not validate their reference arguments. The gap was widest at the **internal - boundary**: the `Create(RandomSource)` factories and internal constructors, where a class's dependencies (the random - source, the interval/string/URI specs) first enter it. The public API can never route a `null` into those members, - so a `null` reaching them could only come from an internal wiring mistake — and would surface later as a - `NullReferenceException` far from its cause. -* The contract suite was strictly **black-box**: no `InternalsVisibleTo` existed, so every test exercised the public - surface only. The 2026-07-20 architecture audit (§9.3) recorded this as a deliberate choice — it proves the public - API is sufficient to specify the library, and makes engine refactors test-transparent. -* Constructing an exception happens on the error-handling and logging path. `System.Exception` tolerates a `null` - message and inner exception. -* The library floors on **.NET Standard 2.0** (so `ArgumentNullException.ThrowIfNull`, a .NET 6+ API, is unavailable), - and the contract suites additionally run on the .NET Framework 4.7.2 support floor. Reflection nullability metadata - (`NullabilityInfoContext`) is a .NET 6+ API. - -## Decision - -Every `public` or `internal` member of `JustDummies` — constructor or method — rejects a `null` non-nullable -reference-type argument with an `ArgumentNullException` naming the parameter, exception-type constructors excepted; a -reflection-driven convention test enforces this across the whole surface, and the library's internals are opened to the -contract suite so it can. - -## Rationale - -* **The class, not the assembly, is the trust boundary.** A member cannot assume its callers are correct, and "caller" - includes another class of the same assembly. Validating what crosses the boundary — and only there, trusting what a - validating member has already accepted — is what keeps a `null` from travelling far from the mistake that produced - it, without redundant re-checking inside the class. -* **Nullable annotations are not enforcement.** Because they vanish at run time, the only mechanism that actually - rejects a `null` is a runtime guard. This is the same reasoning the repository already accepts for making value - objects classes rather than structs; applying it to argument validation is consistent, not new. -* **The internal boundary is the guard most worth having and the hardest to test.** It is where dependencies first - enter a class, so it is where an internal wiring bug is caught — yet the public API can never drive a `null` there, so - a public-only convention would leave exactly that guard unverified. Verifying it is what makes opening the internals - worth its cost. -* **Only reflection makes the convention self-maintaining.** The convention must hold for every member that exists and - every one added later; a test that discovers members by reflection holds a new generator, factory, or fluent method - to it automatically, with nothing to add. Hand-written per-parameter tests forget exactly the new member the - convention exists to catch. -* **Relaxing black-box is the price of verifying the internal boundary, and it is bounded.** Since a `null` cannot - reach the internal members through the public API, verifying their guards requires internal access. The behavioural - suites keep their black-box posture and its benefits; the single test that needs internals names no member — it is - reflection-generic — so it stays refactor-transparent. What is given up is only the property that *all* tests touch - the public surface alone. -* **Exceptions are exempt because a guard there would defeat its own purpose.** Their constructors run while an error - is being handled or logged; throwing an `ArgumentNullException` over a `null` message would mask the original - failure, and the base type already tolerates it. - -## Alternatives Considered - -### Keep the black-box posture: enforce the convention over the public surface only - -Considered because it preserves the deliberate posture the audit records without opening any internals. Rejected -because it leaves the internal boundary's guards — the `Create` factories and internal constructors the public API can -never route a `null` through — unverified, which is the coverage the convention most needs; and it does not match the -decision's own scope, which is *public or internal*. - -### Exercise internals by reflection without `InternalsVisibleTo` - -Considered because it keeps the literal "no `InternalsVisibleTo`" fact true. Rejected because it still exercises -internals — so it relaxes the very same posture — while forcing the test to reach, through reflection alone, types it -is not allowed to name; if the posture is being relaxed, doing it explicitly is clearer and no more of a departure. - -### Hand-written per-parameter tests - -Considered as the most black-box-friendly option. Rejected because it does not self-maintain: every new member needs a -new test, and a forgotten guard on a new member — the exact defect the convention exists to prevent — is exactly what a -hand-written suite also forgets. - -### Rely on nullable reference annotations, or an analyzer, instead of runtime guards - -Considered because annotations document intent at compile time and an analyzer could flag missing guards. Rejected -because neither rejects a `null` at run time, which is the guarantee sought; downstream consumers may compile with -nullable analysis disabled, and the library's own class-not-struct rule already rests on run-time enforcement being the -only real one. - -## Consequences - -### Positive - -* A `null` argument fails fast at the boundary, as an `ArgumentNullException` naming the parameter, instead of later as - a `NullReferenceException` far from the cause. -* The convention is self-enforcing: a new `public`/`internal` member is held to it automatically, with no test to write. -* The internal boundary — previously unreachable by any test — is now verified. - -### Negative - -* The deliberate black-box test posture is relaxed: the library's internals are visible to the contract suite. -* A small, permanent volume of guard code is spread across the public and internal surface. -* The convention test uses .NET 6+ reflection nullability metadata, so it runs on the modern leg only and is excluded - from the net472 support-floor build; the guards it enforces are themselves netstandard2.0. - -### Risks - -* The convention test can only verify a member it can construct valid arguments for. Mitigation: a member it cannot - exercise is reported as *uncovered* and fails the test (fail-loud), never silently skipped — a coverage gap shows up - as a red test, to be closed by a sample or an explicit test. -* Open internals could tempt future tests into white-box coupling. Mitigation: the convention test names no member, and - the behavioural suites stay black-box. - -## Follow-up Actions - -* None required for the convention to hold: future members are covered automatically. Keep the convention test green. -* The 2026-07-20 audit's §9.3 observation that "no `InternalsVisibleTo` exists" is, from this decision on, deliberately - no longer true. - -## References - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) — JustDummies is a standalone, error-agnostic package. -* [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.md) — the Testing package rebases its arbitrary values on JustDummies. -* 2026-07-20 JustDummies architecture and design audit, §9.3 (testing strategy — the black-box posture). -* `CLAUDE.md` — the value-object class-not-struct rule (run-time enforcement of invariants). diff --git a/doc/handwritten/for-maintainers/adr/0047-measure-justdummies-mutation-against-the-unit-suite-only.fr.md b/doc/handwritten/for-maintainers/adr/0047-measure-justdummies-mutation-against-the-unit-suite-only.fr.md deleted file mode 100644 index d20a388c..00000000 --- a/doc/handwritten/for-maintainers/adr/0047-measure-justdummies-mutation-against-the-unit-suite-only.fr.md +++ /dev/null @@ -1,96 +0,0 @@ -# ADR-0047 | Mesurer la mutation de JustDummies contre la seule suite unitaire déterministe - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0047-measure-justdummies-mutation-against-the-unit-suite-only.md) - -**Statut :** Accepté -**Proposé :** 2026-07-27 -**Accepté :** 2026-07-31 -**Décideurs :** Reefact - -## Contexte - -L'ADR-0043 a configuré le run de mutation de JustDummies pour utiliser **les deux** suites de tests -comme oracle censé tuer chaque mutant : `JustDummies.UnitTests` **et** `JustDummies.PropertyTests` -(`build/stryker/justdummies.json`, `test-projects`). - -La suite property est basée sur FsCheck. Chaque propriété tire ~100 cas aléatoires par run, depuis une -graine aléatoire. Cela en fait deux choses à la fois : - -* **La moitié coûteuse du coût par mutant.** Chaque mutant rejoue tout l'oracle - (`"coverage-analysis": "off"`, ADR-0043), et cent cas par propriété dominent ce temps — ce qui, sur un - gros fichier changé comme `Any.cs`, fait la différence entre minutes et dizaines de minutes. -* **Un oracle non-déterministe.** Un verdict de mutation répond à « un test de l'oracle échoue-t-il sur ce - mutant ? ». Avec un oracle randomisé, cette réponse dépend de la graine FsCheck : un mutant peut être - **tué sur les tirages d'un run et survivant sur ceux d'un autre**. Le score de mutation reflète alors la - graine autant que le code et les assertions — l'inverse du chiffre reproductible et vrai que - `"coverage-analysis": "off"` existe pour protéger (ADR-0043). - -Le non-déterminisme n'est pas hypothétique. Le 2026-07-27 (issue #335), la suite property elle-même a -flanché en CI : un bug regex `IgnoreCase` latent n'a surgi qu'après ~89 cas FsCheck et a fait échouer le -leg `Build & test` d'une pull request **sans rapport**. Le hasard qui rend un vrai test intermittent rend -intermittent un verdict de mutation bâti dessus. - -## Décision - -L'oracle de mutation de JustDummies est **la seule suite unitaire déterministe** : `test-projects` dans -`build/stryker/justdummies.json` ne liste plus que `JustDummies.UnitTests`. La suite property FsCheck est -retirée de l'oracle. Elle continue de tourner dans `Build & test` comme vraie assurance — elle ne juge -simplement plus les mutants. - -## Justification - -* **Un score reproductible.** La mutation mesure désormais si les tests **d'exemple** (unitaires) épinglent - le comportement — une propriété du code et de ces tests seuls. Le même commit donne le même score, run - après run, ce qui est tout l'intérêt de le mesurer. -* **Plus rapide, là où ça fait mal.** Les cent cas par propriété de la suite property sont le goulot par - mutant ; les retirer raccourcit chaque run de mutation — le leg par-PR comme le balayage hebdomadaire. -* **Les property tests protègent toujours la bibliothèque.** Ils tournent dans `Build & test` et attrapent - les régressions ; ils sont seulement retirés du *juge* de mutation. La mutation demande « tes assertions - épinglent-elles ce comportement ? », et un test d'exemple est l'oracle naturel et déterministe de cette - question. Une propriété qui re-randomise à chaque run ne l'est pas — elle répond à une autre question - (l'invariant tient-il sur de nombreuses entrées ?), que la suite pose toujours là où c'est sa place - (ADR-0040). - -## Alternatives considérées - -### Garder la suite property dans l'oracle - -Rejeté : elle rend le score de mutation non-reproductible (dépendant de la graine) et est la moitié la plus -lente du run. Les deux sont exactement les coûts que cette décision supprime. - -### Semer la suite property à une graine fixe pour le run de mutation - -Considéré parce que cela rendrait l'oracle déterministe sans le retirer. Rejeté : c'est toujours la moitié -lente (cent cas par propriété), et cela épingle le score à une graine arbitraire au lieu de supprimer la -dépendance à une graine — un levier caché qui déplace tous les scores dès qu'on y touche. Semer ou non la -suite property dans **`Build & test`** pour tuer le landmine du rouge intermittent (#335) est une question -séparée, tranchée pour elle-même. - -## Conséquences - -### Positives - -* Le score de mutation de JustDummies est reproductible : il dépend du code et des tests unitaires, pas - d'une graine aléatoire. -* Chaque run de mutation de JustDummies est plus rapide — le leg par-PR comme le balayage complet - hebdomadaire. - -### Négatives - -* Un comportement épinglé **uniquement** par un property test, sans aucun test unitaire l'affirmant, - apparaît désormais comme un **survivant** de mutation. C'est un vrai signal, pas un faux : il dit - « aucun test d'exemple n'épingle ceci ». Là où la couverture compte vraiment, le correctif est - d'ajouter un test unitaire — l'ADR-0040 régit déjà quelle suite possède quel cas. -* La base du score se déplace. Avec `break: 0`, cela ne fait rien échouer ; le prochain balayage - hebdomadaire publie le nouveau chiffre. - -## Références - -* ADR-0043 — Contrôler les pull requests sur le score de mutation du diff : le run dont ceci restreint - l'oracle. -* ADR-0040 — Séparer le banc de tests de JustDummies entre une suite d'exemples et une suite de - propriétés : pourquoi les deux suites répondent à des questions différentes, d'où l'une est l'oracle de - mutation et l'autre non. -* ADR-0046 — Rendre la porte de mutation par pull request consultative : la décision sœur de - vitesse/blocage. -* Issue #335 — le flake de la propriété `IgnoreCase` qui a rendu le non-déterminisme concret. diff --git a/doc/handwritten/for-maintainers/adr/0047-measure-justdummies-mutation-against-the-unit-suite-only.md b/doc/handwritten/for-maintainers/adr/0047-measure-justdummies-mutation-against-the-unit-suite-only.md deleted file mode 100644 index 8a9dcc82..00000000 --- a/doc/handwritten/for-maintainers/adr/0047-measure-justdummies-mutation-against-the-unit-suite-only.md +++ /dev/null @@ -1,91 +0,0 @@ -# ADR-0047 | Measure JustDummies mutation against the deterministic unit suite only - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0047-measure-justdummies-mutation-against-the-unit-suite-only.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-27 -**Accepted:** 2026-07-31 -**Decision Makers:** Reefact - -## Context - -ADR-0043 configured the JustDummies mutation run to use **both** test suites as the oracle that must -kill each mutant: `JustDummies.UnitTests` **and** `JustDummies.PropertyTests` -(`build/stryker/justdummies.json`, `test-projects`). - -The property suite is FsCheck-based. Each property draws ~100 random cases per run, from a random seed. -That makes it two things at once: - -* **The expensive half of the per-mutant cost.** Every mutant re-runs the whole oracle - (`"coverage-analysis": "off"`, ADR-0043), and a hundred cases per property dominates that time — - which, on a large changed file like `Any.cs`, is the difference between minutes and tens of minutes. -* **A non-deterministic oracle.** A mutation verdict answers "does *any* test in the oracle fail on this - mutant?" With a randomized oracle, that answer depends on the FsCheck seed: a mutant can be **killed on - one run's draws and survive on another's**. The mutation score then reflects the seed as much as the - code and the assertions — the opposite of the reproducible, true figure `"coverage-analysis": "off"` - exists to protect (ADR-0043). - -The non-determinism is not hypothetical. On 2026-07-27 (issue #335) the property suite itself flaked in -CI: a latent `IgnoreCase` regex bug surfaced only after ~89 FsCheck cases and failed the `Build & test` -leg of an **unrelated** pull request. The same randomness that flakes a real test flakes a mutation -verdict built on it. - -## Decision - -The JustDummies mutation oracle is the **deterministic unit suite only**: `test-projects` in -`build/stryker/justdummies.json` lists `JustDummies.UnitTests` alone. The FsCheck property suite is -removed from the oracle. It still runs in `Build & test` as a real assurance — it simply no longer judges -mutants. - -## Rationale - -* **A reproducible score.** Mutation now measures whether the **example** (unit) tests pin behaviour — a - property of the code and those tests only. The same commit yields the same score, run after run, which - is the whole point of measuring it. -* **Faster, on the paths that hurt.** The property suite's hundred-cases-per-property is the per-mutant - bottleneck; removing it shortens every mutation run — the per-PR diff leg and the weekly sweep alike. -* **Property tests still protect the library.** They run in `Build & test` and catch regressions; they - are only removed from the mutation *judge*. Mutation testing asks "do your assertions pin this - behaviour?", and an example test is the natural, deterministic oracle for that question. A property - that re-randomises every run is not — it answers a different question (does the invariant hold across - many inputs?), which the suite still asks where it belongs (ADR-0040). - -## Alternatives Considered - -### Keep the property suite in the oracle - -Rejected: it makes the mutation score non-reproducible (seed-dependent) and is the slowest half of the -run. Both are the exact costs this decision removes. - -### Seed the property suite to a fixed seed for the mutation run - -Considered because it would make the oracle deterministic without dropping it. Rejected: it is still the -slow half (a hundred cases per property), and it pins the score to one arbitrary seed rather than -removing the dependence on a seed at all — a hidden lever that moves every score when touched. Whether to -seed the property suite in **`Build & test`** to end the intermittent-red landmine (#335) is a separate -question, decided on its own terms. - -## Consequences - -### Positive - -* The JustDummies mutation score is reproducible: it depends on the code and the unit tests, not on a - random seed. -* Every JustDummies mutation run is faster — the per-PR diff leg and the weekly full sweep. - -### Negative - -* A behaviour pinned **only** by a property test, with no unit test asserting it, now shows as a mutation - **survivor**. That is a true signal, not a false one: it says "no example test pins this." Where the - coverage genuinely matters, the fix is to add a unit test — ADR-0040 already governs which suite owns - which case. -* The score baseline shifts. With `break: 0` this fails nothing; the next weekly sweep publishes the new - figure. - -## References - -* ADR-0043 — Gate pull requests on the mutation score of the diff: the run this narrows the oracle of. -* ADR-0040 — Split the JustDummies test bed between an example suite and a property suite: why the two - suites answer different questions, which is why one is the mutation oracle and the other is not. -* ADR-0046 — Make the per-pull-request mutation gate advisory: the sibling speed/blocking decision. -* Issue #335 — the `IgnoreCase` property flake that made the non-determinism concrete. diff --git a/doc/handwritten/for-maintainers/adr/0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.fr.md b/doc/handwritten/for-maintainers/adr/0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.fr.md deleted file mode 100644 index 5403af04..00000000 --- a/doc/handwritten/for-maintainers/adr/0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.fr.md +++ /dev/null @@ -1,100 +0,0 @@ -# ADR-0048 | Garantir qu'une valeur regex générée matche son pattern, par redraw borné - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.md) - -**Statut :** Accepté -**Proposé :** 2026-07-27 -**Accepté :** 2026-07-31 -**Décideurs :** Reefact - -## Contexte - -`Any.StringMatching(...)` parse un pattern en arbre une fois et, à chaque tirage, le parcourt pour -**construire** une valeur directement — jamais générer-puis-filtrer. La construction reflète le -sous-ensemble régulier de la sémantique du moteur .NET, de sorte qu'une valeur générée est un membre -authentique du pattern. - -Quelques coins de la gestion du **match à vide** du moteur ne peuvent pas être reflétés -structurellement, parce que la réponse de .NET à « la chaîne vide matche-t-elle ? » pour une -**alternative nullable sous un quantificateur** est implémentation-définie et dépend de détails qu'une -construction structurelle ne porte pas : l'**ordre** des alternatives, et la **forme** de la branche vide -(un `|` nu contre un atome quantifié à zéro comme `\S{0}`). Mesuré (issue #335) : - -| pattern (ancré, `IgnoreCase`) | le moteur matche `""` | -| ------------------------------------- | --------------------- | -| `(?:\S{0}b{0}){1,2}` | oui | -| `(?:r{1,2}\|\S{0}){1,2}` | **non** | -| `(?:\S{0}\|r{1,2}){1,2}` (ordre inversé) | oui | -| `(?:r\|){1,2}` (branche vide nue) | oui | - -La construction structurelle a choisi la branche `\S{0}b{0}` et émis `""`, que le moteur refuse ensuite -pour cette forme — donc `Any.StringMatching` a retourné une valeur que le pattern même dont elle est -issue ne matche pas. Les patterns qui déclenchent ça sont dégénérés : FsCheck *génère* `\S{0}` (matcher -`\S` zéro fois) ; un humain écrit `\S*`. Mais le contrat « une valeur générée matche son pattern » était -violé. - -## Décision - -Après la construction structurelle, la valeur est **vérifiée contre le vrai moteur .NET** (un match -complet ancré sous la seule option honorée par le générateur — `IgnoreCase`) et **redessinée en cas -d'échec**, borné. La vérification a le dernier mot : une valeur que le moteur refuserait n'est jamais -retournée. Épuiser le plafond lève une `AnyGenerationException`. - -## Justification - -* **Tenir l'invariant par construction, pas par modélisation.** Les coins à-vide du moteur sont - ordre-dépendants, forme-dépendants, et spécifiques à l'implémentation et à la version — un jeu perdu à - poursuivre dans un modèle écrit à la main. Vérifier la sortie contre le moteur fait tenir « une valeur - générée matche son pattern » pour ce défaut **et toute divergence future** entre la construction - structurelle et le moteur, sans règle arcanique à maintenir. -* **Le redraw borné est l'idiome maison.** L'ADR-0033 satisfait déjà les exclusions de chaînes par un - redraw borné : un chemin structurel rapide plus un filet borné. C'est la même forme pour la même - raison. -* **Le coût est négligeable.** Un pattern supporté matche à la première construction ; seuls ces coins - rares redessinent, et une valeur valide apparaît en une poignée de tirages. La génération n'est pas une - boucle chaude, et `Any.StringMatching(Regex)` détient déjà un `Regex` compilé. Le plafond transforme un - pattern que la construction ne peut jamais satisfaire en une erreur claire au lieu d'une boucle - illimitée. -* **La reproductibilité est préservée.** Le redraw consomme d'autres tirages de la même source seedée, - donc une graine rejoue le run exactement. - -## Alternatives considérées - -### Modéliser la sémantique de match à vide du moteur - -Rejeté. Le comportement ci-dessus est ordre-dépendant, forme-dépendant, et n'est pas documenté par le -moteur comme un contrat stable ; un modèle serait fragile et devrait être revu à chaque évolution du -moteur — sans jamais être prouvé complet. - -### Refuser les patterns dégénérés comme non supportés - -Considéré : refuser un terme quantifié à zéro (`X{0}`) et/ou une alternative nullable sous quantificateur -par une `UnsupportedRegexException`, en gardant la génération purement structurelle. Rejeté parce que -détecter **chaque** divergence en amont est presque aussi dur que la modéliser — le risque étant de -refuser des patterns valides tout en en ratant d'autres — et cela rétrécit une capacité documentée pour -des patterns simplement inhabituels, pas hors du sous-ensemble supporté. Le redraw borné couvre toute la -classe sans détecteur fragile. - -## Conséquences - -### Positives - -* « Une valeur générée matche son pattern » est incassable — pour ce bug et pour toute divergence future - modèle/moteur. La property de round-trip `IgnoreCase` (#335) tient par construction, pas par chance de - la graine. - -### Négatives - -* Une valeur est construite puis vérifiée, plutôt que construite et retournée inconditionnellement — un - petit écart au « jamais généré puis filtré ». Le mécanisme principal reste structurel ; la vérification - est un filet pour l'échec rare, et la documentation du générateur le dit. -* Un pattern véritablement insatisfiable lève désormais une `AnyGenerationException` après le plafond au - lieu de retourner une valeur fausse — un échec plus clair, mais un échec là où l'ancien code retournait - silencieusement du n'importe quoi. - -## Références - -* ADR-0033 — Satisfaire les exclusions de chaînes par un redraw borné : l'idiome que ceci réutilise. -* ADR-0030 — Tirer des chaînes arbitraires depuis un ensemble terminal explicite : la philosophie - structurelle « construire, pas filtrer » que ceci complète plutôt que remplace. -* Issue #335 — le flake de round-trip `IgnoreCase` qui a rendu la divergence de match à vide concrète. diff --git a/doc/handwritten/for-maintainers/adr/0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.md b/doc/handwritten/for-maintainers/adr/0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.md deleted file mode 100644 index bae50be0..00000000 --- a/doc/handwritten/for-maintainers/adr/0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.md +++ /dev/null @@ -1,93 +0,0 @@ -# ADR-0048 | Guarantee a generated regex value matches its pattern, by bounded redraw - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-27 -**Accepted:** 2026-07-31 -**Decision Makers:** Reefact - -## Context - -`Any.StringMatching(...)` parses a pattern into a tree once and, on each draw, walks it to **build** a value -directly — never generate-then-filter. The build mirrors the regular subset of the .NET engine's -semantics, so a generated value is a genuine member of the pattern. - -A few corners of the engine's **empty-match** handling cannot be mirrored structurally, because .NET's -answer to "does the empty string match?" for a **nullable alternative under a quantifier** is -implementation-defined and depends on details a structural build does not carry: the **order** of the -alternatives, and the **form** of the empty branch (a bare `|` versus a zero-quantified atom such as -`\S{0}`). Measured (issue #335): - -| pattern (anchored, `IgnoreCase`) | engine matches `""` | -| ------------------------------------- | ------------------- | -| `(?:\S{0}b{0}){1,2}` | yes | -| `(?:r{1,2}\|\S{0}){1,2}` | **no** | -| `(?:\S{0}\|r{1,2}){1,2}` (order swapped) | yes | -| `(?:r\|){1,2}` (bare-empty branch) | yes | - -The structural build picked the `\S{0}b{0}` branch and emitted `""`, which the engine then refuses for -that shape — so `Any.StringMatching` returned a value the very pattern it was built from does not match. -The patterns that trigger this are degenerate: FsCheck *generates* `\S{0}` (match `\S` zero times); a -human writes `\S*`. But the contract "a generated value matches its pattern" was broken. - -## Decision - -After the structural build, the value is **verified against the real .NET engine** (a full, anchored -match under the one option the generator honoured — `IgnoreCase`) and **redrawn on a miss**, bounded. The -check is the last word: a value the engine would reject is never returned. Exhausting the cap raises an -`AnyGenerationException`. - -## Rationale - -* **Keep the invariant by construction, not by modelling.** The engine's empty-match corners are - order-dependent, form-dependent, and implementation- and version-specific — a losing game to chase in a - hand-written model. Verifying the output against the engine makes "a generated value matches its - pattern" hold for this defect **and any future divergence** between the structural build and the engine, - with no arcane rule to maintain. -* **Bounded redraw is the house idiom.** ADR-0033 already meets string exclusions with a bounded redraw: - a structural fast path plus a bounded safety net. This is the same shape for the same reason. -* **The cost is immaterial.** A supported pattern matches on the first build; only these rare corners - redraw, and a valid value appears within a handful of draws. Generation is not a hot loop, and - `Any.StringMatching(Regex)` already holds a compiled `Regex`. The cap turns a pattern the build can - never satisfy into a clear error instead of an unbounded loop. -* **Reproducibility is preserved.** The redraw consumes further draws from the same seeded source, so a - seed still replays the run exactly. - -## Alternatives Considered - -### Model the engine's empty-match semantics - -Rejected. The behaviour above is order-dependent, form-dependent, and not something the engine documents -as a stable contract; a model of it would be brittle and would need revisiting on engine changes — while -never being provably complete. - -### Refuse the degenerate patterns as unsupported - -Considered: refuse a zero-quantified term (`X{0}`) and/or a nullable alternative under a quantifier with -an `UnsupportedRegexException`, keeping generation purely structural. Rejected because detecting **every** -divergence eagerly is nearly as hard as modelling it — the risk is refusing some valid patterns while -still missing others — and it shrinks a documented capability for patterns that are merely unusual, not -outside the supported subset. The bounded redraw covers the whole class without a fragile detector. - -## Consequences - -### Positive - -* "A generated value matches its pattern" is un-breakable — for this bug and for any future model/engine - divergence. The `IgnoreCase` round-trip property (#335) holds by construction, not by luck of the seed. - -### Negative - -* A value is built and then checked, rather than built and returned unconditionally — a small departure - from "never generated then filtered". The primary mechanism stays structural; the check is a rare-miss - safety net, and the generator's documentation says so. -* A genuinely unsatisfiable pattern now raises an `AnyGenerationException` after the cap instead of - returning a wrong value — a clearer failure, but a failure where the old code silently returned garbage. - -## References - -* ADR-0033 — Meet string exclusions with a bounded redraw: the idiom this reuses. -* ADR-0030 — Draw arbitrary strings from an explicit terminal set: the structural, build-don't-filter - philosophy this complements rather than replaces. -* Issue #335 — the `IgnoreCase` round-trip flake that made the empty-match divergence concrete. diff --git a/doc/handwritten/for-maintainers/adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.fr.md b/doc/handwritten/for-maintainers/adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.fr.md deleted file mode 100644 index e6491612..00000000 --- a/doc/handwritten/for-maintainers/adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.fr.md +++ /dev/null @@ -1,179 +0,0 @@ -# ADR-0049 | Retirer le générateur JustDummies de la matrice de mutation par pull request - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md) - -**Statut :** Accepté -**Proposé :** 2026-07-28 -**Accepté :** 2026-07-28 -**Décideurs :** Reefact - -## Contexte - -L'ADR-0043 conditionne chaque pull request au score de mutation de ce qu'elle a changé. -`justdummies-mutation.yml` exécute ce gate sous forme d'une matrice à trois pattes — le générateur -(`justdummies`), son adaptateur xUnit v3 (`justdummies-xunit`) et ses analyseurs -(`justdummies-analyzers`) —, chacune limitée au diff par `--since`. - -Deux de ces trois pattes terminent en une quinzaine de secondes à une minute et demie. Celle du -générateur ne termine pas du tout. - -Sur la pull request #337 — un diff de correction en quatre commits, **99 lignes de production -changées** — la patte `Mutate the diff (justdummies)` a sélectionné **844 mutants** et tournait encore -après **soixante minutes**, sans avoir produit de score, avant d'être annulée à la main. Ce n'est pas un -cas isolé : le coût de cette patte est fixé par la taille des *fichiers* que le diff touche, pas par la -taille du diff. - -Trois contraintes, chacune enregistrée et mesurée, rendent ce coût structurel et non accidentel : - -* **`--since` a une granularité par fichier, pas par ligne.** Stryker mute tous les mutants d'un fichier - changé. Ces 99 lignes ont entraîné des fichiers entiers — `StringSpec.cs` (246 mutants), - `Any.Combine.cs` (205), `ContinuousIntervalSpec.cs` (204), `CollectionState.cs` (109) — de sorte que - près de neuf dixièmes du travail portaient sur du code que la pull request ne touche pas. -* **`"coverage-analysis": "off"` est obligatoire, pas un réglage.** Sous le runner MTP, la sélection de - tests de Stryker classe à tort des mutants tués comme non couverts, donc chaque mutant rejoue tout - l'oracle. C'est une décision d'exactitude (ADR-0043, et `mutation.en.md`, « Two settings that are not - tuning knobs »), pas un levier disponible ici. -* **JustDummies est la plus grosse bibliothèque du dépôt** — quelques milliers de mutants — ce qui est - déjà la raison pour laquelle son sweep *complet* porte `timeout-minutes: 350`. - -Le débit observé sur `ubuntu-latest` est d'environ **quatorze mutants par minute**. Une patte de deux à -trois minutes n'admet donc au plus que **quarante-cinq mutants**. - -Chaque levier exposé par Stryker 4.16 a été mesuré sur le diff de #337 : - -| Levier | Mutants | Réduction | -|---|---|---| -| Référence (`--since`) | 844 | — | -| `mutation-level: Basic` | 648 | −23 % | -| `ignore-mutations: [string]` | 766 | −9 % | -| `ignore-mutations: [string, block, statement]` | 542 | −36 % | - -Stryker.NET 4.16 n'offre **ni plafond de mutants ni échantillonnage** : les seuls filtres sont quels -mutateurs s'exécutent, quelles catégories de mutateurs s'exécutent, et quels *fichiers* sont mutés. Les -motifs `mutate` limités à des lignes — le seul levier qui ferait correspondre le travail au diff — sont -**silencieusement inertes** : `**/RegexNode.cs` sélectionne les 34 mutants de ce fichier, tandis que -`**/RegexNode.cs{153..165}`, `**/RegexNode.cs{153-165}` et les formes relatives au projet en -sélectionnent **zéro**, aussi bien dans le fichier de configuration qu'en ligne de commande. Un gate -configuré ainsi passerait au vert sans avoir rien testé. - -Le sharding de la patte sur les fichiers changés a également été envisagé et mesuré. Plusieurs motifs -`--mutate` se composent bien en union (34 + 13 = 47 mutants, vérifié), donc le sharding est -implémentable — mais un shard ne peut pas être plus petit qu'un fichier, et huit des fichiers centraux de -la bibliothèque dépassent à eux seuls le budget de quarante-cinq mutants : `RegexParser.cs` (507), -`DecimalIntervalSpec.cs` (388), `OrdinalIntervalSpec.cs` (387), `WideIntervalSpec.cs` (382), -`StringSpec.cs` (357), `UriSpec.cs` (304), `ContinuousIntervalSpec.cs` (284), `CollectionState.cs` (188). - -La meilleure combinaison atteignable tourne autour de −50 %, pour une exigence de −95 %. - -## Décision - -La patte `justdummies` est **retirée de la matrice par pull request** dans `justdummies-mutation.yml`. -`justdummies-xunit` et `justdummies-analyzers` conservent la leur. Le score de mutation du générateur -continue d'être mesuré par le **sweep complet hebdomadaire**, inchangé. - -Le job `gate` et son nom de check sont inchangés, donc aucune entrée de branch protection ne bouge. - -## Justification - -* **La patte ne produit rien aujourd'hui.** Elle n'est pas lente, elle est inachevée : soixante minutes - de runner pour aucun score. Retirer un check qui ne rapporte jamais ne perd aucun signal — cela cesse - de payer pour son absence. -* **Les mesures ferment les alternatives.** Chaque levier interne plafonne à −36 %, le sharding est buté - par le plus gros fichier changé, et le périmètre à la ligne n'existe pas dans cette version de Stryker. - Ce n'est pas « on n'a pas assez réglé ». -* **L'ADR-0046 lui a déjà retiré son autorité.** Le gate par PR est consultatif ; la barre appliquée est - le sweep hebdomadaire. Cette patte rapportait dans un canal qui ne peut pas refuser une pull request. -* **Le retrait étroit préserve ce qui marche.** Les pattes adaptateur et analyseurs sont petites, - terminent en quatre-vingt-dix secondes, et gardent le retour de mutation par PR là où il est abordable. - Retirer les trois jetterait un signal fonctionnel pour corriger un problème qui ne le concerne pas. - -## Alternatives envisagées - -### Sharder la patte sur les fichiers changés - -Envisagée parce qu'elle ne demande aucun ADR — c'est un détail d'implémentation du « gate le diff » de -l'ADR-0043, et la composition de plusieurs motifs `--mutate` a été vérifiée. Rejetée parce que le -plancher d'un shard est un fichier entier : sur le diff de #337, le shard `StringSpec.cs` seul fait -246 mutants, soit environ dix-huit minutes, et huit fichiers centraux dépassent individuellement le -budget. Cela transformerait « ne termine jamais » en « quinze à vingt minutes dès que la pull request -touche quelque chose d'intéressant » : de la vraie machinerie, pour une cible toujours manquée. - -### Plafonner le travail avec `mutation-level` et `ignore-mutations` - -Rejetée sur les chiffres ci-dessus : −36 % au mieux, un ordre de grandeur d'écart. Cela coûte en outre du -signal au mauvais endroit — retirer les mutations `statement` et `block`, c'est cesser de tester la -suppression des gardes d'arguments, qui fait partie des défauts que la mutation attrape le mieux -(l'ADR-0045 est la décision que ces gardes mettent en œuvre). - -### Échantillonner un sous-ensemble borné de mutants par pull request - -Envisagée parce qu'un signal partiel sur une patte consultative est défendable. Rejetée parce que Stryker -n'expose aucun échantillonnage : les mutants sont générés de façon déterministe et exhaustive depuis -l'arbre syntaxique, donc le seul moyen de borner leur nombre est de borner les *fichiers*. Sélectionner -les fichiers changés jusqu'à un budget de mutants biaise systématiquement l'échantillon vers les petits, -si bien que `RegexParser`, `StringSpec`, `UriSpec` et les moteurs d'intervalles — là où la mutation vaut -le plus — ne seraient jamais couverts par pull request. Faire tourner la sélection remonte à l'inverse -des survivants dans des fichiers que la pull request n'a pas touchés, ce qui n'aide le relecteur à -décider de rien. - -### Exécuter une patte nocturne limitée au diff - -Rejetée parce qu'elle n'existe pas telle que décrite : la limitation au diff a besoin du point de fork -d'une pull request, et après un merge il n'y a plus de diff auquel se comparer. Une exécution nocturne ne -peut être que le sweep *complet* — le job le plus long du dépôt — sept fois par semaine au lieu d'une, ce -qui coûte plus cher, pas moins. - -### Augmenter `timeout-minutes` au-delà de soixante - -Rejetée : la patte rapporterait une heure ou plus après que la pull request est prête, sur un check qui -ne peut pas la bloquer. C'est le coût sans le bénéfice. - -## Conséquences - -### Positives - -* Une heure de runner par pull request touchant le générateur, dépensée pour aucun résultat, est - récupérée. -* La liste des checks de la pull request se stabilise en quatre-vingt-dix secondes environ, au lieu de - rester suspendue à une patte qui ne rapporte jamais. -* Le retour de mutation par PR survit là où il fonctionne — l'adaptateur et les analyseurs. - -### Négatives - -* Une régression de mutation dans le générateur est vue le lundi plutôt que sur la pull request qui l'a - introduite. Avec `break: 0` sur cette bibliothèque, rien n'était appliqué sur la pull request de toute - façon ; ce qui est perdu est la liste des survivants dans le résumé du run, pas un gate. -* `justdummies-mutation.yml` et `mutation.yml` n'exécutent plus une matrice identique. Cette parité est - énoncée dans `justdummies-mutation.en.md`, que cette décision impose de mettre à jour. - -### Risques - -* **Le seuil que cela reporte.** `justdummies.json` porte `break: 0` parce qu'aucun score n'a été - mesuré ; le premier sweep complet doit publier le chiffre qui le fixera. Le jour où il le fera, la - patte par PR redeviendra souhaitable — et cette décision devra être revisitée plutôt que tenue pour - acquise. Atténuation : l'action de suivi ci-dessous. - -## Actions de suivi - -* Mettre à jour `justdummies-mutation.en.md` et sa traduction française : la matrice compte deux pattes - sur les pull requests, trois sur le sweep complet, et pourquoi. -* Rouvrir cette décision si Stryker acquiert des motifs `mutate` limités aux lignes qui fonctionnent, ou - si la sélection de couverture MTP (stryker-net#3629) est corrigée de sorte que `"coverage-analysis"` - puisse être activé — l'un comme l'autre change le modèle de coût qui décide de ceci. -* Revisiter quand le premier sweep hebdomadaire publiera le chiffre JustDummies et que `break` cessera - d'être 0. - -## Références - -* ADR-0043 — Conditionner les pull requests au score de mutation du diff : la décision que celle-ci - restreint. -* ADR-0046 — Rendre le gate de mutation par pull request consultatif : pourquoi la patte n'a aucune - autorité à perdre. -* ADR-0047 — Mesurer la mutation de JustDummies contre la seule suite unitaire : la tentative précédente - pour rendre cette patte abordable. -* ADR-0045 — Garder les arguments publics et internes contre `null` : les gardes que `ignore-mutations` - cesserait de tester. -* `.github/workflows/justdummies-mutation.yml`, `build/stryker/justdummies.json`. -* Pull request #337 — le run dont l'annulation après soixante minutes a motivé ceci. -* [stryker-net#3629](https://github.com/stryker-mutator/stryker-net/issues/3629) — le défaut de sélection - de couverture MTP derrière `"coverage-analysis": "off"`. diff --git a/doc/handwritten/for-maintainers/adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md b/doc/handwritten/for-maintainers/adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md deleted file mode 100644 index 7ea97afa..00000000 --- a/doc/handwritten/for-maintainers/adr/0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md +++ /dev/null @@ -1,169 +0,0 @@ -# ADR-0049 | Drop the JustDummies generator from the per-pull-request mutation matrix - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-28 -**Accepted:** 2026-07-28 -**Decision Makers:** Reefact - -## Context - -ADR-0043 gates every pull request on the mutation score of what it changed. `justdummies-mutation.yml` -runs that gate as a three-leg matrix — the generator (`justdummies`), its xUnit v3 adapter -(`justdummies-xunit`) and its analyzers (`justdummies-analyzers`) — each scoped to the diff with -`--since`. - -Two of those three legs finish in about ninety seconds. The generator's does not finish at all. - -On pull request #337 — a four-commit bug-fix diff of **99 changed production lines** — the -`Mutate the diff (justdummies)` leg selected **844 mutants** and was still running after **sixty -minutes**, producing no score, before it was cancelled by hand. It was not an outlier: the leg's cost is -set by the size of the *files* the diff touches, not by the size of the diff. - -Three constraints, each recorded and measured, together make that cost structural rather than incidental: - -* **`--since` is file-scoped, not line-scoped.** Stryker mutates every mutant in a changed file. Those - 99 lines pulled in whole files — `StringSpec.cs` (246 mutants), `Any.Combine.cs` (205), - `ContinuousIntervalSpec.cs` (204), `CollectionState.cs` (109) — so roughly nine tenths of the work - landed on code the pull request never touched. -* **`"coverage-analysis": "off"` is mandatory, not tuning.** Under the MTP runner Stryker's test - selection misclassifies killed mutants as uncovered, so every mutant re-runs the whole oracle. That is - an accuracy decision (ADR-0043, and `mutation.en.md`, "Two settings that are not tuning knobs"), not a - lever available here. -* **JustDummies is the largest library in the repository** — a few thousand mutants — which is why its - *full* sweep already carries `timeout-minutes: 350`. - -The observed rate on `ubuntu-latest` is about **fourteen mutants per minute**. A two-to-three minute leg -therefore admits at most **forty-five mutants**. - -Every lever Stryker 4.16 exposes was measured against the #337 diff: - -| Lever | Mutants | Reduction | -|---|---|---| -| Baseline (`--since`) | 844 | — | -| `mutation-level: Basic` | 648 | −23 % | -| `ignore-mutations: [string]` | 766 | −9 % | -| `ignore-mutations: [string, block, statement]` | 542 | −36 % | - -Stryker.NET 4.16 offers **no mutant cap and no sampling**: the only filters are which mutators run, which -mutator categories run, and which *files* are mutated. Line-scoped `mutate` patterns — the one lever that -would match the diff to the work — are **silently inert**: `**/RegexNode.cs` selects that file's 34 -mutants, while `**/RegexNode.cs{153..165}`, `**/RegexNode.cs{153-165}` and the project-relative forms -each select **zero**, in the configuration file as well as on the command line. A gate configured that -way would go green having tested nothing. - -Sharding the leg over the changed files was considered and measured too. Multiple `--mutate` patterns do -compose as a union (34 + 13 = 47 mutants, verified), so sharding is implementable — but a shard cannot be -smaller than one file, and eight of the library's central files exceed the forty-five-mutant budget on -their own: `RegexParser.cs` (507), `DecimalIntervalSpec.cs` (388), `OrdinalIntervalSpec.cs` (387), -`WideIntervalSpec.cs` (382), `StringSpec.cs` (357), `UriSpec.cs` (304), `ContinuousIntervalSpec.cs` (284), -`CollectionState.cs` (188). - -The best achievable combination is roughly −50 %, against a requirement of −95 %. - -## Decision - -The `justdummies` leg is **removed from the per-pull-request matrix** in `justdummies-mutation.yml`. -`justdummies-xunit` and `justdummies-analyzers` keep theirs. The generator's mutation score continues to -be measured by the **weekly full sweep**, unchanged. - -The `gate` job and its check name are unchanged, so no branch-protection entry moves. - -## Rationale - -* **The leg produces nothing today.** It is not slow, it is unfinished: sixty minutes of runner time for - no score. Removing a check that never reports loses no signal — it stops paying for the absence of one. -* **The measurements close the alternatives.** Every in-tool lever tops out at −36 %, sharding is floored - by the largest changed file, and line-scoping does not exist in this Stryker version. This is not - "we did not tune it hard enough". -* **ADR-0046 already removed its authority.** The per-PR gate is advisory; the enforced bar is the weekly - sweep. The leg was reporting into a channel that cannot fail a pull request. -* **The narrow removal keeps what works.** The adapter and analyzer legs are small, finish in ninety - seconds, and keep per-PR mutation feedback where it is affordable. Dropping all three would discard a - working signal to fix an unrelated one. - -## Alternatives Considered - -### Shard the leg over the changed files - -Considered because it needs no ADR — it is an implementation detail of ADR-0043's "gate the diff", and -multiple `--mutate` patterns were verified to compose. Rejected because a shard's floor is one whole -file: on the #337 diff the `StringSpec.cs` shard alone is 246 mutants, about eighteen minutes, and eight -central files are individually over budget. It would turn "never finishes" into "fifteen to twenty -minutes whenever the pull request touches anything interesting" — real machinery, for a target it still -misses. - -### Cap the work with `mutation-level` and `ignore-mutations` - -Rejected on the numbers above: −36 % at best, an order of magnitude short. It also costs signal in the -wrong place — dropping `statement` and `block` mutations stops testing the removal of argument guards, -which is among the defects mutation testing catches best (ADR-0045 is the decision those guards -implement). - -### Sample a bounded subset of mutants per pull request - -Considered because a partial signal on an advisory leg is defensible. Rejected because Stryker exposes no -sampling: mutants are generated deterministically and exhaustively from the syntax tree, so the only way -to bound the count is to bound the *files*. Selecting changed files up to a mutant budget biases the -sample systematically toward the small ones, so `RegexParser`, `StringSpec`, `UriSpec` and the interval -engines — where mutation testing is worth most — would never be covered per pull request. Rotating the -selection instead surfaces survivors in files the pull request did not touch, which does not help the -reviewer decide anything. - -### Run a nightly diff-scoped leg instead - -Rejected because it does not exist as described: diff-scoping needs a pull request's fork point, and -after a merge there is no diff to scope to. A nightly can only be the *full* sweep — the repository's -longest job — seven times a week instead of once, which costs more, not less. - -### Raise `timeout-minutes` above sixty - -Rejected: the leg would report an hour or more after the pull request is ready, on a check that cannot -block it. That is the cost without the benefit. - -## Consequences - -### Positive - -* An hour of runner time per pull request touching the generator, spent on no result, is recovered. -* The pull request check list settles in about ninety seconds instead of hanging on a leg that never - reports. -* Per-PR mutation feedback survives where it works — the adapter and the analyzers. - -### Negative - -* A mutation regression in the generator is now seen on Monday rather than on the pull request that - introduced it. With `break: 0` on this library, nothing was being enforced on the pull request either - way; what is lost is the survivor list in the run summary, not a gate. -* `justdummies-mutation.yml` and `mutation.yml` no longer run an identical matrix. That parity is stated - in `justdummies-mutation.en.md`, which this decision requires updating. - -### Risks - -* **The threshold this defers.** `justdummies.json` carries `break: 0` because no score has been - measured; the first full sweep is meant to publish the figure that sets it. Once it does, the per-PR - leg becomes worth having again — and this decision will need revisiting rather than assuming it is - settled. Mitigation: the follow-up below. - -## Follow-up Actions - -* Update `justdummies-mutation.en.md` and its French translation: the matrix is two legs on pull - requests, three on the full sweep, and why. -* Re-open this decision if Stryker gains working line-scoped `mutate` patterns, or if the MTP coverage - selection (stryker-net#3629) is fixed so `"coverage-analysis"` can be turned on — either one changes the - cost model that decides this. -* Revisit when the first weekly sweep publishes the JustDummies figure and `break` stops being 0. - -## References - -* ADR-0043 — Gate pull requests on the mutation score of the diff: the decision this narrows. -* ADR-0046 — Make the per-pull-request mutation gate advisory: why the leg has no authority to lose. -* ADR-0047 — Measure JustDummies mutation against the unit suite only: the previous attempt to make this - leg affordable. -* ADR-0045 — Guard public and internal arguments against null: the guards `ignore-mutations` would stop - testing. -* `.github/workflows/justdummies-mutation.yml`, `build/stryker/justdummies.json`. -* Pull request #337 — the run whose cancellation after sixty minutes prompted this. -* [stryker-net#3629](https://github.com/stryker-mutator/stryker-net/issues/3629) — the MTP coverage - selection defect behind `"coverage-analysis": "off"`. diff --git a/doc/handwritten/for-maintainers/adr/0050-let-a-size-maximum-cap-without-steering-the-draw.fr.md b/doc/handwritten/for-maintainers/adr/0050-let-a-size-maximum-cap-without-steering-the-draw.fr.md deleted file mode 100644 index f284d701..00000000 --- a/doc/handwritten/for-maintainers/adr/0050-let-a-size-maximum-cap-without-steering-the-draw.fr.md +++ /dev/null @@ -1,199 +0,0 @@ -# ADR-0050 | Laisser un maximum de taille plafonner sans piloter le tirage, et plafonner une taille explicitement demandée - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0050-let-a-size-maximum-cap-without-steering-the-draw.md) - -**Statut :** Accepté -**Proposé :** 2026-07-28 -**Accepté :** 2026-07-28 -**Décideurs :** Reefact - -## Contexte - -JustDummies permet à un test de déclarer une taille par deux familles : `WithLength`, `WithMinLength`, -`WithMaxLength` et `WithLengthBetween` sur les chaînes ; `WithCount`, `WithMinCount`, `WithMaxCount` et -`WithCountBetween` sur les collections. - -Non contraint, un dummy est délibérément petit : une chaîne tire entre 0 et 16 caractères, une -collection entre 0 et 8 éléments. L'ADR-0025 désigne ce défaut comme le « 0 to a handful » que les -générateurs de chaînes et de collections partagent déjà, et le réutilise pour les quantificateurs regex -non bornés. - -Un maximum déclaré, en revanche, ne se compose pas avec ce défaut — il le **remplace**. Le tirage devient -uniforme sur tout l'intervalle déclaré, si bien que la borne haute sert aussi d'indice de taille : -`WithMaxLength(100000)` produit des chaînes d'environ 60 000 caractères, là où le même générateur laissé -non contraint en produit de 0 à 16. Deux politiques de taille différentes s'appliquent à ce qu'un lecteur -perçoit comme une seule et même chose. - -La seule validation d'argument sur ces méthodes est la non-négativité ; rien ne borne le haut. Poussés à -`int.MaxValue`, les quatre points d'entrée ont été mesurés comme se comportant de quatre manières -différentes : - -| déclaration | comportement mesuré | -| ----------------------------- | ------------------------------------------------------------------ | -| `WithLength(int.MaxValue)` | `ArgumentOutOfRangeException` nommant un paramètre interne, issue d'un débordement arithmétique dans le tirage | -| `WithMaxLength(int.MaxValue)` | rend une chaîne d'environ 130 Mo | -| `WithMaxCount(int.MaxValue)` | s'exécute pendant des minutes | -| `WithCount(int.MaxValue)` | échoue immédiatement | - -Cette divergence n'est pas conçue. Deux des quatre découlent directement du maximum qui pilote le tirage. -Les deux autres partagent un même chemin de code et ne diffèrent que par l'endroit où le nombre demandé -tombe par rapport aux limites de l'allocateur : une demande de capacité est refusée d'emblée, l'autre est -accordée puis remplie élément par élément. Aucun des quatre échecs n'est levé par la bibliothèque : deux -sont des exceptions du BCL nommant des paramètres que l'appelant n'a jamais écrits, un est une attente -non bornée, un est une valeur silencieusement énorme. - -La bibliothèque possède déjà une taxonomie d'exceptions. Une erreur d'appelant sur un argument isolé -remonte en exception d'argument du BCL — `UnsupportedRegexException` le documente pour un pattern mal -formé, et l'ADR-0045 l'a fixé pour `null` sur toute la surface, avec un test de convention par réflexion -pour l'appliquer. Une contradiction *entre* contraintes déclarées lève une -`ConflictingAnyConstraintException` à la déclaration. Une génération qui échoue malgré des contraintes -acceptées lève une `AnyGenerationException`. - -Les grandes tailles ont des usages légitimes : les tests qui exercent une limite métier (« refuse un -libellé de plus de 255 caractères », « le lot se découpe au-delà de 1 000 éléments »). Ces tailles sont -calibrées sur la limite testée — des centaines, des milliers, des dizaines de milliers — soit deux ordres -de grandeur en dessous des valeurs qui produisent les comportements ci-dessus. - -JustDummies n'a jamais été publié, donc le sens d'une borne déclarée est encore libre d'être fixé (le -même acquis sur lequel s'appuyait l'ADR-0041). - -## Décision - -Un maximum de taille déclaré ne fait jamais que rétrécir un tirage et ne l'élargit jamais au-delà du -spread par défaut, et une taille que le générateur doit réellement produire — une longueur ou une -cardinalité exacte ou minimale — est refusée au-delà de 1 000 000 par une `ArgumentOutOfRangeException` à -la déclaration. - -## Justification - -* **Le maximum qui pilote est la cause, pas un quatrième symptôme.** Dès lors qu'un maximum ne fait que - plafonner, une borne lâche n'enfle plus le tirage, et deux des quatre comportements mesurés cessent - d'exister : un `WithMaxLength` énorme rend une chaîne petite ordinaire, un `WithMaxCount` énorme une - collection petite ordinaire. Supprimer une cause vaut mieux que garder quatre effets, et c'est ce qui - rend le garde restant assez petit pour tenir en une phrase. -* **Une borne est une permission, pas une demande.** « Au plus N » énonce ce que la valeur ne doit pas - dépasser ; il ne dit rien de la taille souhaitée. C'est de la lire comme une demande que vient le - désaccord entre le défaut non contraint et le défaut borné, pour ce qu'un lecteur perçoit comme le même - générateur. Sous cette décision, une seule politique gouverne la taille partout : un dummy est petit à - moins que quelque chose ne demande explicitement plus, et seuls un minimum, une taille exacte ou un - fragment requis peuvent le demander. -* **Ne plafonner que ce qui doit être produit supprime les faux positifs du garde.** Un maximum ne coûte - rien à honorer, donc plafonner une chaîne à une largeur de colonne de quatre millions reste légal et - continue de produire de petits dummies. Seule une taille que la bibliothèque devrait matérialiser est - refusée — soit exactement l'ensemble qui décide de la mémoire et du travail que coûte un tirage. -* **Le plafond relève de la validation d'argument, pas des exceptions propres à la bibliothèque.** Une - taille trop grande est un argument isolément inutilisable, exactement comme la taille négative déjà - rejetée à cet endroit ; ce n'est pas une contradiction entre deux contraintes, et ce n'est pas une - génération qui a échoué. Suivre la taxonomie laisse inchangé le nombre de types d'exceptions et de - catégories documentées, et remplace un message nommant un paramètre interne par un message nommant le - paramètre que l'appelant a écrit. -* **1 000 000 se place dans l'écart entre le légitime et l'absurde.** C'est cinq ordres de grandeur - au-dessus du spread par défaut, donc l'usage ordinaire ne peut pas l'approcher ; c'est deux ordres de - grandeur au-dessus de la plus grande limite métier qu'un test de bord exerce plausiblement, donc un tel - test n'est jamais refusé ; et une valeur de cette taille se matérialise encore en millisecondes, donc - le plafond ne transforme jamais un test lent en test rapide — il transforme un gel ou un échec - d'allocation en échec diagnostiquable. Un plafond plus bas commencerait à refuser le test de bord qui - vérifie légitimement une entrée de 64 Ko. -* **C'est un test de convention qui maintient la règle vraie.** Le débordement derrière le premier - comportement mesuré existe parce que la même arithmétique a déjà été rendue sûre une ligne plus haut et - pas ici ; une règle appliquée à la main à chaque méthode prenant une taille sera oubliée par le - prochain builder exactement de la même façon. L'ADR-0045 a établi l'application par réflexion comme la - réponse de ce dépôt à une règle qui doit tenir sur toute une surface, y compris les membres pas encore - écrits. - -## Alternatives considérées - -### Plafonner tout argument de taille, maxima compris - -Considérée pour l'uniformité d'une règle unique sans exception à retenir. Rejetée : un maximum est -gratuit à honorer dès lors qu'il ne pilote plus le tirage, donc en refuser un grand n'achète aucune -protection tout en refusant une déclaration légitime — un plafond reflétant une limite de stockage -supérieure au plafond. La règle tient en une phrase dans les deux cas, et cette version-ci n'a pas de -faux positifs. - -### Lever le dépassement de plafond comme une exception de la bibliothèque - -Considérée : une `ConflictingAnyConstraintException`, ou un nouveau membre de la hiérarchie propre à la -bibliothèque, pour que toute la surface d'échec s'attrape en une clause. Rejetée parce qu'elle contredit -la taxonomie consignée ailleurs dans la bibliothèque : un argument isolément inutilisable est une erreur -d'appelant, pas une interaction de contraintes, et le faire correspondre à un conflit ferait dire deux -choses différentes au mot « conflit ». Réserver la hiérarchie de la bibliothèque à ce que la bibliothèque -décide elle-même est ce qui garde cette hiérarchie signifiante. - -### Offrir une échappatoire par appel pour les très grandes tailles - -Considérée, pour qu'aucun usage légitime ne soit jamais bloqué. Rejetée sur le terrain de la demande : -aucun usage de ce type n'est recensé, l'échappatoire invite le mésusage que le plafond existe pour -empêcher, et en ajouter une plus tard est un ajout non cassant alors qu'en retirer une serait cassant. Un -besoin réel se traite en révisant le plafond — une décision — plutôt que par un contournement par appel. - -### Traiter `Between` comme une demande explicite de sa plage - -Considérée parce que `WithLengthBetween(0, 100000)` se lit comme une demande de valeurs réparties sur -cette plage, et que sous cette décision il rend le petit défaut à la place. Rejetée parce qu'elle -briserait l'identité entre `WithLengthBetween(a, b)` et le même générateur déclaré avec un minimum et un -maximum : deux écritures d'une seule contrainte tireraient différemment, alors que l'uniformité de -l'algèbre de contraintes est une propriété délibérée de cette API. En pratique, une plage partant de zéro -s'écrit pour exprimer une limite, et un test qui veut de grandes valeurs relève le minimum — ce qui se -lit pour ce que c'est. - -### Ne corriger que le débordement arithmétique - -Considérée comme le changement minimal, puisque c'est le seul comportement qui produit un message -déroutant. Rejetée : elle traite le moins nuisible des quatre. Une chaîne de 130 Mo silencieuse et une -exécution de plusieurs minutes coûtent bien plus cher à diagnostiquer qu'une exception au message -médiocre, et ni l'une ni l'autre n'est touchée par une arithmétique protégée du débordement. - -## Conséquences - -### Positives - -* Une seule politique de taille sur toute l'API : un dummy est petit à moins que quelque chose ne demande - explicitement plus. -* Deux des quatre comportements mesurés disparaissent comme conséquence de la politique, sans qu'aucun - garde n'intervienne. -* Une taille absurde est signalée à la déclaration, contre le paramètre que l'appelant a écrit, au lieu - de se manifester par un gel, un échec d'allocation ou un message du BCL sur de l'arithmétique interne. -* Le débordement arithmétique devient inatteignable, puisqu'une taille produite ne peut plus approcher la - plage où il survient. -* Aucun nouveau type d'exception et aucune nouvelle catégorie documentée. - -### Négatives - -* Une déclaration qui produisait de grandes valeurs en produit désormais de petites. Que - `WithMaxLength(100000)` rende des chaînes de 0 à 16 caractères est le nouveau comportement voulu, et il - surprendra quiconque lisait la borne comme un indice de taille — la documentation doit énoncer la règle - explicitement plutôt que de la laisser deviner. -* `WithLengthBetween(0, N)` rend le spread par défaut plutôt que des valeurs réparties sur la plage, ce - qui est le prix accepté pour garder `Between` décomposable. -* Le plafond est une constante sans dérivation. Il est défendable, pas démontrable, et l'argument qui le - soutient repose sur l'écart entre le légitime et l'absurde plutôt que sur une mesure du runtime. - -### Risques - -* Un consommateur ayant légitimement besoin de plus que le plafond est bloqué jusqu'à révision. Atténué - par la taille de l'écart : le plafond est très au-dessus de tout test de bord calibré sur une limite - métier. -* Le test de convention doit reconnaître un paramètre porteur de taille pour tenir un futur builder à la - règle ; un paramètre de taille nommé hors convention y échapperait silencieusement. C'est la même - exposition que l'ADR-0045 a acceptée pour sa propre règle par réflexion. - -## Actions de suivi - -* Trancher, à l'implémentation, si le plafond porte sur la taille que l'appelant énonce directement ou - sur le minimum effectif une fois les fragments requis comptés — les deux ne diffèrent que pour une - déclaration dont le préfixe, le suffixe ou les valeurs contenues sont eux-mêmes proches du plafond. -* Garder l'arithmétique du tirage protégée du débordement indépendamment du plafond : l'inatteignabilité - est une propriété de la règle actuelle, pas une garantie, et la forme sûre ne coûte rien. -* Énoncer la règle dans la documentation utilisateur, anglaise et française, là où les contraintes de - taille sont décrites. - -## Références - -* ADR-0025 — Generate strings from a home-grown regular subset : nomme le défaut « 0 to a handful » que - cette décision restaure là où un maximum le court-circuitait. -* ADR-0041 — Draw flag-enum combinations behind an opt-in : l'acquis selon lequel une bibliothèque non - publiée peut encore fixer le sens d'un tirage non contraint. -* ADR-0045 — Guard public and internal arguments against null : la posture de validation d'argument et le - test de convention par réflexion que cette décision réutilise. -* Issue #226 — le backlog JustDummies sous lequel les items demand-driven de l'audit ont été classés. diff --git a/doc/handwritten/for-maintainers/adr/0050-let-a-size-maximum-cap-without-steering-the-draw.md b/doc/handwritten/for-maintainers/adr/0050-let-a-size-maximum-cap-without-steering-the-draw.md deleted file mode 100644 index 23595e3a..00000000 --- a/doc/handwritten/for-maintainers/adr/0050-let-a-size-maximum-cap-without-steering-the-draw.md +++ /dev/null @@ -1,184 +0,0 @@ -# ADR-0050 | Let a size maximum cap without steering the draw, and ceiling an explicitly demanded size - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0050-let-a-size-maximum-cap-without-steering-the-draw.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-28 -**Accepted:** 2026-07-28 -**Decision Makers:** Reefact - -## Context - -JustDummies lets a test declare a size through two families: `WithLength`, `WithMinLength`, -`WithMaxLength` and `WithLengthBetween` on strings; `WithCount`, `WithMinCount`, `WithMaxCount` and -`WithCountBetween` on collections. - -Unconstrained, a dummy is deliberately small: a string draws between 0 and 16 characters, a collection -between 0 and 8 elements. ADR-0025 refers to this as the "0 to a handful" default the string and -collection generators already share, and reuses it for unbounded regex quantifiers. - -A declared maximum, however, does not compose with that default — it **replaces** it. The draw becomes -uniform over the whole declared interval, so the upper bound doubles as a size hint: -`WithMaxLength(100000)` yields strings of roughly 60 000 characters, while the same generator left -unconstrained yields 0 to 16. Two different size policies apply to what a reader sees as one thing. - -The only argument validation on these methods is non-negativity; nothing bounds the top. Pushed to -`int.MaxValue`, the four entry points were measured to behave in four different ways: - -| declaration | measured behaviour | -| ---------------------------- | --------------------------------------------------------------- | -| `WithLength(int.MaxValue)` | `ArgumentOutOfRangeException` naming an internal parameter, from an arithmetic overflow inside the draw | -| `WithMaxLength(int.MaxValue)` | returns a string of about 130 MB | -| `WithMaxCount(int.MaxValue)` | runs for minutes | -| `WithCount(int.MaxValue)` | fails immediately | - -The divergence is not designed. Two of the four follow directly from the maximum steering the draw. The -other two share one code path and differ only in where the requested number falls relative to the -allocator's limits: one capacity request is refused outright, the other is granted and then filled one -element at a time. None of the four failures is raised by the library: two are BCL exceptions naming -parameters the caller never wrote, one is an unbounded wait, one is a silently enormous value. - -The library already has an exception taxonomy. A caller mistake on a single argument surfaces as a BCL -argument exception — `UnsupportedRegexException` documents this for a malformed pattern, and ADR-0045 -fixed it for `null` across the whole surface, enforced by a reflection-driven convention test. A -contradiction *between* declared constraints raises `ConflictingAnyConstraintException` at declaration -time. A generation that fails despite accepted constraints raises `AnyGenerationException`. - -Large sizes do have legitimate uses: tests that exercise a business limit ("rejects a label longer than -255 characters", "the batch splits past 1 000 items"). Those sizes are calibrated on the limit under -test — hundreds, thousands, tens of thousands — two orders of magnitude below the values that produce -the behaviours above. - -JustDummies has never been released, so the meaning of a declared bound is still free to be fixed -(the same standing ADR-0041 relied on). - -## Decision - -A declared size maximum only ever narrows a draw and never widens it beyond the default spread, and a -size the generator must actually produce — an exact or minimum length or count — is refused above -1 000 000 with an `ArgumentOutOfRangeException` at declaration time. - -## Rationale - -* **The steering maximum is the cause, not a fourth symptom.** Once a maximum only caps, a loose bound - no longer inflates the draw, and two of the four measured behaviours stop existing: an enormous - `WithMaxLength` yields an ordinary small string, an enormous `WithMaxCount` an ordinary small - collection. Removing a cause is worth more than guarding four effects, and it is what makes the - remaining guard small enough to specify in one sentence. -* **A bound is a permission, not a request.** "At most N" states what the value must not exceed; it says - nothing about what size is wanted. Reading it as a request is what makes the unconstrained default and - the bounded default disagree for what a reader sees as the same generator. Under this decision one - policy governs size everywhere: a dummy is small unless something explicitly asks for more, and only a - minimum, an exact size or a required fragment can ask. -* **Ceilinging only what must be produced removes the guard's false positives.** A maximum costs - nothing to honour, so capping a string at a column width of four million stays legal and keeps - yielding small dummies. Only a size the library would have to materialize is refused — which is - exactly the set that decides how much memory and work a draw costs. -* **The ceiling belongs to the argument-validation category, not to the library's own exceptions.** A - size too large is a single argument unusable on its own, exactly like the negative size already - rejected there; it is not a contradiction between two constraints, and it is not a generation that - failed. Following the taxonomy keeps the count of exception types and documented categories unchanged, - and replaces a message naming an internal parameter with one naming the parameter the caller wrote. -* **1 000 000 sits in the gap between legitimate and absurd.** It is five orders of magnitude above the - default spread, so ordinary use cannot approach it; it is two orders of magnitude above the largest - business limit a boundary test plausibly exercises, so such a test is never refused; and a value of - that size still materializes in milliseconds, so the ceiling never turns a slow test into a fast one — - it turns a hang or an allocation failure into a diagnosable one. A lower ceiling would start refusing - the boundary test that legitimately checks a 64 KB input. -* **A convention test is what keeps the rule true.** The overflow behind the first measured behaviour - exists because the same arithmetic was already made overflow-safe one line away and not here; a rule - applied by hand to each size-taking method will be forgotten by the next builder in exactly the same - way. ADR-0045 established reflection-driven enforcement as this repository's answer to a rule that must - hold across a whole surface, including the members not yet written. - -## Alternatives Considered - -### Ceiling every size argument, maxima included - -Considered for the uniformity of a single rule with no exception to remember. Rejected: a maximum is -free to honour once it no longer steers the draw, so refusing a large one buys no protection while -refusing a legitimate declaration — a cap mirroring a storage limit larger than the ceiling. The rule -stays one sentence either way, and this version has no false positives. - -### Raise the ceiling breach as a library exception - -Considered: a `ConflictingAnyConstraintException`, or a new member of the library's own hierarchy, so -that the whole failure surface is catchable in one clause. Rejected because it contradicts the taxonomy -recorded elsewhere in the library: an argument that is unusable on its own is a caller mistake, not a -constraint interaction, and mapping it to a conflict would make the word "conflict" mean two different -things. Reserving the library's hierarchy for what the library itself decides is what keeps that -hierarchy meaningful. - -### Offer a per-call escape hatch for very large sizes - -Considered, so that no legitimate use is ever blocked. Rejected on demand grounds: no such use is -recorded, the escape hatch invites the misuse the ceiling exists to prevent, and adding one later is a -non-breaking addition whereas removing one would be breaking. A genuine need is met by revisiting the -ceiling — a decision — rather than by a per-call bypass. - -### Treat `Between` as an explicit request for its range - -Considered because `WithLengthBetween(0, 100000)` reads as a request for values spread across that -range, and under this decision it yields the small default instead. Rejected because it would break the -identity between `WithLengthBetween(a, b)` and the same generator declared with a minimum and a maximum: -two spellings of one constraint would draw differently, and the uniform constraint algebra is a -deliberate property of this API. In practice a range starting at zero is written to express a limit, and -a test that wants large values raises the minimum — which reads as what it is. - -### Fix only the arithmetic overflow - -Considered as the minimal change, since it is the one behaviour that produces a confusing message. -Rejected: it addresses the least harmful of the four. A silent 130 MB string and a run of several -minutes cost far more to diagnose than an exception with a poor message, and neither is touched by -overflow-safe arithmetic. - -## Consequences - -### Positive - -* One size policy across the API: a dummy is small unless something explicitly asks for more. -* Two of the four measured behaviours disappear as a consequence of the policy, with no guard involved. -* An absurd size is reported at declaration time, against the parameter the caller wrote, instead of - surfacing as a hang, an allocation failure, or a BCL message about internal arithmetic. -* The arithmetic overflow becomes unreachable, since a produced size can no longer approach the range - where it occurs. -* No new exception type and no new documented category. - -### Negative - -* A declaration that used to yield large values now yields small ones. `WithMaxLength(100000)` returning - 0-to-16-character strings is the intended new behaviour, and it will surprise anyone who read the - bound as a size hint — the documentation has to state the rule explicitly rather than let it be - inferred. -* `WithLengthBetween(0, N)` yields the default spread rather than values across the range, which is the - accepted price of keeping `Between` decomposable. -* The ceiling is a constant with no derivation. It is defensible, not provable, and the argument for it - rests on the gap between legitimate and absurd rather than on a measurement of the runtime. - -### Risks - -* A consumer legitimately needing more than the ceiling is blocked until it is revisited. Mitigated by - the size of the gap: the ceiling is far above any boundary test calibrated on a business limit. -* The convention test must recognize a size-carrying parameter to hold a future builder to the rule; a - size parameter named outside the convention would escape it silently. This is the same exposure - ADR-0045 accepted for its own reflection-driven rule. - -## Follow-up Actions - -* Settle, during implementation, whether the ceiling applies to a size the caller states directly or to - the effective minimum after required fragments are counted — the two differ only for a declaration - whose prefix, suffix or contained values are themselves near the ceiling. -* Keep the draw's arithmetic overflow-safe independently of the ceiling: unreachability is a property of - the current rule, not a guarantee, and the safe form costs nothing. -* State the rule in the user documentation, English and French, where the size constraints are - described. - -## References - -* ADR-0025 — Generate strings from a home-grown regular subset: names the "0 to a handful" default this - decision restores where a maximum used to bypass it. -* ADR-0041 — Draw flag-enum combinations behind an opt-in: the standing that an unreleased library may - still fix the meaning of an unconstrained draw. -* ADR-0045 — Guard public and internal arguments against null: the argument-validation posture and the - reflection-driven convention test this decision reuses. -* Issue #226 — the JustDummies backlog the audit's demand-driven items were filed under. diff --git a/doc/handwritten/for-maintainers/adr/0051-filter-the-datetimeoffset-pool-by-the-declared-offset.fr.md b/doc/handwritten/for-maintainers/adr/0051-filter-the-datetimeoffset-pool-by-the-declared-offset.fr.md deleted file mode 100644 index 8a0d91bc..00000000 --- a/doc/handwritten/for-maintainers/adr/0051-filter-the-datetimeoffset-pool-by-the-declared-offset.fr.md +++ /dev/null @@ -1,117 +0,0 @@ -# ADR-0051 | Filtrer le pool DateTimeOffset par le décalage déclaré - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0051-filter-the-datetimeoffset-pool-by-the-declared-offset.md) - -**Statut :** Accepté -**Proposé :** 2026-07-28 -**Accepté :** 2026-07-28 -**Décideurs :** Reefact - -Supersède l'[ADR-0037](0037-vary-the-datetimeoffset-offset-dimension.fr.md). - -## Contexte - -L'ADR-0037 a doté `AnyDateTimeOffset` d'une dimension de décalage et a consigné, sous *Risques*, que la combiner à -`OneOf` laisserait le décalage inappliqué : « `WithOffset` combiné à `OneOf` ne remplace pas le décalage propre d'une -valeur `OneOf`. Atténuation : documenté, et cohérent avec la sémantique d'énumération terminale de `OneOf`. » - -L'atténuation n'a pas tenu, et le risque est plus grand qu'une surprise. - -La documentation XML publique de `WithOffset` énonce qu'elle « épingle la dimension de décalage — **chaque valeur -générée porte exactement ce décalage** » et déclare lever `ConflictingAnyConstraintException` « lorsque la contrainte -en contredit une déjà déclarée ». Combinée à `OneOf`, elle ne faisait ni l'un ni l'autre : la contrainte était -abandonnée dans les deux ordres de déclaration, aucune exception n'était levée, et les valeurs sortaient avec leur -propre décalage. Le contrat publié disait l'inverse de ce que faisait le code, et le readme de JustDummies ne -mentionne pas du tout l'interaction. - -La bibliothèque répond à cette forme de façon cohérente partout ailleurs. -`Any.Int32().OneOf(1, 2, 3).GreaterThan(10)` et `Any.DateTime().OneOf(d1, d2).After(2022)` lèvent toutes deux une -`ConflictingAnyConstraintException` ; `OneOf(1, 2, 3).GreaterThan(1)` resserre et tire. `DateTimeOffset` était la -seule famille où une contrainte déclarée après un pool n'était ni appliquée ni refusée. - -La règle qui gouverne une contrainte fluent dans ce dépôt est qu'une méthode offerte par la DSL doit être honorée -quand ses arguments le permettent et doit échouer quand ils ne le permettent pas. L'abandonner en silence n'est ni -l'un ni l'autre. - -## Décision - -Un décalage déclaré **filtre** le pool `OneOf` aux valeurs dont il admet le décalage, dans les deux ordres de -déclaration, et entre en conflit lorsqu'il n'en admet aucune. - -## Justification - -* **Cela restaure le contrat publié.** `WithOffset` promet que chaque valeur générée porte ce décalage, et c'est - désormais le cas — y compris pour une valeur du pool, puisqu'une valeur portant un autre décalage n'est simplement - pas tirée. -* **Cela conserve la moitié juste de l'ADR-0037.** Une valeur du pool est toujours rendue telle quelle, décalage - compris : la reconstruire depuis l'instant normaliserait le décalage vers UTC, ce que l'ADR-0037 voulait - précisément éviter. Ce qui change, c'est *quelles* valeurs du pool peuvent être tirées, pas la façon de rendre - celle qui l'est. -* **Cela met les deux ordres d'accord.** Déclarer le pool d'abord ou le décalage d'abord aboutit maintenant au même - verdict, propriété que la bibliothèque garantit déjà pour toute autre paire de contraintes et sur laquelle un - appelant n'a aucun moyen de raisonner autrement. -* **Une contradiction est signalée plutôt qu'avalée.** Un décalage qu'aucune valeur du pool ne porte est une - spécification que le générateur ne peut pas satisfaire ; échouer à la déclaration est la raison d'être du contrôle - anticipé, et c'est ce que la documentation annonçait déjà à l'appelant. - -## Alternatives envisagées - -### Conserver le comportement et corriger plutôt la documentation - -Envisagée parce que c'est la résolution la moins chère et parce que l'ADR-0037 y était déjà parvenue par le -raisonnement. Rejetée parce que la documentation devrait alors décrire une règle valable pour une famille de -générateurs et pour aucune autre, et parce que l'appelant qui écrit `WithOffset` après un pool demande quelque chose -que la bibliothèque sait trancher : ou bien une valeur du pool porte ce décalage, ou bien aucune. Documenter un -no-op silencieux n'en fait pas une bonne réponse, et la divergence se découvrirait là où un test passe pour la -mauvaise raison. - -### Réécrire le décalage de la valeur du pool avec celui déclaré - -Envisagée parce qu'elle honore `WithOffset` littéralement dans tous les cas, sans contradiction à signaler. Rejetée -parce qu'elle modifie la valeur fournie par l'appelant : `OneOf` énumère des valeurs exactes, et en rendre une qui -n'a jamais été dans le pool est une surprise pire que celle qu'on supprime. Elle détruit aussi l'instant, puisque -déplacer le décalage en conservant l'heure locale donne un autre point dans le temps. - -### Rendre `OneOf` terminal sur `AnyDateTimeOffset` - -Envisagée parce qu'un type terminal rendrait la combinaison inécrivable, ce qui est la garantie la plus forte -possible. Rejetée parce qu'elle supprime des combinaisons légitimes et utiles — `OneOf(...).Except(...)`, et un -décalage que certaines valeurs du pool portent bel et bien — et parce que la question plus large des pools terminaux -se règle pour elle-même du côté des pools de chaînes et d'objets, plutôt que famille par famille. - -## Conséquences - -### Positives - -* `WithOffset` et `WithOffsetBetween` veulent dire la même chose quel que soit ce que le générateur porte par - ailleurs. -* Une combinaison pool/décalage impossible est signalée à la déclaration, avec un message nommant ce qui a été - demandé et ce que le pool admet. -* `AnyDateTimeOffset` cesse d'être la seule famille où une contrainte déclarée peut disparaître. - -### Négatives - -* Un appelant qui s'appuyait sur l'ancien silence — écrire `WithOffset` après un pool en attendant que le pool - l'emporte — obtient désormais soit un pool filtré, soit un conflit. Ce comportement était contredit par la - documentation de la méthode elle-même, donc le changement corrige le code plutôt que l'attente, mais c'est un - changement de comportement dans un générateur déjà livré. - -### Risques - -* **Un pool d'une seule valeur au décalage discordant échoue là où il générait.** C'est la correction voulue, et le - message nomme les deux côtés pour que la solution soit évidente — retirer la contrainte de décalage, ou mettre au - pool une valeur qui le porte. Atténuation : le message énonce ce que la dimension de décalage admet. - -## Actions de suivi - -* Passer l'ADR-0037 au statut *Superseded* avec un lien vers celle-ci. -* Garder en phase la section décalage du readme de JustDummies : elle documente `WithOffset` et `WithOffsetBetween` - sans mentionner `OneOf`, ce qui n'était exact que sous l'ancien comportement. - -## Références - -* ADR-0037 — Faire varier la dimension d'offset de DateTimeOffset : la décision que celle-ci supersède, et l'entrée - *Risques* qu'elle referme. -* ADR-0030 — Tirer des chaînes arbitraires d'un ensemble terminal explicite : la sémantique de pool terminal sur - laquelle l'ADR-0037 s'appuyait. -* `AnyDateTimeOffset` dans le projet `JustDummies`. diff --git a/doc/handwritten/for-maintainers/adr/0051-filter-the-datetimeoffset-pool-by-the-declared-offset.md b/doc/handwritten/for-maintainers/adr/0051-filter-the-datetimeoffset-pool-by-the-declared-offset.md deleted file mode 100644 index dc8962c0..00000000 --- a/doc/handwritten/for-maintainers/adr/0051-filter-the-datetimeoffset-pool-by-the-declared-offset.md +++ /dev/null @@ -1,109 +0,0 @@ -# ADR-0051 | Filter the DateTimeOffset pool by the declared offset - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0051-filter-the-datetimeoffset-pool-by-the-declared-offset.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-28 -**Accepted:** 2026-07-28 -**Decision Makers:** Reefact - -Supersedes [ADR-0037](0037-vary-the-datetimeoffset-offset-dimension.md). - -## Context - -ADR-0037 gave `AnyDateTimeOffset` an offset dimension and recorded, under *Risks*, that combining it with `OneOf` -would leave the offset unapplied: "`WithOffset` combined with `OneOf` does not replace a `OneOf` value's own offset. -Mitigation: documented, and consistent with `OneOf`'s terminal enumeration semantics." - -The mitigation did not hold, and the risk is larger than a surprise. - -`WithOffset`'s public XML documentation states that it "pins the offset dimension — **every generated value carries -exactly that offset**" and declares `ConflictingAnyConstraintException` "when the constraint contradicts a constraint -already declared". Combined with `OneOf`, it did neither: the constraint was dropped in both declaration orders, no -exception was raised, and the values came out with their own offsets. The published contract said the opposite of -what the code did, and the JustDummies readme does not mention the interaction at all. - -The library answers this shape consistently everywhere else. `Any.Int32().OneOf(1, 2, 3).GreaterThan(10)` and -`Any.DateTime().OneOf(d1, d2).After(2022)` both raise `ConflictingAnyConstraintException`; `OneOf(1, 2, 3) -.GreaterThan(1)` narrows and draws. `DateTimeOffset` was the one family where a constraint declared after a pool was -neither applied nor refused. - -The repository's governing rule for a fluent constraint is that a method the DSL offers must be honoured when its -arguments permit and must fail when they do not. Silently discarding it is neither. - -## Decision - -A declared offset **filters** the `OneOf` pool to the values whose offset it admits, in either declaration order, and -contradicts when it admits none. - -## Rationale - -* **It restores the published contract.** `WithOffset` promises that every generated value carries that offset, and - now every generated value does — including a pooled one, because a pooled value carrying a different offset is - simply not drawn. -* **It keeps the half of ADR-0037 that was right.** A pooled value is still returned verbatim, offset included: - rebuilding it from the instant would normalize the offset to UTC, which is exactly what ADR-0037 set out to avoid. - What changes is *which* pooled values may be drawn, not how a drawn one is rendered. -* **It makes the two orders agree.** Declaring the pool first or the offset first now reaches the same verdict, which - is the property the library already guarantees for every other constraint pair and which a caller has no way to - reason about otherwise. -* **A contradiction is reported rather than swallowed.** An offset no pooled value carries is a specification the - generator cannot satisfy; failing at declaration is what the eager check exists for, and it is what the - documentation already told the caller to expect. - -## Alternatives Considered - -### Keep the behaviour and fix the documentation instead - -Considered because it is the cheapest resolution and because ADR-0037 had already reasoned its way to it. Rejected -because the documentation would then have to describe a rule that holds for one generator family and no other, and -because the caller writing `WithOffset` after a pool is asking for something the library can decide: either a pooled -value carries that offset or none does. Documenting a silent no-op does not make it a good answer, and the divergence -would be discovered at the point where a test passes for the wrong reason. - -### Rewrite the pooled value's offset to the declared one - -Considered because it honours `WithOffset` literally in every case, with no contradiction to report. Rejected because -it changes the value the caller supplied: `OneOf` enumerates exact values, and returning one that was never in the -pool is a worse surprise than the one being removed. It also destroys the instant, since moving the offset while -keeping the local time yields a different point in time. - -### Make `OneOf` terminal on `AnyDateTimeOffset` - -Considered because a terminal type would make the combination unwritable, which is the strongest possible guarantee. -Rejected because it removes combinations that are legitimate and useful — `OneOf(...).Except(...)`, and an offset that -some pooled value does carry — and because the wider question of terminal pools is being settled on its own terms for -the string and object pools rather than family by family. - -## Consequences - -### Positive - -* `WithOffset` and `WithOffsetBetween` mean the same thing whatever else the generator carries. -* An impossible pool/offset combination is reported at declaration, with a message naming what was asked and what the - pool admits. -* `AnyDateTimeOffset` stops being the one family where a declared constraint can vanish. - -### Negative - -* A caller who relied on the old silence — writing `WithOffset` after a pool and expecting the pool to win — now gets - either a filtered pool or a conflict. That behaviour was contradicted by the method's own documentation, so the - change corrects the code rather than the expectation, but it is a behaviour change in a shipped generator. - -### Risks - -* **A pool of one value with a mismatched offset now fails where it used to generate.** That is the intended - correction, and the message names both sides so the fix is obvious — drop the offset constraint, or pool a value - that carries it. Mitigation: the message states what the offset dimension admits. - -## Follow-up Actions - -* Flip ADR-0037's status to *Superseded* with a link here. -* Keep the JustDummies readme's offset section in step: it documents `WithOffset` and `WithOffsetBetween` without - mentioning `OneOf`, which was correct only under the old behaviour. - -## References - -* ADR-0037 — Vary the DateTimeOffset offset dimension: the decision this supersedes, and the *Risks* entry it closes. -* ADR-0030 — Draw arbitrary strings from an explicit terminal set: the terminal-pool semantics ADR-0037 leaned on. -* `AnyDateTimeOffset` in the `JustDummies` project. diff --git a/doc/handwritten/for-maintainers/adr/0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.fr.md b/doc/handwritten/for-maintainers/adr/0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.fr.md deleted file mode 100644 index 257e5e82..00000000 --- a/doc/handwritten/for-maintainers/adr/0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.fr.md +++ /dev/null @@ -1,168 +0,0 @@ -# ADR-0052 | Tirer les nombres arbitraires dans une magnitude ordinaire - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.md) - -**Statut :** Accepté -**Proposé :** 2026-07-28 -**Accepté :** 2026-07-28 -**Décideurs :** Reefact - -## Contexte - -Les générateurs flottants et décimaux échantillonnent uniformément entre leurs bornes. Non contraintes, -ces bornes sont le domaine entier du type — pour `double`, une plage couvrant quelque 616 décades. - -Un tirage uniforme sur une telle plage est uniforme *par valeur*, pas par magnitude, et il y a autant de -place entre 1e307 et 1e308 qu'entre 0 et 1e307. Toute la masse de probabilité se situe donc à quelques -décades du maximum du type. Mesuré sur 5 000 tirages : - -| mesure | résultat | -| --------------------------------------------------- | ------------ | -| `Any.Double()` — `\|v\| < 1e6` | 0 / 5000 | -| `Any.Single()` — `\|v\| < 1e34` | 0 / 5000 | -| `Any.Decimal()` — `\|v\| < 1e24` | 0 / 5000 | -| `Any.Double().Positive()` × 1,2 → `Infinity` | 16,1 % | -| `Any.Decimal()` × 1,2m → `OverflowException` | 17,1 % | -| `x + 1 == x` sur un tirage `Positive()` | vrai | - -À ces magnitudes, un type flottant cesse de se comporter comme de l'arithmétique : une multiplication -supplémentaire déborde — en `Infinity` pour les types binaires, contagieux et produisant des `NaN` en -aval, et en `OverflowException` levée pour `decimal`. La précision est épuisée, d'où `x + 1 == x`. Une -contrainte d'échelle n'a plus de chiffre décimal sur lequel agir : `Any.Decimal().WithScale(2)` était -satisfaite par 5 000 tirages sur 5 000, tous des entiers à 29 chiffres — vraie et vide à la fois. - -Les magnitudes où s'exécute le code ordinaire, et où vivent les défauts d'arrondi, de comparaison et de -formatage, ne sont jamais visitées. - -Les générateurs entiers partagent la même distribution — `Any.Int32()` tire sous 1e6 dans 0,06 % des cas, -`Any.Int64()` dans 0 cas sur 5 000 — mais pas la même conséquence : un grand entier reste un entier -ordinaire, l'arithmétique entière C# wrappe silencieusement au lieu de saturer ou de lever, `x + 1 != x` -tient toujours, et un débordement d'entier dans le code testé est fréquemment un vrai défaut. Les builders -entiers reposent en outre sur le moteur ordinal partagé, dont quatre familles de builders dépendent. - -`Half` s'arrête à 65 504 : son domaine entier se situe déjà dans les magnitudes ordinaires. - -L'ADR-0050 a consigné la règle homologue pour les *tailles* : un dummy est petit à moins que quelque chose -ne demande explicitement plus, un maximum étant une permission et non une demande. Il a délibérément borné -sa portée aux tailles et laissé les valeurs ouvertes. JustDummies n'a jamais été publié, donc le sens du -tirage non contraint est encore libre d'être fixé — l'acquis sur lequel s'appuyait l'ADR-0041. - -## Décision - -Une valeur flottante ou décimale arbitraire est tirée dans une magnitude ordinaire d'un million, cette -fenêtre rognant l'intervalle déclaré et s'effaçant seulement là où elle le laisserait vide, tandis que les -générateurs entiers conservent la plage entière de leur type. - -## Justification - -* **Un dummy qui casse le test qu'il décore a échoué à sa seule mission.** La bibliothèque existe pour - fournir une valeur dont le test ne se soucie pas du contenu. Une valeur qui fait déborder une - multiplication sans rapport une fois sur six n'est pas cela : elle fait de la *fixture* la cause de - l'échec, et le diagnostic coûte bien plus cher que ce que la valeur a fait gagner. C'est tout - l'argument, et les mesures ci-dessus en sont la preuve. -* **Rogner plutôt que remplacer est ce qui garde la règle honnête.** Un appelant qui nomme une magnitude — - un intervalle situé au-delà de la fenêtre — l'obtient exactement, parce que la fenêtre s'efface là où - elle ne laisserait rien. Un appelant qui *permet* seulement une magnitude continue de tirer des valeurs - ordinaires, parce que permettre n'est pas demander. La fenêtre ne brise donc jamais une borne déclarée ; - elle refuse seulement de la viser. C'est la règle de l'ADR-0050 pour les tailles, transposée telle - quelle aux valeurs, de sorte que la bibliothèque énonce un principe et non deux. -* **Elle redonne du sens aux contraintes bâties par-dessus.** Une contrainte d'échelle que tout tirage - satisfait et qu'aucun n'exerce est pire qu'absente : elle se lit comme de la couverture dans un test qui - n'en a pas. Les magnitudes ordinaires rendent à un `decimal` ses chiffres décimaux, donc `WithScale` - contraint de nouveau. -* **Un million se place là où un dummy est quelconque.** Assez grand pour ressembler à une vraie quantité - et exercer le formatage multi-chiffres, assez petit pour que toute arithmétique plausible reste à des - centaines de décades du débordement, et il laisse à un `double` environ neuf chiffres significatifs sous - la virgule. Un type déjà à l'intérieur n'est pas touché, et c'est pourquoi `Half` ne demande aucun cas - particulier : une règle qui rétrécit l'extravagant et se tait ailleurs est une règle, pas une liste - d'exceptions. -* **Les générateurs entiers sont exclus sur preuve, pas par commodité.** Leur distribution est la même, - leur conséquence non : rien dans les mesures ne montre l'arithmétique entière se casser, et le - débordement qu'un grand entier peut provoquer en aval est souvent le défaut qu'un test doit révéler - plutôt qu'un bruit qu'il doit éviter. Y étendre la règle atteindrait de surcroît le moteur ordinal - partagé dont dépendent quatre familles de builders, pour un dommage non démontré. - -## Alternatives considérées - -### Échantillonner log-uniformément sur tout le domaine - -Considérée comme l'option gardant toute magnitude atteignable tout en rendant les extrêmes rares, plus -proche de « n'importe quelle valeur du type » qu'une fenêtre bornée. Rejetée sur ses chiffres : elle -corrigerait le débordement (la décade supérieure tombe à environ 0,01 % des tirages) mais ne toucherait -presque pas le second défaut — la fenêtre ordinaire de 1 à 1e6 fait 6 décades sur 616, donc elle serait -visitée environ 1 % du temps — tout en en introduisant un troisième, puisque la moitié des tirages -tomberait sous 1e0 avec une longue traîne vers 1e-200. Des valeurs aussi petites cassent une autre classe -de code — divisions, comparaisons à epsilon, accumulations qui absorbent le terme — donc l'échange revient -à troquer une pathologie contre deux. - -### Mélanger valeurs ordinaires et valeurs remarquables - -Considérée parce que tirer majoritairement des valeurs ordinaires avec un 0, un ±1 ou un extrême du -domaine de temps en temps donnerait de la couverture de bord gratuite. Rejetée parce qu'elle rend le dummy -*remarquable* : un tirage sur dix ferait de la fixture le sujet du test, et une suite qui échoue une fois -sur dix pour une raison que le test n'a jamais nommée est exactement le mode de défaillance que cette -décision existe pour supprimer. Un test qui veut un extrême doit le nommer. - -### Étendre la règle aux générateurs entiers - -Considérée par cohérence, puisque les exclure laisse la bibliothèque avec deux politiques par défaut pour -des nombres. Rejetée pour cette décision sur la preuve ci-dessus — même distribution, conséquence -matériellement plus douce — et sur le rayon d'explosion, le moteur ordinal étant partagé par quatre -familles de builders. L'asymétrie est acceptée sciemment et consignée ici plutôt que laissée à découvrir, -et elle est un candidat légitime pour un ADR ultérieur si le cas entier venait à mordre. - -### Ajouter un opt-in explicite pour les valeurs extrêmes - -Considérée parce que le comportement actuel stresse les débordements par accident, et que borner le défaut -y met fin. Rejetée comme API inutile : la capacité existe déjà et se lit mieux qu'un opt-in — un -intervalle nommant la magnitude est honoré exactement. Une couverture qui se déclenche 16 % du temps dans -des tests qui parlent d'autre chose est un coût plutôt qu'un bénéfice, et rendre ce test explicite est un -gain sur ce que la suite dit d'elle-même. - -## Conséquences - -### Positives - -* L'arithmétique ordinaire sur un tirage non contraint reste finie et ne lève pas, sur tous les types - continus. -* Les valeurs générées occupent enfin les magnitudes où vivent les défauts d'arrondi, de comparaison et de - formatage. -* Les contraintes posées sur la valeur — l'échelle avant tout — contraignent de nouveau quelque chose. -* La bibliothèque énonce un principe unique pour les tailles et les valeurs : un dummy est quelconque à - moins que quelque chose ne demande explicitement le contraire. - -### Négatives - -* Un appelant ayant déclaré un intervalle large ne reçoit plus de valeurs réparties dessus : - `Between(0, double.MaxValue)` rend des valeurs ordinaires. C'est la lecture voulue — la borne est - honorée, pas visée — mais elle surprendra quiconque lisait une borne large comme une demande, et la - documentation doit l'énoncer plutôt que de la laisser deviner. -* La couverture accidentelle des débordements que fournissait l'ancien défaut disparaît. Il faut la - demander explicitement, ce qui est un gain d'intention et une perte pour qui s'y appuyait sans le - savoir. -* La bibliothèque porte deux politiques par défaut pour des nombres : types continus bornés, entiers - pleine plage. - -### Risques - -* Un million est une constante défendable, pas dérivée. L'argument repose sur l'écart entre les magnitudes - qu'emploie le code ordinaire et celles où les types se comportent mal, pas sur une mesure d'un - consommateur particulier. -* Un consommateur dont le domaine vit légitimement au-dessus de la fenêtre — quantités astronomiques ou - cryptographiques — doit nommer son intervalle. Atténué par le fait que la fenêtre s'efface précisément - dans ce cas. - -## Actions de suivi - -* Énoncer la règle dans la documentation du package, là où la surface de contraintes est décrite. -* Réexaminer l'exclusion des entiers si un consommateur signale la même classe de dommage. - -## Références - -* ADR-0050 — Let a size maximum cap without steering the draw : le même principe appliqué aux tailles, - dont cette décision reprend délibérément le vocabulaire (« une borne est une permission, pas une - demande »). -* ADR-0041 — Draw flag-enum combinations behind an opt-in : l'acquis selon lequel une bibliothèque non - publiée peut encore fixer le sens d'un tirage non contraint. -* ADR-0040 — Split the JustDummies test bed between example and property suites : pourquoi la règle de la - fenêtre est quantifiée en propriétés tandis que les extrêmes mesurés restent des exemples. diff --git a/doc/handwritten/for-maintainers/adr/0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.md b/doc/handwritten/for-maintainers/adr/0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.md deleted file mode 100644 index 1ff5ae59..00000000 --- a/doc/handwritten/for-maintainers/adr/0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.md +++ /dev/null @@ -1,159 +0,0 @@ -# ADR-0052 | Draw arbitrary numbers within an ordinary magnitude - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-28 -**Accepted:** 2026-07-28 -**Decision Makers:** Reefact - -## Context - -The floating-point and decimal generators sample uniformly between their bounds. Unconstrained, those -bounds are the type's whole domain — for `double`, a range spanning some 616 decades. - -A uniform draw over such a range is uniform *by value*, not by magnitude, and there is as much room -between 1e307 and 1e308 as between 0 and 1e307. Essentially all the probability mass therefore sits within -a few decades of the type's maximum. Measured on 5 000 draws: - -| measurement | result | -| --------------------------------------------------- | ------------ | -| `Any.Double()` — `\|v\| < 1e6` | 0 / 5000 | -| `Any.Single()` — `\|v\| < 1e34` | 0 / 5000 | -| `Any.Decimal()` — `\|v\| < 1e24` | 0 / 5000 | -| `Any.Double().Positive()` × 1.2 → `Infinity` | 16.1 % | -| `Any.Decimal()` × 1.2m → `OverflowException` | 17.1 % | -| `x + 1 == x` on a `Positive()` draw | true | - -At those magnitudes a floating-point type stops behaving like arithmetic: a further multiplication -overflows — to `Infinity` for the binary types, which is contagious and yields `NaN` downstream, and to a -thrown `OverflowException` for `decimal`. Precision is exhausted, so `x + 1 == x`. A scale constraint has -no fractional digits left to act on: `Any.Decimal().WithScale(2)` was satisfied by 5 000 draws out of -5 000, every one of them a 29-digit integer — true and empty at once. - -The magnitudes where ordinary code runs, and where rounding, comparison and formatting defects live, are -never visited. - -The integer generators share the same distribution — `Any.Int32()` draws below 1e6 in 0.06 % of cases, -`Any.Int64()` in 0 of 5 000 — but not the same consequence: a large integer is an ordinary integer, C# -integer arithmetic wraps silently rather than saturating or throwing, `x + 1 != x` always holds, and an -integer overflow in the code under test is frequently a genuine defect. The integer builders also ride the -shared ordinal engine, which four builder families depend on. - -`Half` stops at 65 504, so its entire domain already lies within ordinary magnitudes. - -ADR-0050 recorded the counterpart rule for *sizes*: a dummy is small unless something explicitly asks for -more, a maximum being a permission rather than a request. It deliberately scoped itself to sizes and left -values open. JustDummies has never been released, so the meaning of the unconstrained draw is still free -to be fixed — the standing ADR-0041 relied on. - -## Decision - -An arbitrary floating-point or decimal value is drawn from within an ordinary magnitude of one million, -that window clipping the declared interval and stepping aside only where it would leave that interval -empty, while the integer generators keep the full range of their type. - -## Rationale - -* **A dummy that breaks the test it decorates has failed at its one job.** The library's purpose is to - supply a value whose content the test does not care about. A value that makes an unrelated - multiplication overflow one time in six is not that: it turns the *fixture* into the cause of the - failure, and the diagnosis costs far more than the value saved anybody. This is the whole argument, and - the measurements above are its evidence. -* **Clipping, rather than replacing, is what keeps the rule honest.** A caller who names a magnitude — - an interval lying beyond the window — gets exactly it, because the window steps aside when it would - leave nothing. A caller who merely *permits* a magnitude keeps drawing ordinary values, because - permitting is not requesting. The window therefore never breaks a declared bound; it only declines to - target one. That is ADR-0050's rule for sizes, transplanted unchanged to values, so the library states - one principle rather than two. -* **It restores meaning to the constraints built on top.** A scale constraint that every draw satisfies - and none exercises is worse than an absent one: it reads as coverage in a test that has none. Ordinary - magnitudes give a decimal its fractional digits back, so `WithScale` constrains again. -* **One million sits where a dummy is unremarkable.** It is large enough to look like a real quantity and - to exercise multi-digit formatting, small enough that any plausible further arithmetic stays hundreds of - decades from overflow, and it leaves a `double` around nine significant digits below the decimal point. - A type already inside it is untouched, which is why `Half` needs no special case: a rule that narrows - the extravagant and is silent elsewhere is a rule, not a list of exceptions. -* **The integer generators are excluded on evidence, not on convenience.** Their distribution is the same, - their consequence is not: nothing in the measurements shows integer arithmetic breaking down, and the - overflow a large integer may provoke downstream is often the defect a test should surface rather than - noise it should avoid. Extending the rule there would also reach the shared ordinal engine that four - builder families depend on, for a harm not demonstrated. - -## Alternatives Considered - -### Sample log-uniformly over the full domain - -Considered as the option that keeps every magnitude reachable while making the extremes rare, which is -closer to "any value of the type" than a bounded window. Rejected on its numbers: it would fix the -overflow (the top decade shrinks to about 0.01 % of draws) but barely touch the second defect — the -ordinary window of 1 to 1e6 is 6 decades out of 616, so it would still be visited about 1 % of the time — -while introducing a third, since half of all draws would fall below 1e0 with a long tail toward 1e-200. -Values that small break a different class of code — divisions, epsilon comparisons, accumulations that -absorb the term — so the trade is one pathology for two. - -### Mix ordinary values with notable ones - -Considered because drawing mostly ordinary values with an occasional 0, ±1 or domain extreme would give -edge-case coverage for free. Rejected because it makes the dummy *remarkable*: one draw in ten would turn -the fixture into the subject of the test, and a suite that fails once in ten runs for a reason the test -never named is the exact failure mode this decision exists to remove. A test that wants an extreme should -name it. - -### Extend the rule to the integer generators - -Considered for consistency, since leaving them out gives the library two default policies for numbers. -Rejected for this decision on the evidence above — same distribution, materially milder consequence — and -on blast radius, the ordinal engine being shared by four builder families. The asymmetry is accepted -knowingly and recorded here rather than left to be discovered, and it is a legitimate candidate for a -later ADR should the integer case turn out to bite. - -### Add an explicit opt-in for extreme values - -Considered because the current behaviour stress-tests overflow by accident, and bounding the default -stops that. Rejected as unnecessary API: the capability already exists and reads better than an opt-in -would — an interval naming the magnitude is honoured exactly. Coverage that fires 16 % of the time inside -tests about something else is a cost rather than a benefit, and making that test explicit is a gain in -what the suite says about itself. - -## Consequences - -### Positive - -* Ordinary arithmetic on an unconstrained draw stays finite and does not throw, on every continuous type. -* Generated values finally occupy the magnitudes where rounding, comparison and formatting defects live. -* Constraints layered on the value — scale above all — constrain something again. -* The library states one principle across sizes and values: a dummy is unremarkable unless something - explicitly asks otherwise. - -### Negative - -* A caller who declared a wide interval no longer receives values spread across it: `Between(0, - double.MaxValue)` yields ordinary values. This is the intended reading — the bound is honoured, not - targeted — but it will surprise anyone who read a wide bound as a request, and the documentation has to - say so rather than let it be inferred. -* The accidental overflow coverage the old default provided is gone. It has to be asked for explicitly, - which is an improvement in intent and a loss for anyone who was relying on it without knowing. -* The library carries two default policies for numbers: continuous types bounded, integers full-range. - -### Risks - -* One million is a defensible constant, not a derived one. The argument rests on the gap between the - magnitudes ordinary code uses and those where the types misbehave, not on a measurement of any - particular consumer. -* A consumer whose domain legitimately lives above the window — astronomical or cryptographic - quantities — must name its interval. Mitigated by the window stepping aside for exactly that case. - -## Follow-up Actions - -* State the rule in the package documentation, where the constraint surface is described. -* Revisit the integer exclusion if a consumer reports the same class of harm there. - -## References - -* ADR-0050 — Let a size maximum cap without steering the draw: the same principle applied to sizes, whose - vocabulary ("a bound is a permission, not a request") this decision reuses deliberately. -* ADR-0041 — Draw flag-enum combinations behind an opt-in: the standing that an unreleased library may - still fix the meaning of an unconstrained draw. -* ADR-0040 — Split the JustDummies test bed between example and property suites: why the window's rule is - quantified as properties while the measured extremes stay as examples. diff --git a/doc/handwritten/for-maintainers/adr/0053-unify-discrete-generation-in-one-ordinal-space.fr.md b/doc/handwritten/for-maintainers/adr/0053-unify-discrete-generation-in-one-ordinal-space.fr.md deleted file mode 100644 index ddeae8f9..00000000 --- a/doc/handwritten/for-maintainers/adr/0053-unify-discrete-generation-in-one-ordinal-space.fr.md +++ /dev/null @@ -1,221 +0,0 @@ -# ADR-0053 | Unifier la génération discrète dans un espace ordinal unique, avec un moteur dédié seulement là où le substrat arithmétique l'impose - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0053-unify-discrete-generation-in-one-ordinal-space.md) - -**Statut :** Accepté -**Proposé :** 2026-07-28 -**Accepté :** 2026-07-28 -**Décideurs :** Reefact - -## Contexte - -JustDummies expose la même algèbre de contraintes en forme d'intervalle sur un large ensemble de types -valeur : les huit entiers de largeur fixe, `char`, `TimeSpan`, `DateTime`, `DateTimeOffset`, `DateOnly`, -`TimeOnly`, les trois types à virgule flottante binaire, `decimal` et les deux entiers 128 bits. Sur tous, -un test peut déclarer des bornes, une liste blanche, des exclusions et — là où le type a un pas naturel — -un réseau : un multiple, une granularité temporelle ou une échelle décimale. - -Deux promesses valables pour toute la bibliothèque contraignent la façon d'implémenter cette algèbre. Les -valeurs sont **construites pour satisfaire** les contraintes déclarées plutôt que tirées puis filtrées : -un générateur qui existe doit produire une valeur en un seul tirage, sans boucle de reprise. Et des -contraintes qui se contredisent doivent échouer à la déclaration avec un message nommant **les deux** -côtés, ce qui exige que chaque borne porte la contrainte qui l'a posée, et non un simple nombre. - -Les types se divisent selon leur substrat arithmétique, non selon leur nature : - -* Tout type discret dont le domaine tient sur 64 bits — les entiers, les types temporels fondés sur les - ticks, les numéros de jour et d'heure du jour, `char` — admet une projection **préservant l'ordre** vers - l'intervalle des entiers non signés 64 bits. Bornes, exclusions, pas, cardinalité et échantillonnage - deviennent alors un seul et même problème pour tous, énoncé une fois sur les ordinaux. -* `Int128` et `UInt128` ont des domaines qui dépassent 64 bits : aucune telle projection vers un ordinal - 64 bits n'existe. -* La virgule flottante binaire IEEE est continue. Ses motifs de bits sont monotones et pourraient être - projetés, mais un tirage uniforme sur les motifs de bits n'est pas un tirage uniforme sur les valeurs — - environ la moitié des `double` se situent dans `[-1, 1]`. Exclure un point d'un continuum diffère aussi - en nature d'une exclusion dans un ensemble fini : la collision est de mesure nulle, et la contrainte doit - pourtant être honorée exactement. -* `decimal` est une mantisse de 96 bits assortie d'une échelle, et il n'a pas d'échelle des valeurs - représentables successives : une borne exclusive ne peut donc pas s'exprimer en passant à la valeur - adjacente, comme c'est le cas pour les entiers et pour les flottants. - -La cible plancher est netstandard2.0 (l'ADR-0022 fixe le plancher .NET Framework sur lequel la bibliothèque -doit continuer de se charger). Elle n'offre aucune abstraction d'arithmétique générique sur les types -numériques, ni aucun entier 128 bits, si bien que l'arithmétique ne peut pas être écrite une fois contre un -paramètre de type numérique dans du code qui doit compiler sur le plancher. C# interdit par ailleurs le -motif de classe de base générique auto-référentielle pour les générateurs publics scellés que cette API -expose. - -La duplication qui en résulte est réelle et a été mesurée par l'audit d'architecture du 20/07/2026 : les -quatorze générateurs numériques sont des clones quasi identiques à substitution de type près — environ -2 450 lignes — et les cinq générateurs temporels suivent le même motif sur quelque 800 lignes de plus. Un -balayage scripté de ces familles de clones n'a trouvé aucun écart de comportement par copier-coller, et -l'issue #214 a depuis ajouté des garde-fous de parité par réflexion, sur les points d'entrée miroir comme -sur le jeu de méthodes de contrainte de chaque famille. - -Cet agencement est la décision qui façonne le plus les entrailles de la bibliothèque, et il contraint la -façon dont tout futur générateur discret ou numérique sera ajouté. Son raisonnement ne vit que dans la -documentation XML interne, alors que des décisions plus petites — le plafond d'arité d'`Any.Combine` -(ADR-0015) — portent des enregistrements. - -## Décision - -Tout type valeur discret dont le domaine tient sur 64 bits est généré par un moteur partagé unique opérant -sur un espace ordinal commun d'entiers non signés 64 bits, et un moteur distinct n'existe que là où le -substrat arithmétique ne peut pas y être représenté : les entiers 128 bits, la virgule flottante binaire -IEEE et `decimal`. - -## Justification - -* **L'espace ordinal est ce qui permet d'énoncer les promesses difficiles une seule fois.** La - satisfiabilité immédiate, l'exclusion exacte en un tirage, le comptage de cardinalité et la détection de - conflit sont les parties de l'algèbre qu'il est facile de rater subtilement, et coûteux de rater en plus - d'un endroit. Sur les ordinaux, elles forment un seul problème avec une seule implémentation, et tout - type discret 64 bits hérite des mêmes garanties par construction plutôt que par relecture. Une borne de - `DateTime` et une borne d'`Int64` sont alors le même objet : la promesse de nommer les deux côtés d'un - conflit n'a pas à être regagnée type par type. -* **La séparation suit le substrat, ce qui la rend réfutable plutôt qu'affaire de goût.** Chaque moteur - dédié existe parce qu'une propriété énoncée de son arithmétique — largeur au-delà de 64 bits, continuité, - absence d'échelle des valeurs représentables — rend inapplicable la formulation du moteur partagé, et non - parce que son type « semblait » différent. La règle se lit comme un test qu'un futur mainteneur peut - appliquer : un nouveau type reçoit une projection si son domaine tient dans l'espace ordinal, et un - moteur seulement s'il est démontrable qu'il n'y tient pas. -* **Un tirage uniforme doit être uniforme sur les valeurs, pas sur les représentations.** C'est pourquoi - les motifs de bits monotones de la virgule flottante ne sont pas forcés dans l'espace ordinal, bien que - la projection existe. L'uniformité ordinale est exactement juste là où des ordinaux consécutifs - désignent des valeurs consécutives, et exactement fausse là où ce n'est pas le cas ; garder les types - continus sur leur propre moteur préserve le sens d'« arbitraire » pour les deux groupes. -* **La cible plancher supprime l'alternative générique : le choix se réduit à un moteur partagé plus trois - exceptions, ou à aucun partage du tout.** Sans arithmétique générique sur netstandard2.0, partager - l'arithmétique entre types numériques exige soit une indirection ordinale, soit du code par type. - L'espace ordinal achète le partage pour le plus grand groupe — treize types — en n'utilisant que - l'arithmétique entière fournie par le plancher, et le paie aux trois endroits où il ne peut réellement - pas s'appliquer. -* **La duplication acceptée est bornée et désormais gardée.** Le coût de cette décision est que les - parties de l'algèbre non partageables sont énoncées jusqu'à quatre fois. Ce coût a été accepté en - connaissance de cause : le balayage de l'audit n'a trouvé aucune dérive de comportement, et les - garde-fous de parité de l'issue #214 font passer « les clones s'accordent » d'une discipline à un test - qui échoue. Une décision dont le principal inconvénient est surveillé par un test n'est pas dans la même - position qu'une décision dont l'inconvénient est surveillé par l'attention. - -## Alternatives considérées - -### Un moteur unique sur un espace ordinal élargi - -Considérée comme la version de cette conception sans exception : tout projeter dans un ordinal 128 bits, ou -de précision arbitraire, et garder un moteur unique pour toute la surface numérique et discrète. - -Rejetée d'abord au titre de la cible plancher — netstandard2.0 n'a aucun type entier 128 bits, si bien que -le moteur partagé ne pourrait pas compiler là où la bibliothèque doit se charger, et un substitut à -précision arbitraire placerait un type numérique allouant sur chaque tirage des treize types qui n'en ont -aucun besoin. Elle n'atteindrait pas non plus ce qu'elle promet : élargir l'ordinal ne traite que le -problème de largeur, laissant l'uniformité en virgule flottante et l'échelle manquante de `decimal` -exactement en l'état. Le moteur unifié aurait toujours besoin de branches par substrat, ayant perdu la -propriété qui rendait l'unification intéressante. - -### Un moteur par type, sans partage - -Considérée pour sa simplicité : chaque générateur possède ses bornes, ses exclusions et son échantillonnage, -sans indirection à comprendre ni notion d'ordinal à apprendre. - -Rejetée parce qu'elle multiplie les parties difficiles de l'algèbre — satisfiabilité immédiate, projection -des exclusions, provenance des conflits — par le nombre de types plutôt que par le nombre de substrats. Les -familles de clones mesurées par l'audit montrent ce que cela coûte même là où le code est produit par -discipline : la duplication qui subsiste sous cette décision est la part non partageable, et un moteur par -type dupliquerait aussi la part partageable. - -### Une classe de base numérique générique sur un paramètre de type auto-référentiel - -Considérée comme la voie offerte par le langage pour abstraire l'arithmétique sans indirection ordinale, ce -qui donnerait une implémentation unique tout en préservant l'arithmétique native de chaque type. - -Rejetée comme indisponible plutôt qu'indésirable : C# interdit ce motif pour les types de générateurs -publics scellés que cette API expose, et netstandard2.0 ne fournit aucune contrainte d'arithmétique -générique à travers laquelle l'arithmétique pourrait s'exprimer — la classe de base n'aurait donc rien à -abstraire. L'issue #214 a enregistré les tests de parité par réflexion comme mitigation de la duplication -que cette alternative devait supprimer. - -### Projeter la virgule flottante par ses motifs de bits - -Considérée parce que les formats binaires IEEE s'ordonnent de façon monotone en tant qu'entiers, ce qui -rend la projection disponible et ferait entrer trois types de plus dans le moteur partagé. - -Rejetée parce qu'elle change silencieusement ce que signifie une valeur arbitraire. L'uniformité sur les -ordinaux devient une uniformité sur les représentations, ce qui, en virgule flottante, concentre le tirage -près de zéro ; et l'exclusion ponctuelle sur un continuum, qui doit être honorée exactement sur un ensemble -de mesure nulle, est un problème différent de l'exclusion sur un ensemble ordinal fini. La projection est -possible, la sémantique ne l'est pas, et cette équivalence est la seule raison de partager un moteur. - -### Projeter `decimal` par sa mantisse et son échelle - -Considérée pour la même raison : `decimal` est discret, une injection dans un espace ordinal paraît donc -naturelle, et il ne resterait que deux moteurs dédiés. - -Rejetée parce que la discrétion de `decimal` n'est pas uniforme. Une même valeur possède plusieurs -représentations à des échelles différentes, et la distance entre valeurs représentables adjacentes dépend de -l'échelle : une projection préservant l'ordre vers un intervalle ordinal contigu n'existe donc pas sans -fixer d'abord une échelle — laquelle est une contrainte que l'appelant peut déclarer ou non. Les bornes et -les pas de `decimal` doivent par conséquent s'exprimer dans sa propre arithmétique. - -## Conséquences - -### Positives - -* La partie difficile de l'algèbre discrète — satisfiabilité immédiate, exclusion exacte, cardinalité, - provenance des conflits — a une seule implémentation pour treize types : un correctif ou une nouvelle - contrainte atterrit une fois pour tous. -* Ajouter un type discret dont le domaine tient sur 64 bits, c'est une projection et un nom d'affichage, - pas un nouveau moteur. -* La frontière entre moteurs est énoncée comme une propriété du substrat : un futur mainteneur peut décider - où appartient un nouveau type sans rejuger l'architecture. -* Un tirage discret reste uniforme sur les valeurs de son type, et aucune projection dans l'espace des - représentations ne déforme un tirage continu. Quelle magnitude favorise un tirage continu non contraint - est une décision distincte, enregistrée dans l'ADR-0052 ; celle-ci ne règle que le fait que la - déformation ne vient jamais de la projection. - -### Négatives - -* Quatre moteurs signifient que les parties de l'algèbre qui *paraissent* partageables sont écrites jusqu'à - quatre fois, et qu'une évolution de l'algèbre peut devoir être appliquée dans chacune. Le moteur 128 bits - est délibérément un frère mot pour mot du moteur ordinal, ce qui est le cas le plus net de ce coût. -* Un lecteur doit apprendre l'indirection ordinale avant de suivre comment une borne de `DateTime` devient - un tirage ; le moteur partagé est agnostique du domaine par conception, donc rien en lui ne nomme les - types qu'il sert. -* La décision fixe la frontière des 64 bits comme seuil de partage. C'est l'arithmétique entière la plus - large que fournit la cible plancher, pas un optimum démontré. - -### Risques - -* Les familles de clones peuvent dériver en comportement à la prochaine édition de l'une d'elles. Mitigé - par les garde-fous de parité de l'issue #214, qui échouent sur une contrainte renommée ou manquante, et - borné par le fait que la dérive trouvée par l'audit portait sur la documentation, non sur le comportement. -* Un futur type pourrait tenir dans l'espace ordinal en principe alors que sa sémantique rend l'uniformité - ordinale fausse, comme c'est le cas de la virgule flottante. Le test de la décision est énoncé en termes - de représentabilité : un tel cas doit donc être reconnu sur ses propres mérites plutôt qu'en appliquant - la règle à la lettre. - -## Actions de suivi - -* Enregistrer séparément le contrat de déterminisme et de source ambiante (issue #216) ; c'est l'autre - décision transversale que l'audit a trouvée non enregistrée, et celle-ci ne la règle pas. -* À la prochaine divergence entre le moteur 128 bits et le moteur ordinal, préciser si elle est - intentionnelle ou s'il s'agit d'une dérive — cette décision attend qu'ils restent frères mot pour mot. - -## Références - -* ADR-0011 — Héberger JustDummies comme paquet autonome dans ce dépôt : la décision d'empaquetage à - l'intérieur de laquelle se place cette architecture interne. -* ADR-0013 — Verrouiller les collections distinctes par cardinalité, sinon par tirage borné : le - raisonnement de cardinalité que sert le comptage du moteur partagé. -* ADR-0015 — Plafonner `Any.Combine` à l'arité huit : la décision plus petite dont l'enregistrement - existant rendait l'absence de celle-ci frappante. -* ADR-0022 — Fixer le plancher de support .NET Framework de la bibliothèque à 4.7.2 : la cible plancher qui - supprime l'alternative de l'arithmétique générique. -* ADR-0025 — Générer des chaînes correspondantes depuis un sous-ensemble régulier maison : la décision - voisine de construire les valeurs constructivement plutôt que par filtrage. -* ADR-0052 — Tirer les nombres arbitraires dans une magnitude ordinaire : la décision voisine qui régit - ce que favorise un tirage continu non contraint, sur les deux moteurs que celle-ci garde séparés. -* Issue #217 — l'item de l'audit qui a demandé cet enregistrement. -* Issue #214 — les garde-fous de parité sur lesquels cette décision s'appuie pour garder sûre la - duplication qu'elle accepte. -* [Audit d'architecture et de conception JustDummies du 20/07/2026](../audit/2026-07-20-dummies-architecture-and-design-audit.fr.md), - §5 — là où l'enregistrement manquant a été signalé, avec la taille mesurée des familles de clones. diff --git a/doc/handwritten/for-maintainers/adr/0053-unify-discrete-generation-in-one-ordinal-space.md b/doc/handwritten/for-maintainers/adr/0053-unify-discrete-generation-in-one-ordinal-space.md deleted file mode 100644 index d21dbf38..00000000 --- a/doc/handwritten/for-maintainers/adr/0053-unify-discrete-generation-in-one-ordinal-space.md +++ /dev/null @@ -1,207 +0,0 @@ -# ADR-0053 | Unify discrete generation in one ordinal space, with a dedicated engine only where the arithmetic substrate forces one - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0053-unify-discrete-generation-in-one-ordinal-space.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-28 -**Accepted:** 2026-07-28 -**Decision Makers:** Reefact - -## Context - -JustDummies exposes the same interval-shaped constraint algebra over a wide set of value types: the -eight fixed-width integers, `char`, `TimeSpan`, `DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`, -the three binary floating-point types, `decimal`, and the two 128-bit integers. Across all of them a -test may declare bounds, an allow-list, exclusions, and — where the type has a natural stride — a -lattice such as a multiple, a temporal granularity, or a decimal scale. - -Two library-wide promises constrain how that algebra may be implemented. Values are **built to -satisfy** the declared constraints rather than drawn and filtered, so a generator that exists must -produce a value in one draw with no retry loop. And constraints that contradict each other must fail -at declaration time with a message naming **both** sides, which requires each bound to carry the -constraint that set it rather than just a number. - -The types divide by arithmetic substrate, not by kind: - -* Every discrete type whose domain fits 64 bits — the integers, the ticks-based time types, day and - time-of-day numbers, `char` — admits an **order-preserving** mapping onto the unsigned 64-bit - range. Bounds, exclusions, strides, cardinality and sampling are then the same problem for all of - them, stated once over ordinals. -* `Int128` and `UInt128` have domains that exceed 64 bits, so no such mapping into a 64-bit ordinal - exists. -* IEEE binary floating point is continuous. Its bit patterns are monotonic and could be mapped, but a - uniform draw over bit patterns is not a uniform draw over values — roughly half of all `double` - values lie in `[-1, 1]`. Excluding a point from a continuum also differs in kind from excluding one - from a finite set: the collision has measure zero, yet the constraint must still hold exactly. -* `decimal` is a 96-bit mantissa with a scale, and it has no next-representable-value ladder, so an - exclusive bound cannot be expressed by stepping to the adjacent value the way it can for integers - and for floats. - -The floor target is netstandard2.0 (ADR-0022 fixes the .NET Framework floor the library must keep -loading on). It offers no generic math abstraction over numeric types, and no 128-bit integers at -all, so arithmetic cannot be written once against a numeric type parameter in code that must compile -on the floor. C# additionally forbids the self-referential generic base class pattern for the public -sealed builders this API exposes. - -The resulting duplication is real and was measured by the 2026-07-20 architecture audit: the fourteen -numeric builders are near-identical clones modulo type substitution — roughly 2 450 lines — and the -five temporal builders follow the same pattern for some 800 more. A scripted scan of those clone -families found no behavioural copy-paste slip, and issue #214 has since added reflection-driven parity -guards over both the mirrored entry points and each family's constraint method set. - -This arrangement is the decision that most shapes the library's internals, and it constrains how every -future discrete or numeric builder is added. Its reasoning lives only in internal XML documentation, -while smaller decisions — the `Any.Combine` arity cap (ADR-0015) — carry records. - -## Decision - -Every discrete value type whose domain fits 64 bits is generated through one shared engine over a -common unsigned 64-bit ordinal space, and a separate engine exists only where the arithmetic substrate -cannot be represented there: 128-bit integers, IEEE binary floating point, and `decimal`. - -## Rationale - -* **The ordinal space is what lets the hard promises be stated once.** Eager satisfiability, exact - one-draw exclusion, cardinality counting and conflict detection are the parts of the algebra that - are easy to get subtly wrong and expensive to get wrong in more than one place. Over ordinals they - are one problem with one implementation, and every 64-bit discrete type inherits the same guarantees - by construction rather than by review. A `DateTime` bound and an `Int64` bound are then the same - object, so the promise to name both sides of a conflict does not have to be re-earned per type. -* **The split follows the substrate, which makes it falsifiable rather than a matter of taste.** Each - dedicated engine exists because a stated property of its arithmetic — width beyond 64 bits, - continuity, absence of a representable-value ladder — makes the shared engine's formulation - inapplicable, not because its type felt different. The rule reads as a test a future maintainer can - apply: a new type gets a mapping if its domain fits the ordinal space, and an engine only if it can - be shown not to. -* **A uniform draw must be uniform over values, not over representations.** This is why the monotonic - bit patterns of floating point are not pressed into the ordinal space even though the mapping - exists. Ordinal uniformity is exactly right where consecutive ordinals mean consecutive values, and - exactly wrong where they do not; keeping the continuous types on their own engine preserves the - meaning of "arbitrary" for both groups. -* **The floor target removes the generic alternative, so the choice is between one shared engine plus - three exceptions, or none at all.** Without generic math on netstandard2.0, sharing arithmetic across - numeric types requires either an ordinal indirection or per-type code. The ordinal space buys sharing - for the largest group — thirteen types — using only integer arithmetic the floor provides, and pays - for it in the three places where it genuinely cannot apply. -* **The accepted duplication is bounded and now guarded.** The cost of this decision is that the parts - of the algebra which cannot be shared are stated up to four times. That cost was accepted knowingly: - the audit's scan found no behavioural drift, and the parity guards from issue #214 turn "the clones - agree" from a discipline into a failing test. A decision whose main drawback is watched by a test is - in a different position than one whose drawback is watched by attention. - -## Alternatives Considered - -### One engine over a widened ordinal space - -Considered as the version of this design with no exceptions: map everything into a 128-bit ordinal, or -an arbitrary-precision one, and keep a single engine for the whole numeric and discrete surface. - -Rejected on the floor target first — netstandard2.0 has no 128-bit integer type, so the shared engine -could not compile where the library must load, and an arbitrary-precision substitute would put an -allocating numeric type on every draw for the thirteen types that need none. It also would not achieve -what it promises: widening the ordinal addresses only the width problem, leaving floating-point -uniformity and `decimal`'s missing ladder exactly as they were. The unified engine would still need -per-substrate branches, having lost the property that made unification worth it. - -### Per-type engines with no sharing - -Considered for its simplicity: each builder owns its own bounds, exclusions and sampling, with no -indirection to understand and no ordinal concept to learn. - -Rejected because it multiplies the algebra's difficult parts — eager satisfiability, exclusion -mapping, conflict provenance — by the number of types rather than by the number of substrates. The -clone families the audit measured show what that costs even where the code is generated by discipline: -the duplication that remains under this decision is the part that cannot be shared, and per-type -engines would make the shareable part duplicated too. - -### A generic numeric base class over a self-referential type parameter - -Considered as the language-level way to abstract the arithmetic without an ordinal indirection, which -would give one implementation and preserve each type's native arithmetic. - -Rejected as unavailable rather than undesirable: C# forbids the pattern for the public sealed builder -types this API exposes, and netstandard2.0 provides no generic math constraint through which the -arithmetic could be expressed, so the base class would have nothing to abstract over. Issue #214 -recorded reflection-driven parity tests as the mitigation for the duplication this alternative was -meant to remove. - -### Ordinal-map floating point through its bit patterns - -Considered because IEEE binary formats order monotonically as integers, which makes the mapping -available and would fold three more types into the shared engine. - -Rejected because it silently changes what an arbitrary value means. Uniformity over ordinals becomes -uniformity over representations, which for floating point concentrates the draw near zero; and point -exclusion over a continuum, which must be honoured exactly on a set of measure zero, is a different -problem from exclusion over a finite ordinal set. The mapping is possible, the semantics are not -equivalent, and the equivalence is the only reason to share an engine. - -### Ordinal-map `decimal` through its mantissa and scale - -Considered for the same reason: `decimal` is discrete, so an injection into an ordinal space seems -natural, and it would leave only two dedicated engines. - -Rejected because `decimal`'s discreteness is not uniform. The same value has several representations at -different scales, and the distance between adjacent representable values depends on the scale, so an -order-preserving mapping onto a contiguous ordinal range does not exist without first fixing a scale — -which is a constraint a caller may or may not declare. Bounds and strides for `decimal` therefore have -to be expressed in its own arithmetic. - -## Consequences - -### Positive - -* The difficult part of the discrete algebra — eager satisfiability, exact exclusion, cardinality, - conflict provenance — has one implementation for thirteen types, so a fix or a new constraint lands - once for all of them. -* Adding a discrete type whose domain fits 64 bits is a mapping and a display name, not a new engine. -* The engine boundary is stated as a property of the substrate, so a future maintainer can decide where - a new type belongs without re-litigating the architecture. -* A discrete draw stays uniform over its type's values, and no representation-space mapping distorts a - continuous one. Which magnitude an unconstrained continuous draw favours is a separate decision, - recorded in ADR-0052; this one settles only that the distortion never comes from the mapping. - -### Negative - -* Four engines mean the shareable-looking parts of the algebra are written up to four times, and a - change to the algebra may have to be applied in each. The 128-bit engine is deliberately a verbatim - sibling of the ordinal one, which is the clearest case of this cost. -* A reader must learn the ordinal indirection before following how a `DateTime` bound becomes a draw; - the shared engine is domain-agnostic by design, so nothing in it names the types it serves. -* The decision fixes a 64-bit boundary as the sharing threshold. It is the widest integer arithmetic - the floor target provides, not a derived optimum. - -### Risks - -* The clone families can drift behaviourally the next time one is edited. Mitigated by the parity - guards from issue #214, which fail on a renamed or missing constraint, and bounded by the fact that - the drift the audit found was in documentation rather than in behaviour. -* A future type may fit the ordinal space in principle while its semantics make ordinal uniformity - wrong, as floating point does. The decision's test is stated in terms of representability, so such a - case has to be recognized on its own merits rather than by applying the rule literally. - -## Follow-up Actions - -* Record the determinism and ambient-source contract separately (issue #216); it is the other - cross-cutting decision the audit found unrecorded, and it is not settled by this one. -* When the 128-bit engine and the ordinal engine next diverge, state whether the divergence is - intentional or a drift — this decision expects them to stay verbatim siblings. - -## References - -* ADR-0011 — Host JustDummies as a standalone package in this repository: the packaging decision this - internal architecture sits inside. -* ADR-0013 — Gate distinct collections by cardinality, otherwise by a bounded draw: the cardinality - reasoning that the shared engine's counting serves. -* ADR-0015 — Cap `Any.Combine` at arity eight: the smaller decision whose existing record made this - one's absence conspicuous. -* ADR-0022 — Floor the library's .NET Framework support at 4.7.2: the floor target that removes the - generic-math alternative. -* ADR-0025 — Generate matching strings from a home-grown regular subset: the neighbouring decision to - build values constructively rather than by filtering. -* ADR-0052 — Draw arbitrary numbers within an ordinary magnitude: the neighbouring decision governing - what an unconstrained continuous draw favours, on the two engines this decision keeps separate. -* Issue #217 — the audit item that asked for this record. -* Issue #214 — the parity guards this decision relies on to keep its accepted duplication safe. -* [2026-07-20 JustDummies architecture & design audit](../audit/2026-07-20-dummies-architecture-and-design-audit.md), - §5 — where the missing record was reported, with the measured size of the clone families. diff --git a/doc/handwritten/for-maintainers/adr/0054-decide-a-constraint-surface-by-constructive-versus-rejective.fr.md b/doc/handwritten/for-maintainers/adr/0054-decide-a-constraint-surface-by-constructive-versus-rejective.fr.md deleted file mode 100644 index f1d55066..00000000 --- a/doc/handwritten/for-maintainers/adr/0054-decide-a-constraint-surface-by-constructive-versus-rejective.fr.md +++ /dev/null @@ -1,237 +0,0 @@ -# ADR-0054 | Décider la surface de contraintes d'un générateur par constructif contre rejectif, et non par terminalité - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0054-decide-a-constraint-surface-by-constructive-versus-rejective.md) - -**Statut :** Accepté -**Proposé :** 2026-07-28 -**Accepté :** 2026-07-28 -**Décideurs :** Reefact - -Supersède l'[ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.fr.md). - -## Contexte - -Chaque générateur `JustDummies` est une recette fluente : chaque contrainte restreint ce qui peut être tiré, deux -contraintes contradictoires échouent à la déclaration avec une `ConflictingAnyConstraintException` qui nomme les deux -côtés, et la valeur est construite pour satisfaire toute la spécification plutôt que générée puis filtrée. Quelles -contraintes un générateur donné expose se décidait jusqu'ici générateur par générateur. - -`OneOf` est le cas le plus net de cette dérive. Mesuré sur `main` par réflexion sur les méthodes d'instance publiques -du type retourné, autres que `Generate()` : - -| appel | retourne | contraintes chaînables | -|---|---|---| -| `Any.Int32().OneOf(1, 2)` | `AnyInt32` | 13 — composable | -| `Any.DateTime().OneOf(d)` | `AnyDateTime` | 9 — composable | -| `Any.DateTimeOffset().OneOf(x)` | `AnyDateTimeOffset` | 11 — composable | -| `Any.Guid().OneOf(g)` | `AnyGuid` | 5 — composable | -| `Any.String().OneOf("a", "b")` | `AnyStringOneOf` | 0 — terminal | -| `Any.OneOf(x, y)` | `AnyOneOf` | 0 — terminal | - -Quatre familles renvoient leur propre builder composable ; deux renvoient un type distinct sans issue. Rien, au site -d'appel, ne distingue les deux. - -Autres faits qui cadrent le choix : - -* L'ADR-0030 a rendu `Any.String().OneOf(...)` terminal, au motif que réconcilier un ensemble de valeurs explicite avec - le préfixe, le suffixe, les valeurs contenues, la famille de caractères, la casse et la longueur d'une chaîne - multiplierait les combinaisons contradictoires et leurs messages de conflit, pour une combinaison dont personne - n'avait besoin. Elle listait en *Risque* qu'un appelant puisse attendre la composabilité du `OneOf` scalaire et être - surpris. L'ADR-0025 a rendu `Any.StringMatching(...)` terminal sur le même raisonnement, et l'ADR-0030 s'est alignée - dessus comme précédent. -* Le manque n'est pas théorique. `Any.ElementOf(existingOrders).DifferentFrom(theOneAlreadyUsed)` — tirer un autre - élément d'une fixture — n'existe pas, et `Any.String().OneOf("abc", "de").WithLength(3)` non plus, alors que `"abc"` - satisfait les deux. Le contournement LINQ pour le premier, `pool.Where(x => x != used).ToArray()`, fonctionne mais - rapporte un domaine vidé en `ArgumentException: At least one value is required`, ce qui blâme l'appelant pour un - tableau vide au lieu de nommer les deux contraintes en jeu. Les familles numériques, elles, nomment les deux. -* Les deux coûts que l'ADR-0030 évitait ne sont pas symétriques. Longueur, préfixe, suffixe, valeurs contenues, famille - de caractères et casse *mettent en forme* une chaîne que le générateur construit. `Except`/`DifferentFrom` ne mettent - rien en forme : elles retirent des valeurs. -* Les chaînes n'ont pas de projection ordinale où intégrer une exclusion, si bien que sur une chaîne mise en forme une - exclusion est déjà satisfaite par un **retirage borné** — une exception documentée au « construit, jamais filtré » - que le readme du paquet énonce, et le seul échec qu'`AnyString` diffère à la génération. -* `AnyPattern` fait déjà tourner une boucle bornée construire-vérifier-retirer à chaque tirage : depuis l'ADR-0048, - chaque valeur construite est vérifiée contre le vrai moteur .NET et retirée en cas d'échec, pour que « une valeur - générée matche son motif » tienne par construction. -* Les collections distinctes bornent à la déclaration selon la cardinalité et l'appartenance annoncées par le - générateur d'éléments, via l'interface interne `ICardinalityHint` (ADR-0013). `AnyStringOneOf` et `AnyOneOf` - l'annoncent tous deux ; `AnyString` non. -* L'issue #337 a établi qu'un échec de génération ne peut affirmer que ce que la recherche a réellement établi : un - budget dépensé se rapporte comme un budget dépensé, jamais comme une preuve d'impossibilité. -* Rien n'a été publié. `PublicAPI.Shipped.txt` ne contient que `#nullable enable`, aucun tag `dum-v*` n'existe et la - version est `0.1.0-dev` : changer un type de retour et supprimer un type public ne coûte rien aujourd'hui, et serait - une version majeure après la première publication. - -## Décision - -Les contraintes qu'un générateur expose se décident selon que chacune est **constructive** — elle décrit une valeur que -le générateur doit construire, et n'est offerte que là où il sait en construire une qui la satisfait — ou **rejective** -— elle retire des valeurs d'un domaine, et est offerte partout — plutôt qu'en déclarant un générateur terminal. - -## Justification - -* **« Terminal » décrivait le type retourné, pas le domaine : impossible d'en raisonner.** Les ADR-0030 et ADR-0025 ont - chacune abouti à un refus solide, mais l'ont consigné comme une propriété du générateur : *celui-ci n'expose rien de - plus*. Un appelant ne peut pas le prévoir, et un mainteneur qui ajoute le générateur suivant non plus — le tableau - mesuré ci-dessus est ce à quoi ressemble une règle que personne ne peut appliquer, après que quatre familles sont - parties d'un côté et deux de l'autre. Constructif contre rejectif est une propriété de la contrainte : le même test - répond pour tout générateur, y compris ceux qui ne sont pas encore écrits. -* **Un ensemble de valeurs fourni par l'appelant est un domaine, pas une mise en forme : le coût combinatoire que - l'ADR-0030 refusait ne se présente jamais.** L'argument de l'ADR-0030 était qu'un ensemble explicite devrait être - réconcilié avec chaque contrainte de mise en forme, chaque réconciliation exigeant sa propre analyse de conflit. C'est - vrai tant que le générateur *construit* une chaîne. Dès lors que les valeurs sont fournies, il n'y a plus rien à - construire : chaque autre contrainte devient un test que chaque valeur passe ou échoue, le domaine est l'ensemble des - valeurs qui passent, et la satisfaisabilité est l'unique question de savoir s'il en reste. Une question remplace la - matrice — et elle est tranchée précocement, donc la promesse qu'un générateur qui existe sait générer est tenue. -* **Le refus sur un motif survit au recadrage, et y gagne une raison qu'il n'avait pas.** Une contrainte de forme sur - `Any.StringMatching(...)` exigerait de construire une valeur dans l'intersection de deux langages réguliers, une - machinerie que la bibliothèque n'a pas et n'ajouterait pas pour cela. C'est désormais un énoncé sur la contrainte - plutôt que sur le type : le refus tient à ce qui ne peut pas être construit, pas à une étiquette, et il explique - pourquoi la paire d'exclusion est admise à côté au lieu de ressembler à une incohérence. -* **Une contrainte rejective ne demande aucune machinerie nouvelle et ne crée aucune exception nouvelle au « construit, - jamais filtré ».** Sur une chaîne mise en forme, les exclusions passent déjà par un retirage borné, et cette - exception est déjà documentée. Sur un motif, la boucle qui porterait l'exclusion est celle que l'ADR-0048 fait déjà - tourner à chaque tirage ; l'exclusion y est un prédicat de plus. Sur un ensemble de valeurs la question ne se pose - même pas : le domaine est fini et énumérable, donc les valeurs exclues sont retirées à la déclaration et le tirage - reste un unique choix uniforme. -* **La symétrie rend la surface apprenable ; nommer les deux côtés la garde honnête.** Un appelant qui a rencontré - `Except`/`DifferentFrom` sur un générateur peut les attendre sur le suivant, et un domaine vidé rapporte les deux - contraintes qui l'ont vidé au lieu d'une erreur d'argument sur un tableau que l'appelant n'a jamais écrit. C'est le - même contrat « un Arrange impossible est un défaut du test » que la bibliothèque applique partout ailleurs, étendu - aux deux endroits qui en étaient sortis. -* **Là où une recherche bornée porte l'exclusion, l'échec garde la seule affirmation qu'il peut soutenir.** Un - générateur de motif construit des valeurs depuis son motif ; il n'énumère jamais le langage, donc un budget épuisé est - un indice et non une preuve, et le message le dit — le standard posé par l'issue #337, appliqué au seul nouveau mode - d'échec que cette décision crée. -* **La fenêtre est ouverte maintenant et se referme à la première publication.** Rendre le `OneOf` des chaînes - composable change un type de retour et supprime un type public. Rien n'étant publié, c'est gratuit ; après `dum-v1`, - c'est une version majeure, et l'asymétrie devrait être subie ou payée. - -## Alternatives considérées - -### Garder les types terminaux et leur donner les contraintes de mise en forme - -Considérée parce qu'elle préserve intactes les décisions des ADR-0030 et ADR-0025 tout en refermant le manque de -capacité : un `AnyStringOneOf` composable répondrait à `OneOf("abc", "de").WithLength(3)` sans changer ce que -`Any.String().OneOf` renvoie. - -Rejetée parce qu'elle referme le manque en dupliquant la surface au lieu de supprimer l'asymétrie : l'ensemble des -contraintes existerait deux fois, sur deux types, avec deux jeux de messages de conflit à garder en phase, et -l'appelant devrait toujours savoir quel type il tient. L'asymétrie que montre le tableau mesuré est le défaut ; un -second type composable la laisse en place. - -### Ne rendre composable que `Any.String().OneOf`, et laisser le pool et le motif terminaux - -Considérée parce qu'elle corrige le cas au coût le plus visible — l'ensemble de valeurs des chaînes — pour le plus -petit changement, et laisse deux décisions intactes. - -Rejetée parce qu'elle corrige l'instance et non la règle. `Any.ElementOf(orders).DifferentFrom(used)` est l'idiome pour -lequel ce travail existe et manquerait encore, et le générateur suivant retomberait sur le même arbitrage non -documenté. Consigner la distinction est ce qui rend la surface prévisible ; l'appliquer à un seul des trois endroits -qu'elle couvre ne consignerait rien. - -### Ouvrir aussi le motif aux contraintes de forme, par génération puis filtrage - -Considérée pour une symétrie complète : avec une boucle de retirage déjà en place, une contrainte de longueur ou de -préfixe pourrait être satisfaite en tirant jusqu'à ce qu'une valeur la satisfasse, ce qui donnerait à tous les -générateurs de chaînes les mêmes contraintes. - -Rejetée parce qu'elle satisferait une contrainte *constructive* par rejet, la seule chose que la bibliothèque refuse de -faire. Le nombre de tirages attendu est non borné et dépend du motif — une contrainte de longueur que le motif produit -rarement transforme une déclaration en loterie silencieuse — donc l'échec dépendrait de la chance plutôt que de la -spécification, et la promesse du conflit précoce serait discrètement abandonnée pour toute une classe de contraintes. -Construire dans l'intersection de deux langages réguliers est la seule façon honnête de les offrir, et c'est hors -périmètre. - -### Intersecter le motif et les contraintes de forme par un produit d'automates - -Considérée comme la forme honnête de l'alternative précédente : compiler le motif et les contraintes de forme en -automates et générer depuis le produit satisferait les contraintes constructives par construction, sans aucun filtrage. - -Rejetée pour son coût et pour l'identité du paquet. Elle ajouterait un moteur d'automates à une bibliothèque dont tout -le sous-ensemble régulier est délibérément maison et petit (ADR-0025), pour une combinaison qu'aucun cas d'usage -rapporté ne demande — l'appelant qui veut une valeur mise en forme écrit la forme dans le motif. La décision refuse la -contrainte, et le refus est maintenant énoncé comme une limite de la machinerie plutôt que comme une propriété du type, -donc la porte reste ouverte si un besoin réel apparaît. - -### Laisser le cas du pool au LINQ de l'appelant - -Considérée parce que `pool.Where(x => x != used).ToArray()` fonctionne déjà, ne demande aucune API et garde -`AnyOneOf` minimal. - -Rejetée parce qu'elle dégrade exactement ce que la bibliothèque existe pour protéger : le diagnostic. Filtrer jusqu'au -vide lève `ArgumentException: At least one value is required (Parameter 'values')`, ce qui blâme l'appelant pour un -tableau vide au lieu de nommer le pool et l'exclusion qui l'ont vidé — alors que les familles numériques rapportent -`Cannot apply DifferentFrom(42) because it forbids every value OneOf(42) allows`. Elle sort aussi une décision de -domaine de la spécification pour la placer dans le code d'arrangement, où une collection distincte ne peut plus la -voir. - -## Conséquences - -### Positives - -* Un seul test — cette contrainte est-elle constructive ou rejective ? — répond à ce que n'importe quel générateur, - présent ou futur, peut exposer. L'asymétrie mesurée devient une règle plutôt qu'une table de précédents. -* `Any.String().OneOf(...)` compose avec toutes les contraintes de chaîne, et un ensemble vidé nomme les deux - contraintes en jeu — même verdict quel que soit celui des deux déclaré en premier, chaque ordre le formulant du - côté d'où arrive la seconde déclaration. -* `Any.ElementOf(orders).DifferentFrom(used)` et `Any.StringMatching(p).DifferentFrom(existing)` existent, avec les - diagnostics de conflit et d'échec que le reste de la bibliothèque donne. -* `AnyString` annonce la cardinalité de l'ensemble de valeurs survivant, donc une collection distincte sur un - générateur de chaînes à pool borne toujours précocement — la garantie que l'ADR-0030 obtenait via `AnyStringOneOf` - est tenue par le type qui le remplace. -* Un type public disparaît et aucun n'est ajouté. - -### Négatives - -* Un ensemble de valeurs de chaînes n'est plus un type à usage unique dont la vacuité est impossible par - construction ; c'est un filtre dont la satisfaisabilité doit être validée à chaque contrainte suivante, et cette - validation est du code qui peut être faux. -* Avec un ensemble de valeurs en vigueur, `Containing(...)` est satisfait en testant la valeur fournie plutôt que par - la mise en page côte à côte du chemin constructif : un `"aba"` fourni satisfait donc `Containing("ab").Containing("ba")` - alors qu'une chaîne construite ne le pourrait jamais. Les deux chemins donnent la même réponse partout où le chemin - constructif sait construire, mais le chemin à pool est strictement plus permissif, et cette différence doit être - documentée. -* Cette permissivité n'est atteignable que là où le chemin constructif n'avait pas déjà refusé. Une combinaison qu'il - rejette de son propre chef est refusée dès sa déclaration — le générateur ne peut pas savoir qu'un ensemble de - valeurs arrive, et différer ce refus coûterait à toute chaîne mise en forme son conflit précoce — donc ces mêmes - contraintes avec `OneOf` en dernier entrent encore en conflit, alors qu'avec `OneOf` en premier elles passent. - L'ordre est par ailleurs indifférent ; ici il ne l'est pas, et la surface doit le dire au lieu de promettre une - symétrie qu'elle n'a pas. -* `AnyPattern` n'est plus descriptible comme n'exposant rien : le cadrage « générateur terminal » de l'ADR-0025 a - désormais besoin de la qualification constructif/rejectif pour rester exact. - -### Risques - -* Un appelant peut lire la paire d'exclusion du motif comme une invitation à y attendre aussi des contraintes de forme, - et lire leur absence comme un oubli. Atténué en énonçant le refus et son motif — aucune machinerie pour construire - dans l'intersection de deux langages réguliers — dans la documentation du type lui-même et pas seulement ici. -* Une exclusion sur un motif peut vider un petit langage, et le retirage qui le découvre dépense tout son budget avant - d'échouer. Atténué en gardant ce budget séparé de celui du match, pour qu'aucun des deux échecs n'emprunte les - preuves de l'autre, et par un message qui affirme le budget dépensé et explicitement pas l'impossibilité (issue #337). -* Valider un ensemble de valeurs contre chaque contrainte coûte O(valeurs × contraintes) à la déclaration. Atténué par - le domaine : ce sont des ensembles écrits à la main dans du code d'arrangement de test, évalués une fois par - générateur, jamais par tirage. - -## Actions de suivi - -* Passer l'ADR-0030 au statut *Superseded* avec un lien vers celle-ci, une fois ce document accepté. -* Décider si l'ADR-0025 a besoin d'un successeur : sa décision — générer depuis un sous-ensemble régulier maison — - reste intacte, mais sa description du générateur comme *terminal* est restreinte par ce document. Signalé plutôt que - traité : cette ADR ne revisite pas la façon dont les motifs sont générés. - -## Références - -* ADR-0030 — Tirer des chaînes arbitraires depuis un ensemble de valeurs explicite et terminal : la décision que - celle-ci supersède, et le *Risque* qu'elle consignait sur les appelants attendant la composabilité. -* ADR-0025 — Générer les chaînes qui matchent depuis un sous-ensemble régulier maison : le précédent de générateur - terminal sur lequel l'ADR-0030 s'est alignée, et la raison pour laquelle une contrainte constructive sur un motif - reste refusée. -* ADR-0048 — Garantir qu'une valeur regex générée matche son motif, par retirage borné : la boucle que rejoint une - exclusion. -* ADR-0013 — Borner les collections distinctes par la cardinalité, sinon par un tirage borné : le contrat - `ICardinalityHint` auquel un ensemble de valeurs doit continuer de répondre. -* ADR-0045 — Garder les arguments publics et internes contre null : la convention que suit chaque nouveau membre. -* Issue #352 — l'item d'audit qui a demandé ce document. -* Issue #337 — le standard de véracité des affirmations pour un budget épuisé. -* `AnyString`, `StringSpec`, `AnyOneOf` et `AnyPattern` dans le projet `JustDummies`. diff --git a/doc/handwritten/for-maintainers/adr/0054-decide-a-constraint-surface-by-constructive-versus-rejective.md b/doc/handwritten/for-maintainers/adr/0054-decide-a-constraint-surface-by-constructive-versus-rejective.md deleted file mode 100644 index d16c77e6..00000000 --- a/doc/handwritten/for-maintainers/adr/0054-decide-a-constraint-surface-by-constructive-versus-rejective.md +++ /dev/null @@ -1,222 +0,0 @@ -# ADR-0054 | Decide a generator's constraint surface by constructive versus rejective, not by terminality - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0054-decide-a-constraint-surface-by-constructive-versus-rejective.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-28 -**Accepted:** 2026-07-28 -**Decision Makers:** Reefact - -Supersedes [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md). - -## Context - -Every `JustDummies` generator is a fluent recipe: each constraint narrows what may be drawn, contradictory constraints -fail at declaration with a `ConflictingAnyConstraintException` naming both sides, and the value is built to satisfy the -whole specification rather than generated and filtered. Which constraints a given generator exposes has, until now, -been decided generator by generator. - -`OneOf` is the clearest case of that drift. Measured on `main` by reflection over the returned type's public instance -methods other than `Generate()`: - -| call | returns | chainable constraints | -|---|---|---| -| `Any.Int32().OneOf(1, 2)` | `AnyInt32` | 13 — composable | -| `Any.DateTime().OneOf(d)` | `AnyDateTime` | 9 — composable | -| `Any.DateTimeOffset().OneOf(x)` | `AnyDateTimeOffset` | 11 — composable | -| `Any.Guid().OneOf(g)` | `AnyGuid` | 5 — composable | -| `Any.String().OneOf("a", "b")` | `AnyStringOneOf` | 0 — terminal | -| `Any.OneOf(x, y)` | `AnyOneOf` | 0 — terminal | - -Four families return their own composable builder; two return a distinct dead-end type. Nothing at the call site tells -the two apart. - -Further facts framing the choice: - -* ADR-0030 made `Any.String().OneOf(...)` terminal, on the ground that reconciling an explicit value set with the - prefix, suffix, contained values, character family, casing and length of a string would multiply contradictory - combinations and their conflict messages, for a combination nobody needed. It listed as a *Risk* that a caller may - expect the scalar `OneOf`'s composability and be surprised. ADR-0025 made `Any.StringMatching(...)` terminal on the - same reasoning, and ADR-0030 aligned with it as a precedent. -* The gap is not theoretical. `Any.ElementOf(existingOrders).DifferentFrom(theOneAlreadyUsed)` — drawing another - element of a fixture — does not exist, and `Any.String().OneOf("abc", "de").WithLength(3)` does not either, though - `"abc"` satisfies both. The LINQ workaround for the first, `pool.Where(x => x != used).ToArray()`, works but reports - an emptied domain as `ArgumentException: At least one value is required`, blaming the caller for an empty array - instead of naming the two constraints in play. The numeric families name both. -* The two costs ADR-0030 avoided are not symmetric. Length, prefix, suffix, contained values, character family and - casing *shape* a string the generator builds. `Except`/`DifferentFrom` do not shape anything: they remove values. -* Strings have no ordinal mapping to build an exclusion into, so on a shaped string an exclusion is already met by a - **bounded redraw** — a documented exception to "built, never filtered" that the package readme states, and the only - failure `AnyString` defers to generation. -* `AnyPattern` already runs a bounded build-verify-redraw loop on every draw: since ADR-0048 each built value is - checked against the real .NET engine and redrawn on a miss, so that "a generated value matches its pattern" holds by - construction. -* Distinct collections gate at declaration on the element generator's advertised cardinality and membership through - the internal `ICardinalityHint` (ADR-0013). `AnyStringOneOf` and `AnyOneOf` both advertise it; `AnyString` - does not. -* Issue #337 established that a generation failure may assert only what the search actually established: a spent - budget is reported as a spent budget, never as a proof of impossibility. -* Nothing has been published. `PublicAPI.Shipped.txt` contains only `#nullable enable`, no `dum-v*` tag exists and the - version is `0.1.0-dev`, so changing a return type and removing a public type costs nothing today and would be a - major version after the first release. - -## Decision - -What constraints a generator exposes is decided by whether each constraint is **constructive** — it describes a value -the generator must build, and is offered only where the generator can build one satisfying it — or **rejective** — it -removes values from a domain, and is offered everywhere — rather than by declaring a generator terminal. - -## Rationale - -* **"Terminal" described the returned type, not the domain, so it could not be reasoned about.** ADR-0030 and ADR-0025 - each reached a sound refusal, but recorded it as a property of the generator: *this one exposes nothing further*. A - caller cannot predict that, and neither can a maintainer adding the next generator — the measured table above is what - a rule nobody can apply looks like after four families went one way and two the other. Constructive versus rejective - is a property of the constraint, so the same test answers the question for every generator, including ones not yet - written. -* **A caller-supplied value set is a domain, not a layout, so the combinatorial cost ADR-0030 refused never arises.** - ADR-0030's argument was that an explicit set would have to be reconciled with each shaping constraint, each - reconciliation needing its own conflict analysis. That is true while the generator *builds* a string. Once the values - are supplied there is nothing to build: every other constraint becomes a test each value passes or fails, the domain - is the values that pass, and satisfiability is the single question of whether any remain. One question replaces the - matrix — and it is answered eagerly, so the promise that a generator which exists can generate is kept. -* **The refusal on a pattern survives the reframing, and gains a reason it did not have.** A shape constraint on - `Any.StringMatching(...)` would require building a value in the intersection of two regular languages, machinery the - library does not have and would not add for this. That is now a statement about the constraint rather than about the - type: the refusal stands on why it cannot be built, not on a label, and it explains why the exclusion pair is - admitted alongside it rather than looking like an inconsistency. -* **A rejective constraint needs no new machinery and creates no new exception to "built, never filtered".** On a - shaped string, exclusions are already met by a bounded redraw, and that exception is already documented. On a - pattern, the loop that would carry the exclusion is the one ADR-0048 already turns on every draw; the exclusion is - one more predicate inside it. On a value set the question does not even arise: the domain is finite and enumerable, - so the excluded values are removed at declaration and the draw stays a single uniform pick. -* **Symmetry is what makes the surface learnable; naming both sides is what keeps it honest.** A caller who has met - `Except`/`DifferentFrom` on one generator can expect them on the next, and an emptied domain reports the two - constraints that emptied it instead of an argument error about an array the caller never wrote. That is the same - "an impossible Arrange is a test defect" contract the library applies everywhere else, extended to the two places - that fell outside it. -* **Where a bounded search backs the exclusion, the failure keeps the claim it can support.** A pattern generator - builds values from its pattern; it never enumerates the language, so an exhausted budget is evidence and not proof, - and the message says so — the standard issue #337 set, applied to the one new failure mode this decision creates. -* **The window is open now and closes at the first release.** Making the string `OneOf` composable changes a return - type and deletes a public type. With nothing published that is free; after `dum-v1` it is a major version, and the - asymmetry would have to be lived with or paid for. - -## Alternatives Considered - -### Keep the terminal types and give them the shaping constraints - -Considered because it preserves ADR-0030's and ADR-0025's decisions intact while closing the capability gap: a -composable `AnyStringOneOf` would answer `OneOf("abc", "de").WithLength(3)` without changing what `Any.String().OneOf` -returns. - -Rejected because it closes the gap by duplicating the surface rather than removing the asymmetry: the constraint set -would exist twice, on two types, with two sets of conflict messages to keep in step, and the caller would still have to -know which type they hold. The asymmetry the measured table shows is the defect; a second composable type leaves it in -place. - -### Make only `Any.String().OneOf` composable, and leave the pool and the pattern terminal - -Considered because it fixes the case with the most obvious cost — the string value set — for the smallest change, and -leaves two decisions untouched. - -Rejected because it fixes the instance and not the rule. `Any.ElementOf(orders).DifferentFrom(used)` is the idiom this -work exists for and would still be missing, and the next generator would face the same undocumented judgement call. -Recording the distinction is what makes the surface predictable; applying it to one of the three places it covers would -record nothing. - -### Open the pattern to shape constraints as well, by generating and filtering - -Considered for full symmetry: with a redraw loop already in place, a length or prefix constraint could be met by -drawing until a value satisfies it, making every string generator carry the same constraints. - -Rejected because it would meet a *constructive* constraint by rejection, which is the one thing the library refuses to -do. The expected number of draws is unbounded and pattern-dependent — a length constraint the pattern rarely produces -turns a declaration into a silent lottery — so the failure would depend on luck rather than on the specification, and -the eager-conflict promise would be quietly abandoned for a whole class of constraints. Building in the intersection of -two regular languages is the only honest way to offer them, and it is out of scope. - -### Intersect the pattern with the shape constraints through an automaton product - -Considered as the honest form of the previous alternative: compiling both the pattern and the shape constraints to -automata and generating from the product would meet constructive constraints by construction, with no filtering. - -Rejected on cost and identity. It would add an automata engine to a package whose whole regular subset is deliberately -home-grown and small (ADR-0025), for a combination no reported use case asks for — the caller who wants a shaped value -writes the shape into the pattern. The decision refuses the constraint, and the refusal is now stated as a limit of the -machinery rather than a property of the type, so the door is left open should a real need appear. - -### Leave the pool case to the caller's LINQ - -Considered because `pool.Where(x => x != used).ToArray()` already works, needs no API, and keeps `AnyOneOf` -minimal. - -Rejected because it degrades exactly what the library exists to protect: the diagnostic. Filtering to nothing raises -`ArgumentException: At least one value is required (Parameter 'values')`, which blames the caller for an empty array -instead of naming the pool and the exclusion that emptied it — while the numeric families report -`Cannot apply DifferentFrom(42) because it forbids every value OneOf(42) allows`. It also moves a domain decision out -of the specification and into the arrange code, where a distinct collection can no longer see it. - -## Consequences - -### Positive - -* One test — is this constraint constructive or rejective? — answers what any generator, present or future, may - expose. The measured asymmetry becomes a rule rather than a table of precedents. -* `Any.String().OneOf(...)` composes with every string constraint, and an emptied set names the two constraints in - play — the same verdict whichever of the two was declared first, each order phrasing it from the side the second - declaration arrives on. -* `Any.ElementOf(orders).DifferentFrom(used)` and `Any.StringMatching(p).DifferentFrom(existing)` exist, with the - conflict and failure diagnostics the rest of the library gives. -* `AnyString` advertises the cardinality of its surviving value set, so a distinct collection over a pooled string - generator still gates eagerly — the guarantee ADR-0030 secured through `AnyStringOneOf` is kept by the type that - replaces it. -* One public type disappears and none is added. - -### Negative - -* A string value set is no longer a single-purpose type whose emptiness is impossible by construction; it is a filter - whose satisfiability must be validated on every subsequent constraint, and that validation is code that can be wrong. -* With a value set in force, `Containing(...)` is answered by testing the supplied value rather than by the - side-by-side layout the constructive path uses, so a pooled `"aba"` satisfies `Containing("ab").Containing("ba")` - while a built string never could. The two paths give the same answer wherever the constructive one can build at all, - but the pooled path is strictly more permissive, and that difference has to be documented. -* That permissiveness is reachable only where the constructive path had not already refused. A combination it rejects - on its own terms is refused the moment it is declared — the generator cannot know a value set is coming, and - deferring the refusal would cost every shaped string its eager conflict — so those constraints with `OneOf` last - still conflict, while `OneOf` first accepts them. Order is otherwise immaterial; here it is not, and the surface has - to say so rather than promise a symmetry it does not have. -* `AnyPattern` is no longer describable as exposing nothing: ADR-0025's "terminal generator" framing now needs the - constructive/rejective qualification to stay accurate. - -### Risks - -* A caller may read the pattern's exclusion pair as an invitation to expect shape constraints there too, and read - their absence as an oversight. Mitigated by stating the refusal and its ground — no machinery to build in the - intersection of two regular languages — in the type's own documentation rather than only here. -* An exclusion on a pattern can empty a small language, and the redraw that discovers it costs its whole budget before - failing. Mitigated by keeping the budget separate from the match budget, so neither failure borrows the other's - evidence, and by a message that claims the spent budget and explicitly not impossibility (issue #337). -* Validating a value set against every constraint is O(values × constraints) at declaration. Mitigated by the domain: - these are hand-written sets in test arrange code, evaluated once per generator, never per draw. - -## Follow-up Actions - -* Flip ADR-0030's status to *Superseded* with a link here, once this record is accepted. -* Decide whether ADR-0025 needs a successor: its decision — generate from a home-grown regular subset — stands - untouched, but its description of the generator as *terminal* is narrowed by this record. Flagged rather than acted - on: this ADR does not revisit how patterns are generated. - -## References - -* ADR-0030 — Draw arbitrary strings from an explicit, terminal value set: the decision this supersedes, and the *Risk* - it recorded about callers expecting composability. -* ADR-0025 — Generate matching strings from a home-grown regular subset: the terminal-generator precedent ADR-0030 - aligned with, and the reason a constructive constraint on a pattern stays refused. -* ADR-0048 — Guarantee a generated regex value matches its pattern, by bounded redraw: the loop an exclusion joins. -* ADR-0013 — Gate distinct collections by cardinality, otherwise by a bounded draw: the `ICardinalityHint` contract a - value set must keep answering. -* ADR-0045 — Guard public and internal arguments against null: the convention every new member follows. -* Issue #352 — the audit item that asked for this record. -* Issue #337 — the claim-truthfulness standard for an exhausted budget. -* `AnyString`, `StringSpec`, `AnyOneOf` and `AnyPattern` in the `JustDummies` project. diff --git a/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.fr.md b/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.fr.md index 6ef9fbb4..ea4e22f8 100644 --- a/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.fr.md +++ b/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.fr.md @@ -161,9 +161,9 @@ configuration unique, mais une configuration unique doublée d'une perte silenci ## Références -* [ADR-0031](0031-name-any-factories-after-their-clr-type.fr.md) — le même geste dans un +* [just-dummies ADR-0010](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0010-name-any-factories-after-their-clr-type.md) — le même geste dans un autre registre : une convention rendue vérifiable par la machine plutôt que laissée à l'attention. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.fr.md) — une règle appliquée par une +* [just-dummies ADR-0024](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0024-guard-public-and-internal-arguments-against-null.md) — une règle appliquée par une convention de réflexion, pour la même raison. * Pull request [#360](https://github.com/Reefact/first-class-errors/pull/360) — la mise en œuvre, et les mesures sur lesquelles reposent les alternatives écartées. diff --git a/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.md b/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.md index da0c899c..a5aef31f 100644 --- a/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.md +++ b/doc/handwritten/for-maintainers/adr/0055-enforce-the-style-rules-the-compiler-can-express.md @@ -156,9 +156,9 @@ one configuration and a silent loss of rules. ## References -* [ADR-0031](0031-name-any-factories-after-their-clr-type.md) — the same move in a +* [just-dummies ADR-0010](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0010-name-any-factories-after-their-clr-type.md) — the same move in a different register: a convention made machine-checkable rather than left to attention. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.md) — a rule enforced by a +* [just-dummies ADR-0024](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0024-guard-public-and-internal-arguments-against-null.md) — a rule enforced by a reflection convention, for the same reason. * Pull request [#360](https://github.com/Reefact/first-class-errors/pull/360) — the implementation, and the measurements the rejected alternatives rest on. diff --git a/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.fr.md b/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.fr.md index 65041685..5ba6a1f2 100644 --- a/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.fr.md +++ b/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.fr.md @@ -154,5 +154,5 @@ l'indirection qui a échoué ici. * [ADR-0055](0055-enforce-the-style-rules-the-compiler-can-express.fr.md) — la moitié « compilation » du même problème, et les mesures qui la fondent. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.fr.md) — une convention rendue +* [just-dummies ADR-0024](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0024-guard-public-and-internal-arguments-against-null.md) — une convention rendue observable plutôt que laissée à l'attention. diff --git a/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.md b/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.md index cd83173b..7a9cf449 100644 --- a/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.md +++ b/doc/handwritten/for-maintainers/adr/0056-state-the-coding-rules-where-an-agent-can-act-on-them.md @@ -151,5 +151,5 @@ indirection that failed here. * [ADR-0055](0055-enforce-the-style-rules-the-compiler-can-express.md) — the build-time half of the same problem, and the measurements behind it. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.md) — a convention made +* [just-dummies ADR-0024](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0024-guard-public-and-internal-arguments-against-null.md) — a convention made observable rather than left to attention. diff --git a/doc/handwritten/for-maintainers/adr/0058-suppress-ca1510-while-the-netstandard-floor-stands.fr.md b/doc/handwritten/for-maintainers/adr/0058-suppress-ca1510-while-the-netstandard-floor-stands.fr.md deleted file mode 100644 index cc8cc33e..00000000 --- a/doc/handwritten/for-maintainers/adr/0058-suppress-ca1510-while-the-netstandard-floor-stands.fr.md +++ /dev/null @@ -1,167 +0,0 @@ -# ADR-0058 | Supprimer CA1510 tant que le plancher antérieur à .NET 6 tient - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0058-suppress-ca1510-while-the-netstandard-floor-stands.md) - -**Statut :** Accepté -**Proposé :** 2026-07-29 -**Accepté :** 2026-07-29 -**Décideurs :** Reefact - -## Contexte - -`CA1510` demande que toute garde d'argument de la forme - -```csharp -if (source is null) { throw new ArgumentNullException(nameof(source)); } -``` - -soit réécrite en `ArgumentNullException.ThrowIfNull(source);`. L'aide est plus -concise, et elle porte `[CallerArgumentExpression]`, ce qui évite de répéter le -nom du paramètre. - -Le rapport SonarQube Cloud en compte **323** — de loin le plus gros groupe de -constats du projet, environ 55 % de tous les code smells. Ils se répartissent en -deux populations que le rapport présente à l'identique et qui ne le sont pas : - -* **314 dans `JustDummies`** et **1 dans `JustDummies.UnitTests`**. Les deux - projets sont multi-ciblés de part et d'autre de la frontière .NET 6 — - `netstandard2.0;net8.0` pour la bibliothèque, `net10.0;net472` pour sa suite de - contrat sur le plancher de support (ADR-0022). `ArgumentNullException.ThrowIfNull` - est arrivée avec .NET 6 : l'analyzer la voit sur la jambe moderne et signale - chaque garde, alors que *le même fichier source* doit continuer à compiler sur - la jambe qui ne l'a pas. -* **8 dans `FirstClassErrors.GenDoc`**, qui ne cible que `net8.0`. Là, rien ne - s'y oppose. - -L'échappatoire évidente — un polyfill — n'existe pas pour cette API. Un polyfill -fonctionne quand le compilateur lie par le nom et que la forme est purement -compile-time : un attribut comme `CallerArgumentExpressionAttribute` se déclare -dans son propre assembly et le compilateur le reconnaît. `ThrowIfNull` n'est ni -l'un ni l'autre. C'est une **méthode statique sur un type BCL qui existe déjà en -downlevel**, et C# n'a pas de méthodes d'extension statiques : la seule façon de -la fournir serait de déclarer un `System.ArgumentNullException` concurrent qui -gagne la résolution de nom sur l'ancienne jambe. Masquer un type d'exception du -framework pour satisfaire une règle de style échange un gain cosmétique contre un -piège. - -`CA1510` est signalée en sévérité **Info**. Elle n'a jamais fait échouer un -build, et elle ne porte ni sur la fiabilité ni sur la sécurité. - -## Décision - -`CA1510` est supprimée, par projet et avec la raison inscrite dans le fichier -projet, pour les deux projets qui doivent compiler sous .NET 6 ; elle est -honorée partout où le plancher ne s'applique pas, et les huit gardes de -`FirstClassErrors.GenDoc` sont réécrites avec l'aide. - -## Justification - -* **La règle est insatisfiable là où elle crie le plus fort.** 315 des 323 - constats sont dans du source qui doit compiler sur un framework cible dépourvu - de l'API. Aucune modification de ces fichiers ne les résout ; seul un - déplacement du plancher le ferait. -* **Les alternatives coûtent plus que la règle ne vaut.** Réécrire chaque garde - en appel à une aide maison `Guard.NotNull` toucherait 315 sites d'appel, - ajouterait une indirection à chaque vérification d'argument, et perdrait - précisément le comportement `[CallerArgumentExpression]` qui motive la règle. - Encadrer chaque garde d'un `#if NET6_0_OR_GREATER` doublerait le nombre de - lignes de toutes les gardes de la bibliothèque. -* **Une suppression qui porte sa raison vaut mieux qu'une suppression muette.** - Le `NoWarn` est dans les deux fichiers projet qui portent la contrainte, à côté - d'un commentaire qui nomme le plancher et cette ADR : le prochain mainteneur - lit la raison là où il rencontre l'effet, et sait ce qui la rendra caduque. -* **La suppression est cantonnée, pas globale.** Elle n'est ni dans - `Directory.Build.props` ni dans `.editorconfig` : un projet qui ne franchit pas - la frontière conserve la règle. `FirstClassErrors.GenDoc` le démontre en s'y - conformant. -* **Elle expire d'elle-même.** Le jour où `JustDummies` abandonnera - `netstandard2.0` et où la suite de tests abandonnera `net472`, les lignes - `NoWarn` deviendront mortes et la règle pourra être honorée partout. Il n'y a - rien d'autre à se rappeler. - -## Alternatives envisagées - -### Réécrire les gardes via une aide maison `Guard.NotNull` - -Une aide interne unique, appelée depuis chaque garde, supprimerait le motif que -l'analyzer reconnaît : la règle se tairait sans aucune suppression. - -Rejetée parce qu'elle modifie 315 sites d'appel sans rien acheter que le lecteur -souhaitait : la garde ne se lit pas mieux, chaque vérification d'argument gagne -un niveau d'indirection, et l'ergonomie `[CallerArgumentExpression]` qui rend -`ThrowIfNull` attrayante n'est de toute façon pas reproductible sur -`netstandard2.0`. Elle inventerait aussi un second idiome de garde à côté de -celui qu'utilise le reste du dépôt. - -### Encadrer chaque garde d'un `#if NET6_0_OR_GREATER` - -Strictement correct, et honore la règle sur la jambe moderne. - -Rejetée pour la lisibilité : cela transforme une garde d'une ligne en cinq, 315 -fois, dans une bibliothèque dont les gardes d'arguments sont les lignes les plus -lues. - -### Polyfiller `ArgumentNullException.ThrowIfNull` - -Envisagée en premier, et la raison d'être de cette ADR. Rejetée parce qu'elle -n'est pas réalisable : le membre est statique sur un type qui existe déjà en -downlevel, et le fournir exigerait de masquer `System.ArgumentNullException` -lui-même. - -### Supprimer globalement dans `Directory.Build.props` ou `.editorconfig` - -Moins cher encore — une ligne pour tout le dépôt. - -Rejetée parce qu'elle éteindrait la règle pour des projets qui *peuvent* -l'honorer, `FirstClassErrors.GenDoc` en tête, et parce que l'`.editorconfig` de -ce dépôt ne porte délibérément aucune sévérité de diagnostic (il le dit en -en-tête : le style et les sévérités d'inspection sont l'affaire du DotSettings). - -### Abandonner `netstandard2.0` dans `JustDummies` - -Résout le constat purement et simplement. - -Rejetée parce que le plancher est une promesse produit, pas un détail -d'implémentation : la portée du package — et le plancher de support .NET -Framework 4.7.2 que consigne l'ADR-0022 — vaut plus qu'une règle de style. - -## Conséquences - -### Positives - -* 323 constats disparaissent : 315 par une suppression qui énonce sa raison, 8 - en s'y conformant. -* La contrainte est écrite là où elle mord, si bien que le prochain lecteur n'a - pas à la redécouvrir depuis une erreur de compilation. -* Les projets qui peuvent honorer la règle continuent de le faire, et les - nouveaux en héritent. - -### Négatives - -* Deux fichiers projet portent un `NoWarn` qu'il faudra retirer quand le - plancher bougera ; rien n'impose ce retrait au-delà de cette ADR. -* Une nouvelle garde écrite dans `JustDummies` ne sera pas orientée vers l'aide - moderne sur la jambe moderne, puisque la règle est éteinte pour tout le projet - plutôt que pour la seule construction interne downlevel. - -### Risques - -* Ne lire que le compte (« 55 % des smells partis ») surestime le changement. - Rien n'a été amélioré dans le code pour les 315 ; seul le rapport l'a été. Les - huit réécritures de `FirstClassErrors.GenDoc` constituent l'intégralité du - changement substantiel. -* Un contributeur futur pourrait prendre ce `NoWarn` pour une licence à ignorer - d'autres conseils de l'analyzer dans ces projets. Il est cantonné à un seul - identifiant de règle précisément pour rendre cette lecture difficile à tenir. - -## Actions de suivi - -* Retirer les deux entrées `NoWarn`, et la raison d'être de cette ADR, si et - quand `JustDummies` abandonnera `netstandard2.0` et `JustDummies.UnitTests` - `net472`. - -## Références - -* ADR-0022 — le plancher de support .NET Framework 4.7.2 auquel ces projets sont tenus. -* ADR-0011 — `JustDummies` comme package autonome, dont le plancher sert la portée. -* `JustDummies/JustDummies.csproj`, `JustDummies.UnitTests/JustDummies.UnitTests.csproj` — où vit la suppression. diff --git a/doc/handwritten/for-maintainers/adr/0058-suppress-ca1510-while-the-netstandard-floor-stands.md b/doc/handwritten/for-maintainers/adr/0058-suppress-ca1510-while-the-netstandard-floor-stands.md deleted file mode 100644 index 708b4f6a..00000000 --- a/doc/handwritten/for-maintainers/adr/0058-suppress-ca1510-while-the-netstandard-floor-stands.md +++ /dev/null @@ -1,158 +0,0 @@ -# ADR-0058 | Suppress CA1510 while the pre-.NET-6 floor stands - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0058-suppress-ca1510-while-the-netstandard-floor-stands.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-29 -**Accepted:** 2026-07-29 -**Decision Makers:** Reefact - -## Context - -`CA1510` asks that every argument guard of the shape - -```csharp -if (source is null) { throw new ArgumentNullException(nameof(source)); } -``` - -be rewritten as `ArgumentNullException.ThrowIfNull(source);`. The helper is -terser, and it carries `[CallerArgumentExpression]` so the parameter name no -longer has to be repeated. - -The SonarQube Cloud report counts **323** occurrences — by some distance the -largest single group of findings on the project, and about 55% of all code -smells. They fall into two populations that look identical in the report and are -not: - -* **314 in `JustDummies`** and **1 in `JustDummies.UnitTests`**. Both projects - multi-target across the .NET 6 boundary — `netstandard2.0;net8.0` for the - library, `net10.0;net472` for its contract suite on the support floor - (ADR-0022). `ArgumentNullException.ThrowIfNull` arrived in .NET 6, so the - analyzer sees it on the modern leg and reports every guard, while the *same - source file* must still compile on the leg that does not have it. -* **8 in `FirstClassErrors.GenDoc`**, which targets `net8.0` only. Nothing - stands in the way there. - -The obvious escape — a polyfill — does not exist for this API. Polyfills work -when the compiler binds by name and the shape is compile-time only: an attribute -such as `CallerArgumentExpressionAttribute` can simply be declared in your own -assembly and the compiler recognises it. `ThrowIfNull` is neither. It is a -**static method on a BCL type that already exists downlevel**, and C# has no -static extension methods, so the only way to supply it would be to declare a -competing `System.ArgumentNullException` that wins name resolution on the old -leg. Shadowing a framework exception type to satisfy a style rule trades a -cosmetic gain for a trap. - -`CA1510` is reported at **Info** severity. It has never failed a build, and it -bears on neither reliability nor security. - -## Decision - -`CA1510` is suppressed, per project and with the reason stated in the project -file, for the two projects that must compile below .NET 6; it is honoured -everywhere the floor does not apply, and the eight guards in -`FirstClassErrors.GenDoc` are rewritten to use the helper. - -## Rationale - -* **The rule is unsatisfiable where it is loudest.** 315 of the 323 findings sit - in source that has to compile on a target framework without the API. No - edit to those files can resolve them; only the floor moving would. -* **The alternatives cost more than the rule is worth.** Rewriting every guard - as a call to a home-grown `Guard.NotNull` helper would touch 315 call sites, - add an indirection to every argument check, and lose the - `[CallerArgumentExpression]` behaviour that motivates the rule in the first - place. Wrapping each guard in `#if NET6_0_OR_GREATER` would double the line - count of every guard in the library. -* **A suppression carrying its reason beats a silent one.** The `NoWarn` sits in - the two project files that own the constraint, next to a comment naming the - floor and this ADR, so the next maintainer reads the reason where they meet - the effect — and knows what makes it obsolete. -* **The suppression is scoped, not global.** It is not in `Directory.Build.props` - and not in `.editorconfig`, so a project that does not straddle the boundary - keeps the rule. `FirstClassErrors.GenDoc` proves the point by complying. -* **It expires by itself.** The day `JustDummies` drops `netstandard2.0` and the - test suite drops `net472`, the `NoWarn` lines become dead and the rule can be - honoured throughout. Nothing else has to be remembered. - -## Alternatives Considered - -### Rewrite the guards through a first-party `Guard.NotNull` helper - -A single internal helper called from every guard would remove the pattern the -analyzer matches, so the rule would fall silent without any suppression. - -Rejected because it changes 315 call sites to buy nothing the reader wanted: the -guard reads no better, every argument check gains a level of indirection, and -the `[CallerArgumentExpression]` ergonomics that make `ThrowIfNull` attractive -are not reproducible on `netstandard2.0` anyway. It would also invent a second -guard idiom alongside the one the rest of the repository uses. - -### Bracket every guard with `#if NET6_0_OR_GREATER` - -Strictly correct, and honours the rule on the modern leg. - -Rejected on legibility: it turns a one-line guard into five, 315 times, in a -library whose argument guards are its most-read lines. - -### Polyfill `ArgumentNullException.ThrowIfNull` - -Considered first, and the reason this ADR exists. Rejected because it is not -achievable: the member is static on a type that already exists downlevel, and -supplying it would require shadowing `System.ArgumentNullException` itself. - -### Suppress globally in `Directory.Build.props` or `.editorconfig` - -Cheaper still — one line for the whole repository. - -Rejected because it would switch the rule off for projects that *can* honour it, -`FirstClassErrors.GenDoc` first among them, and because `.editorconfig` in this -repository deliberately carries no diagnostic severities (it says so at the top: -style and inspection severities are the DotSettings' job). - -### Drop `netstandard2.0` from `JustDummies` - -Resolves the finding outright. - -Rejected because the floor is a product promise, not an implementation detail: -the package's reach — and the .NET Framework 4.7.2 support floor that ADR-0022 -records — is worth more than a style rule. - -## Consequences - -### Positive - -* 323 findings clear: 315 by a suppression that states its reason, 8 by - complying. -* The constraint is written down where it bites, so the next reader does not - re-derive it from a build error. -* Projects that can honour the rule still do, and new ones inherit it. - -### Negative - -* Two project files carry a `NoWarn` that must be removed when the floor moves; - nothing enforces that removal beyond this ADR. -* A new guard written in `JustDummies` will not be nudged toward the modern - helper on the modern leg, because the rule is off for the whole project rather - than for the downlevel inner build only. - -### Risks - -* Reading the count alone ("55% of the smells gone") overstates the change. - Nothing about the code improved for the 315; only the report did. The eight - rewrites in `FirstClassErrors.GenDoc` are the whole of the substantive change. -* A future contributor may take the `NoWarn` as licence to ignore other - analyzer guidance in these projects. It is scoped to one rule id precisely so - that reading remains hard to sustain. - -## Follow-up Actions - -* Remove both `NoWarn` entries, and this ADR's reason for being, if and when - `JustDummies` drops `netstandard2.0` and `JustDummies.UnitTests` drops - `net472`. - -## References - -* ADR-0022 — the .NET Framework 4.7.2 support floor these projects are held to. -* ADR-0011 — `JustDummies` as a standalone package, whose reach the floor serves. -* `JustDummies/JustDummies.csproj`, `JustDummies.UnitTests/JustDummies.UnitTests.csproj` — where the suppression lives. diff --git a/doc/handwritten/for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.fr.md b/doc/handwritten/for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.fr.md deleted file mode 100644 index 36541f32..00000000 --- a/doc/handwritten/for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.fr.md +++ /dev/null @@ -1,156 +0,0 @@ -# ADR-0059 | Garder la frontière recette/valeur avec des analyseurs là où le système de types ne l'atteint pas - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) - -**Statut :** Accepté -**Proposé :** 2026-07-29 -**Accepté :** 2026-07-31 -**Décideurs :** Reefact - -## Contexte - -* L'ADR-0020 a supprimé les 28 conversions implicites des générateurs JustDummies vers leur type généré, faisant de - `Generate()` la seule matérialisation. Sa section *Risques* évaluait le danger résiduel comme borné : un - utilisateur qui omet `.Generate()` obtient « une erreur de compilation avec un message actionnable ..., **jamais - une valeur silencieusement fausse** ». Ses *Actions de suivi* concluaient : « Ne pas poursuivre l'analyseur - optionnel suggéré par l'issue #190 ; la suppression le rend inutile. » -* Cette évaluation tient partout où la position cible est typée par la **valeur générée**. `int x = Any.Int32()` - est un `CS0029`, `Any.Int32() == 5` un `CS0019`, et `Assert.Equal(Any.Int32(), value)` un `CS0411`. Là, - supprimer la conversion a bien transformé une substitution silencieuse en erreur de compilation. -* Elle ne tient pas partout où la position cible accepte le **type statique propre** du générateur. Les - générateurs sont des types référence : aucune conversion n'est nécessaire et il n'y en avait donc aucune à - supprimer. `object`, `params object[]`, `dynamic`, un élément d'`object[]` ou de `List`, un trou - d'interpolation, un opérande de concaténation `string`, ainsi que les `object.ToString()` / `object.Equals` - hérités acceptent tous un générateur tel quel. -* Aucun générateur JustDummies ne surcharge `ToString()`. Le rendre sous forme de texte produit donc le nom de - type CLR du constructeur — `$"{Any.String()}"` donne littéralement la chaîne `"JustDummies.AnyString"`. Vérifié - par compilation : chacune des formes ci-dessus compile sans le moindre diagnostic. -* La valeur obtenue est non vide, plausible et identique à chaque exécution. Elle atteint le code sous test comme - s'il s'agissait d'une valeur arbitraire : le test passe au vert tout en exerçant une constante — précisément le - résultat qu'`Any` existe pour empêcher, et celui que l'ADR-0020 avait consigné comme impossible. -* Une seconde forme voisine est silencieuse pour la même raison structurelle. Les générateurs étant des recettes - immuables, une contrainte retourne un nouveau générateur ; un appel dont le résultat est jeté - (`numbers.NonEmpty();`) se lit comme une mutation et perd l'invariant déclaré. Vérifié : aucun diagnostic - compilateur, CA ou IDE ne se déclenche, même en `AnalysisLevel=latest-all`, une invocation étant une instruction - d'expression légale. -* L'ADR-0044 a établi les analyseurs JustDummies de première partie comme la réponse du dépôt à une faute que le - système de types ne peut pas exprimer, et son propre suivi invite à appliquer ce motif aux fautes futures de ce - genre. L'ADR-0035 trace la frontière en sens inverse pour les conflits de contraintes : le système de types - porte ce qui est structurel, l'analyseur porte ce qu'il ne peut pas porter. -* JustDummies est en pré-1.0 : aucun consommateur n'a encore appris l'un ou l'autre comportement. - -## Décision - -La frontière recette/valeur est gardée par des analyseurs JustDummies de première partie dans toute position qui -accepte le type statique propre d'un générateur, position que la suppression des conversions implicites n'a pas -fermée. - -## Justification - -* La décision prise par l'ADR-0020 n'est pas touchée et reste juste : `Generate()` demeure la seule - matérialisation, et aucune conversion implicite ne revient. Ce que le présent ADR révise, c'est une **prédiction** - que l'ADR-0020 formulait sur le monde d'après cette suppression — qu'aucune valeur silencieusement fausse ne - pouvait y survivre — ainsi que l'action de suivi qui reposait sur cette prédiction. Un enregistrement dont le - raisonnement est sain mais dont l'affirmation factuelle est désormais connue comme fausse se corrige par un - nouvel enregistrement, non en laissant l'affirmation se lire comme encore vraie. -* L'analyseur que l'ADR-0020 a écarté et ceux décidés ici ne sont pas le même instrument. Celui qui fut rejeté - était le prix du *maintien* des conversions — une surface permanente de 28 opérateurs plus une règle pour en - policer les pièges, afin de préserver un raccourci. Ceux-ci font l'inverse : rien n'est préservé, aucune surface - n'est ajoutée, ils ferment ce que la suppression a laissé ouvert. L'argument de l'ADR-0020 contre le premier - n'atteint pas les seconds. -* Le point d'application suit ce que chaque mécanisme peut savoir, du même grain que l'ADR-0035 et l'ADR-0044. C# - ne peut pas refuser un type référence dans une position typée `object`, ni rendre illégale une instruction - d'expression ; le système de types ne *peut donc pas* porter ces deux règles, ce qui fait de l'analyseur le seul - mécanisme disponible plutôt qu'un substitut affaibli. -* La sévérité suit le mode de défaillance plutôt que la famille. Un générateur rendu comme texte est un vert - silencieux — la compilation réussit, le test passe, l'assertion ne veut rien dire — soit le cas que l'ADR-0044 a - déjà jugé digne de faire échouer la compilation. Une contrainte jetée est un vert *probabiliste*, rouge - seulement sur l'exécution qui tire hors du domaine visé : elle avertit plutôt qu'elle n'échoue. -* Le coût est borné par ce que les règles renoncent à signaler. Un diagnostic sur la frontière recette/valeur est - peu coûteux à avoir tort, un usage légitime d'un générateur en position `object` étant rare et une suppression - tenant en une ligne ; les règles sont néanmoins cadrées pour rester muettes sur un résultat explicitement jeté et - sur un test négatif vérifiant un conflit, ce qui les garde utilisables dans une suite qui teste le comportement - d'échec de la bibliothèque elle-même. - -## Alternatives considérées - -### Laisser cela à la documentation, comme le prescrivait le suivi de l'ADR-0020 - -Considérée parce que c'est la décision en vigueur, qu'elle ne coûte rien, et que la documentation de la -bibliothèque enseigne déjà longuement le modèle recette/valeur. - -Rejetée parce que la documentation ne peut pas atteindre la défaillance. Le défaut produit une compilation qui -réussit et un test qui passe : il n'existe aucun moment où un lecteur est incité à consulter la documentation, ni -aucun artefact signalant que quelque chose ne va pas. Tous les autres mécanismes de la bibliothèque qui gardent ce -modèle — les conversions supprimées, les conflits de contraintes levés au plus tôt — échouent bruyamment ; laisser -ce seul cas à la prose est le seul endroit où le modèle est enseigné sans être appliqué. - -### Rétablir une conversion implicite étroite pour que le compilateur refuse les positions ambiguës - -Considérée parce qu'une conversion vers le type généré ferait lier la valeur plutôt que la recette dans une -position `object`, fermant le trou dans le langage plutôt qu'à côté. - -Rejetée parce qu'elle réintroduit exactement ce que l'ADR-0020 a supprimé, et pour une plus mauvaise raison : la -conversion est effectuante, non idempotente et levante, et la position `object` est précisément l'endroit où son -comportement serait le moins prévisible. Ce serait échanger une faute diagnosticable contre une faute qui ne l'est -pas. - -### Rendre les générateurs étanches au rendu textuel en surchargeant `ToString()` - -Considérée parce qu'une surcharge retournant la valeur tirée, ou une chaîne délibérément alarmante, rendrait -`$"{Any.String()}"` inoffensif ou manifestement faux au premier coup d'œil, sans aucun analyseur. - -Rejetée dans les deux lectures. Retourner une valeur tirée fait de `ToString()` un tirage effectuant et non -idempotent — la conversion implicite à nouveau, sous un autre nom. Retourner une chaîne d'alarme améliore le -symptôme sans l'empêcher : le test passe toujours, assère toujours sur une constante, et l'alarme ne se manifeste -que si un humain lit la valeur. - -## Conséquences - -### Positives - -* Les deux formes silencieuses deviennent des diagnostics à la compilation : un générateur rendu comme texte fait - échouer la compilation, une contrainte jetée avertit, avec un message qui enseigne le modèle plutôt que de se - contenter de nommer la règle. -* L'affirmation factuelle de l'ADR-0020 est corrigée dans le registre au lieu d'être laissée à la découverte de - celui qui la heurtera, et la raison pour laquelle son action de suivi ne s'applique plus est énoncée là où un - futur mainteneur ira la chercher. -* La catégorie `JustDummies.Usage` donne un domicile aux règles recette/valeur, si bien qu'un consommateur peut les - régler indépendamment des règles de reproductibilité. - -### Négatives - -* L'ensemble de règles grossit, et avec lui la surface documentaire : chaque règle porte une page anglaise et une - page française, une entrée d'index et une ligne de suivi de version. -* Deux règles se déclenchent sur des formes qu'une suite testant le comportement d'échec de JustDummies écrit - légitimement ; toutes deux portent donc une exclusion documentée qu'un lecteur doit connaître pour raisonner sur - ce que les règles n'attrapent pas. - -### Risques - -* La famille des positions `object` est plus large que les deux règles décidées ici — un paramètre typé `object`, - un élément de `params object[]`, `dynamic` — et la couvrir comporte un vrai faux positif : un utilitaire de test - qui accepte délibérément `object` et matérialise lui-même. Atténué en laissant cette règle hors de la présente - décision et en la tranchant sur des observations de mise en pratique plutôt qu'à l'avance. -* Une règle fondée sur l'absence de surcharge de `ToString()` cesserait silencieusement de s'appliquer si un - générateur en gagnait une un jour. Atténué en résolvant spécifiquement l'`object.ToString()` hérité, de sorte - qu'une vraie surcharge est exclue par construction et non par hypothèse. - -## Actions de suivi - -* Ne rien remplacer : la décision de l'ADR-0020 demeure inchangée, et son statut appartient au mainteneur s'il juge - que l'affirmation corrigée le justifie. -* Trancher la règle restante sur les positions `object` à partir des observations recueillies sur les suites de ce - dépôt, et pas avant. - -## Références - -* ADR-0020 — matérialiser les dummies uniquement par `Generate()` ; la décision que celui-ci laisse debout et dont - il corrige l'affirmation de risque résiduel. -* ADR-0044 — fournir des analyseurs JustDummies de première partie ; le motif que la présente décision applique, et - la source du grain de sévérité (« un vert silencieux mérite de faire échouer la compilation »). -* ADR-0035 — appliquer les conflits `Any` structurels à la compilation, ceux dépendant des valeurs à l'exécution ; - le même raisonnement « l'application suit ce que le mécanisme peut savoir », appliqué à la surface de - contraintes. -* Issue #190 — définir et documenter le contrat des conversions implicites de générateurs ; l'origine de - l'analyseur que l'ADR-0020 a écarté. diff --git a/doc/handwritten/for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.md b/doc/handwritten/for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.md deleted file mode 100644 index a07501e2..00000000 --- a/doc/handwritten/for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.md +++ /dev/null @@ -1,145 +0,0 @@ -# ADR-0059 | Guard the recipe-versus-value boundary with analyzers where the type system cannot reach it - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0059-guard-the-recipe-versus-value-boundary-with-analyzers.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-29 -**Accepted:** 2026-07-31 -**Decision Makers:** Reefact - -## Context - -* ADR-0020 removed the 28 implicit conversions from JustDummies generators to their generated types, making - `Generate()` the sole materialization. Its *Risks* section assessed the residual danger as bounded: a user who - omits `.Generate()` hits "a compile-time error with an actionable message ..., **never a silent wrong value**". - Its *Follow-up Actions* concluded: "Do not pursue the optional analyzer suggested in issue #190; the removal - makes it unnecessary." -* That assessment holds wherever the target position is typed by the **generated value**. `int x = Any.Int32()` is - `CS0029`, `Any.Int32() == 5` is `CS0019`, and `Assert.Equal(Any.Int32(), value)` is `CS0411`. There, removing the - conversion did turn a silent substitution into a compile error. -* It does not hold wherever the target position accepts the generator's **own static type**. Generators are - reference types, so no conversion is needed and none was there to remove: `object`, `params object[]`, `dynamic`, - an `object[]` or `List` element, an interpolation hole, an operand of a `string` concatenation, and the - inherited `object.ToString()` / `object.Equals` all accept a generator as it stands. -* No JustDummies generator overrides `ToString()`. Rendering one as text therefore yields the builder's CLR type - name — `$"{Any.String()}"` produces the literal string `"JustDummies.AnyString"`. Verified by compilation: every - shape above builds with zero diagnostics of any kind. -* The resulting value is non-empty, plausible, and identical on every run. It reaches the code under test as if it - were an arbitrary value, so the test passes green while exercising a constant — the precise outcome `Any` exists - to prevent, and the one ADR-0020 recorded as impossible. -* A second, adjacent shape is silent for the same structural reason. Generators are immutable recipes, so a - constraint returns a new generator; a call whose result is discarded (`numbers.NonEmpty();`) reads as a mutation - and drops the declared invariant. Verified: no compiler, CA or IDE diagnostic fires, even at - `AnalysisLevel=latest-all`, because an invocation is a legal expression statement. -* ADR-0044 established first-party JustDummies analyzers as the repository's answer to a mistake the type system - cannot express, and its own follow-up invites applying that pattern to future such mistakes. ADR-0035 draws the - dividing line the other way round for constraint conflicts: the type system carries what is structural, the - analyzer carries what it cannot. -* JustDummies is pre-1.0, so no consumer has yet been taught either behaviour. - -## Decision - -The recipe-versus-value boundary is guarded by first-party JustDummies analyzers in every position that accepts a -generator's own static type, which the removal of the implicit conversions did not close. - -## Rationale - -* The decision ADR-0020 took is untouched and remains right: `Generate()` stays the sole materialization, and no - implicit conversion returns. What this ADR revises is one **prediction** ADR-0020 made about the world after that - removal — that no silent wrong value could survive it — and the follow-up action that rested on the prediction. - A record whose reasoning is sound but whose factual claim is now known to be false is corrected by a new record, - not by leaving the claim to be read as still true. -* The analyzer ADR-0020 declined and the analyzers decided here are not the same instrument. The rejected one was - the price of *keeping* the conversions — a permanent 28-operator surface plus a rule to police its traps, to - preserve a shorthand. These are the opposite: nothing is preserved and no surface is added, they close what the - removal left open. ADR-0020's argument against the first does not reach the second. -* The enforcement point follows what each mechanism can know, the same grain as ADR-0035 and ADR-0044. C# cannot - refuse a reference type in a position typed `object`, and it cannot make an expression statement illegal; the - type system therefore *cannot* carry these two rules, which makes the analyzer the only available mechanism - rather than a weaker substitute for one. -* Severity follows the failure mode rather than the family. A generator rendered as text is a silent green — the - build succeeds, the test passes, the assertion is meaningless — which is the case ADR-0044 already judged worth - failing the build for. A discarded constraint is a *probabilistic* green, red only on the run that draws outside - the intended domain, so it warns rather than fails. -* The cost is bounded by what the rules decline to report. A diagnostic on the recipe-versus-value boundary is - cheap to be wrong about, because a legitimate use of a generator in an `object` position is rare and a - suppression is one line; the rules are nevertheless scoped so that an explicitly discarded result and a - conflict-asserting negative test stay silent, which is what keeps them usable in a suite that tests the library's - own failure behaviour. - -## Alternatives Considered - -### Leave it to documentation, as ADR-0020's follow-up prescribed - -Considered because it is the standing decision, costs nothing, and the library's documentation already teaches the -recipe-versus-value model at length. - -Rejected because documentation cannot reach the failure. The defect produces a passing build and a passing test: -there is no moment at which a reader is prompted to consult the documentation, and no artifact that says anything -is wrong. Every other mechanism in the library that guards this model — the removed conversions, the eager -constraint conflicts — fails loudly; leaving this one case to prose is the only place where the model is taught but -not enforced. - -### Restore a narrow implicit conversion so the compiler can refuse the ambiguous positions - -Considered because a conversion to the generated type would make an `object` position bind the value rather than -the recipe, closing the hole in the language rather than beside it. - -Rejected because it reintroduces exactly what ADR-0020 removed, and for a worse reason: the conversion is -effectful, non-idempotent and throwing, and the `object` position is the one place where its behaviour would be -least predictable. It would trade a diagnosable mistake for an undiagnosable one. - -### Make the generators sealed against text rendering by overriding `ToString()` - -Considered because an override returning the drawn value, or a deliberately alarming string, would make -`$"{Any.String()}"` harmless or obviously wrong at a glance, with no analyzer at all. - -Rejected on both readings. Returning a drawn value makes `ToString()` an effectful, non-idempotent draw — the -implicit conversion again, under another name. Returning an alarm string improves the symptom without preventing -it: the test still passes, still asserts on a constant, and the alarm surfaces only if a human reads the value. - -## Consequences - -### Positive - -* The two silent shapes become build-time diagnostics: a generator rendered as text fails the build, and a - discarded constraint warns, with a message that teaches the model rather than merely naming the rule. -* ADR-0020's factual claim is corrected in the record rather than left to be discovered by whoever hits it, and the - reason its follow-up action no longer applies is stated where a future maintainer will look. -* The `JustDummies.Usage` category gives the recipe-versus-value rules a home, so a consumer can tune them - independently of the reproducibility rules. - -### Negative - -* The rule set grows, and with it the documentation surface: each rule carries an English and a French page, an - index entry and a release-tracking row. -* Two rules fire on shapes that a suite testing JustDummies' own failure behaviour legitimately writes, so both - carry a documented exclusion that a reader must know to reason about what the rules do not catch. - -### Risks - -* The `object`-position family is wider than the two rules decided here — an `object`-typed parameter, a - `params object[]` element, `dynamic` — and covering it carries a genuine false positive: a test helper that - deliberately accepts `object` and materializes it itself. Mitigated by leaving that rule out of this decision and - deciding it on dogfooding evidence rather than in advance. -* A rule keyed on the absence of a `ToString()` override would silently stop applying if a generator ever gained - one. Mitigated by resolving the inherited `object.ToString()` specifically, so a real override is excluded by - construction rather than by assumption. - -## Follow-up Actions - -* Supersede nothing: ADR-0020's decision stands unchanged, and its status is the maintainer's to revisit if the - corrected claim is judged to warrant it. -* Decide the remaining `object`-position rule on dogfooding evidence gathered from this repository's own suites, - not before. - -## References - -* ADR-0020 — materialize dummies only through `Generate()`; the decision this one leaves standing and whose - residual-risk claim it corrects. -* ADR-0044 — ship first-party JustDummies analyzers; the pattern this decision applies, and the source of the - severity grain ("a silent green is worth failing the build"). -* ADR-0035 — enforce structural `Any` conflicts at compile time, value-dependent ones at run time; the same - "enforcement follows what the mechanism can know" reasoning, applied to the constraint surface. -* Issue #190 — define and document the contract of implicit generator conversions; the origin of the analyzer - ADR-0020 declined. diff --git a/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.fr.md b/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.fr.md index 4173679b..2db56707 100644 --- a/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.fr.md +++ b/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.fr.md @@ -11,7 +11,7 @@ Ce dépôt livre deux paquets d'analyseurs. `FirstClassErrors.Analyzers` porte `FCE001`–`FCE022` ; `JustDummies.Analyzers`, créé sous -[ADR-0044](0044-ship-justdummies-analyzers.md), porte `JD001`–`JD028`. +[just-dummies ADR-0023](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0023-ship-justdummies-analyzers.md), porte `JD001`–`JD028`. Les deux sont vérifiés de façon très différente. @@ -204,10 +204,10 @@ précis ne va pas. ## Références -* [ADR-0044](0044-ship-justdummies-analyzers.md) — la décision de livrer des analyseurs +* [just-dummies ADR-0023](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0023-ship-justdummies-analyzers.md) — la décision de livrer des analyseurs JustDummies de première partie. * [ADR-0046](0046-make-the-per-pull-request-mutation-gate-advisory.md) — le contrôle consultatif par pull request que ce document refuse d'imiter. -* [ADR-0059](0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) — les règles +* [just-dummies ADR-0038](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0038-guard-the-recipe-versus-value-boundary-with-analyzers.md) — les règles recette-contre-valeur, dont le dogfooding a produit une partie des preuves ci-dessus. * [Les règles d'analyse JustDummies](../../for-users/analyzers/README.md). diff --git a/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.md b/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.md index 9ba1ce10..c94c0164 100644 --- a/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.md +++ b/doc/handwritten/for-maintainers/adr/0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.md @@ -11,7 +11,7 @@ This repository ships two analyzer packages. `FirstClassErrors.Analyzers` carries `FCE001`–`FCE022`; `JustDummies.Analyzers`, created under -[ADR-0044](0044-ship-justdummies-analyzers.md), carries `JD001`–`JD028`. +[just-dummies ADR-0023](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0023-ship-justdummies-analyzers.md), carries `JD001`–`JD028`. The two are verified very differently. @@ -189,10 +189,10 @@ binary claim that something specific is wrong. ## References -* [ADR-0044](0044-ship-justdummies-analyzers.md) — the decision to ship +* [just-dummies ADR-0023](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0023-ship-justdummies-analyzers.md) — the decision to ship first-party JustDummies analyzers. * [ADR-0046](0046-make-the-per-pull-request-mutation-gate-advisory.md) — the advisory per-pull-request check this record declines to imitate. -* [ADR-0059](0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) — the +* [just-dummies ADR-0038](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0038-guard-the-recipe-versus-value-boundary-with-analyzers.md) — the recipe-versus-value rules, whose dogfooding produced part of the evidence above. * [The JustDummies analyzer rules](../../for-users/analyzers/README.md). diff --git a/doc/handwritten/for-maintainers/adr/0063-throw-the-library-s-own-exceptions-through-named-factories.fr.md b/doc/handwritten/for-maintainers/adr/0063-throw-the-library-s-own-exceptions-through-named-factories.fr.md deleted file mode 100644 index e78af815..00000000 --- a/doc/handwritten/for-maintainers/adr/0063-throw-the-library-s-own-exceptions-through-named-factories.fr.md +++ /dev/null @@ -1,138 +0,0 @@ -# ADR-0063 | Lever les exceptions de la bibliothèque via des factories nommées - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0063-throw-the-library-s-own-exceptions-through-named-factories.md) - -**Statut :** Accepté -**Proposé :** 2026-07-30 -**Accepté :** 2026-07-30 -**Décideurs :** Reefact - -## Contexte - -JustDummies refuse les contradictions au moment de la déclaration, et dit pourquoi. Cette promesse -tient dans un message : `Cannot apply WithLength(3) because StartingWith("ORD-") already requires at -least 4 characters.` Ces messages sont bons, et ils étaient assemblés là où ils étaient levés — au -milieu du code qui décide. - -Résultat : une méthode qui parle de contraintes consacre quatre lignes à de la prose. Dans les specs -d'intervalle, une boucle de tirage se lisait ainsi : - -```csharp -throw new AnyGenerationException( - $"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayGuidance(random.Seed)}", - random.Seed, - new InvalidOperationException($"Every representable value within {NudgeBudget.ToString(CultureInfo.InvariantCulture)} steps of the drawn candidate, in both directions, is excluded or out of bounds. Values further away were not examined, so this is an exhausted local search rather than an empty range.")); -``` - -Quatre lignes que le lecteur doit enjamber pour suivre l'algorithme, dont aucune ne parle de tirer -une valeur. Et quand le même échec est rapporté depuis plusieurs endroits, la formulation est -retapée : la phrase `Cannot apply X because Y.` était écrite à **84 sites de levée** dans la -bibliothèque. - -La duplication est le symptôme visible, et ce n'est pas la raison de cette décision. Un message -assemblé une seule fois reste de la prose au milieu de la logique. - -## Décision - -Toute levée d'une exception que cette bibliothèque déclare passe par une factory statique sur cette -exception, nommée d'après l'échec qu'elle rapporte — que le message se répète ou non, `internal` sauf -si un consommateur doit construire l'exception, ne gardant rien parce que construire une exception ne -doit jamais lever, et regroupant ses arguments en value object quand nommer le cas demanderait plus -de paramètres épars qu'un lecteur ne peut en tenir dans l'ordre — tandis que les exceptions `System`, -que la bibliothèque ne possède pas, gardent la forme de clause de garde qu'ADR-0045 leur impose. - -## Justification - -**Le code métier reste du code métier.** `WithMinimum` parle de resserrer une borne. Qu'une -contradiction produise telle phrase anglaise est de la plomberie, et la plomberie va avec le -mécanisme. Un type d'exception *est* un mécanisme avant tout, ce qui en fait le bon domicile : le -site d'appel énonce quel échec s'est produit, et l'exception sait le dire. - -**Un nom vaut mieux qu'un message au site d'appel.** `throw AnyGenerationException.GridNudgeExhausted(...)` -dit à un lecteur ce qui s'est passé en trois mots. Le message qu'elle produit le dit à -l'*utilisateur*, ce qui est un autre public et un autre moment. Les séparer permet aux deux d'être -bons. - -**La règle est peu coûteuse à suivre et à vérifier.** « Ce fichier contient-il un `throw new` d'une -de nos exceptions ? » est une question à réponse binaire, et c'est ce qui fait tenir une convention. -Une règle nuancée par « quand le message se répète » demanderait un jugement à chaque site et -dériverait, comme le montrent les 84 phrases écrites à la main. - -**L'uniformité est le but, pas l'économie.** Dix factories pour dix sites utilisés une fois chacun -n'est pas du gaspillage : ce sont dix sites qui se lisent comme des constats plutôt que comme de -l'assemblage de chaînes. - -## Alternatives considérées - -### Des factories seulement là où un message se répète - -La première version de ce travail appliquait ce critère, et il est faux dans les deux sens. Il laisse -les échecs uniques assembler de la prose en ligne — précisément le cas que la boucle de tirage des -specs d'intervalle montrait au pire — et il rend la règle invérifiable, puisque « se répète » est une -propriété de toute la bibliothèque, pas du site qu'on écrit. - -### Une factory générique prenant une raison libre - -Essayée, sous la forme `Because(applying, reason)`. Elle centralise la phrase et rien d'autre : -l'appelant compose encore la raison, donc le site d'appel ne dit toujours rien de l'échec. Pire, -c'est une porte de sortie : tant qu'elle existe, aucun cas futur n'a besoin d'un nom. Rejetée — et -les quatre sites qui l'utilisaient se sont révélés être un seul cas nommable. - -### Garder les arguments des factories - -Envisagé puis implémenté brièvement, avant retrait. Cela contredit ADR-0045, dont la convention par -réflexion exclut les types d'exception avant même de regarder l'accessibilité — les gardes n'étaient -donc jamais exercées, et une suite verte ne disait rien à leur sujet. - -### Adopter le modèle d'erreur de FirstClassErrors - -Le voisin évident : FirstClassErrors modélise déjà les erreurs comme des valeurs de première classe, -avec codes, contexte et documentation générée. Rejeté au nom de la frontière que consigne ADR-0011 : -JustDummies ne doit référencer aucun projet FirstClassErrors, et est délibérément *error-agnostic*, -parce qu'elle est référencée par les projets de tests de ses consommateurs et ne doit pas leur -imposer un modèle d'erreur. Ce qui traverse cette frontière, c'est la discipline, pas les types. Les -codes d'erreur sont déclinés avec : ces échecs sont lus une fois par un développeur qui corrige son -test, et un code stable et documenté aurait un coût documentaire qu'aucun de ces lecteurs ne -percevrait. - -## Conséquences - -### Positives - -* Le site d'appel énonce quel échec s'est produit, et rien d'autre : une méthode qui parle de - contraintes se lit comme telle. Le message qu'elle produit s'adresse à un autre lecteur, à un autre - moment ; les séparer permet aux deux d'être bons. -* La formulation d'un échec a un seul domicile. `ConflictingAnyConstraintException` porte la forme de - phrase de tous les conflits, ce qui en fait le seul fichier à lire quand un message doit changer. -* La règle se vérifie à l'œil : « ce fichier lève-t-il une de nos exceptions avec `new` ? » a une - réponse binaire, et c'est ce qui fait tenir une convention. - -### Négatives - -* Un nouvel échec exige une factory avant de pouvoir être levé. Cette friction est voulue — nommer le - cas est l'étape de conception — mais c'est une friction. -* Les types d'exception grossissent, et qui cherche un message doit aller à l'exception plutôt qu'au - code qui la lève. -* Convertir les sites existants touche la majeure partie de la bibliothèque, une tranche - fonctionnelle à la fois. - -### Risques - -* Une factory nommée d'après une *forme de phrase* plutôt que d'après un échec satisferait la lettre - de cette règle en la vidant ; la première tentative a fait exactement cela et a dû être défaite. Le - test est de savoir si le site d'appel se lit comme un constat, sans ses arguments. -* Les messages sont du comportement observable. Les suites unitaires en assertent le contenu, donc - une conversion qui en altérerait un échouerait — la parade étant que les suites restent vertes à - chaque tranche, pas seulement à la fin. - -## Références - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) — JustDummies est autonome et - error-agnostic ; elle ne doit référencer aucun projet FirstClassErrors. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.fr.md) — les gardes d'arguments, - et l'exemption des types d'exception sur laquelle cette décision s'appuie. -* [ADR-0046](0046-make-the-per-pull-request-mutation-gate-advisory.fr.md) — le check de mutation par - PR est consultatif. -* [ADR-0049](0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.fr.md) — - consigne que le `--since` de Stryker sélectionne par fichier, et retire le générateur JustDummies - de la matrice par pull request à cause de ce que cela coûte. diff --git a/doc/handwritten/for-maintainers/adr/0063-throw-the-library-s-own-exceptions-through-named-factories.md b/doc/handwritten/for-maintainers/adr/0063-throw-the-library-s-own-exceptions-through-named-factories.md deleted file mode 100644 index 816cd047..00000000 --- a/doc/handwritten/for-maintainers/adr/0063-throw-the-library-s-own-exceptions-through-named-factories.md +++ /dev/null @@ -1,133 +0,0 @@ -# ADR-0063 | Throw the library's own exceptions through named factories - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0063-throw-the-library-s-own-exceptions-through-named-factories.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-30 -**Accepted:** 2026-07-30 -**Decision Makers:** Reefact - -## Context - -JustDummies refuses contradictions at declaration time, and says why. That promise is kept by a -message: `Cannot apply WithLength(3) because StartingWith("ORD-") already requires at least 4 -characters.` The messages are good, and they were assembled where they were thrown — in the middle -of the code that decides. - -The result is that a method about constraints spends four lines on prose. In the interval specs a -draw loop read like this: - -```csharp -throw new AnyGenerationException( - $"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayGuidance(random.Seed)}", - random.Seed, - new InvalidOperationException($"Every representable value within {NudgeBudget.ToString(CultureInfo.InvariantCulture)} steps of the drawn candidate, in both directions, is excluded or out of bounds. Values further away were not examined, so this is an exhausted local search rather than an empty range.")); -``` - -Four lines the reader must step over to follow the algorithm, none of which is about drawing a -value. And when the same failure is reported from several places the wording is retyped: the -sentence `Cannot apply X because Y.` was written out at **84 throw sites** across the library. - -Duplication is the visible symptom, and it is not the reason for this decision. A message assembled -once is still prose in the middle of logic. - -## Decision - -Every throw of an exception this library declares goes through a static factory on that exception, -named after the failure it reports — whether or not the message repeats, `internal` unless a -consumer must construct the exception, guarding nothing because building an exception must never -throw, and grouping arguments into a value object when naming the case would otherwise take more -loose parameters than a reader can keep in order — while the `System` exceptions the library does -not own keep the guard-clause form ADR-0045 requires of them. - -## Rationale - -**The business code stays business code.** `WithMinimum` is about tightening a bound. That a -contradiction produces a particular English sentence is plumbing, and plumbing belongs with the -mechanism. An exception type *is* a mechanism more than anything else, which makes it the right -home: the call site states which failure occurred, and the exception knows how to say it. - -**A name is worth more than a message at the call site.** `throw AnyGenerationException.GridNudgeExhausted(...)` -tells a reader what happened in three words. The message it produces tells the *user* what happened, -which is a different audience and a different moment. Separating them lets both be good. - -**The rule is cheap to follow and cheap to check.** "Does this file contain `throw new`, for one of -our own exceptions?" is a question with a yes/no answer, which is what makes a convention hold. A -rule qualified by "when the message repeats" would need judgement at every site and would drift, as -the 84 hand-written sentences show. - -**Uniformity is the point, not economy.** Ten factories for ten call sites used once each is not -waste; it is ten call sites that read as statements of fact rather than as string assembly. - -## Alternatives Considered - -### Factories only where a message repeats - -The first version of this work applied that criterion, and it is wrong in both directions. It -leaves single-use failures assembling prose inline — the very case the interval-spec draw loop -showed at its worst — and it makes the rule un-checkable, since "repeats" is a property of the -whole library, not of the site being written. - -### A general-purpose factory taking a free-form reason - -Tried, in the shape `Because(applying, reason)`. It centralises the sentence and nothing else: the -caller still composes the reason, so the call site still says nothing about the failure. Worse, it -is an escape hatch — with it available, no future case needs a name. Rejected, and the four sites -that used it turned out to be one nameable case. - -### Guarding the factories' arguments - -Considered and implemented briefly, then removed. It contradicts ADR-0045, whose reflection -convention excludes exception types before accessibility is ever considered — so the guards were -never exercised, and a green test suite said nothing about them. - -### Adopting the FirstClassErrors error model - -The obvious neighbour: FirstClassErrors already models errors as first-class values with codes, -context and generated documentation. Rejected on the boundary ADR-0011 records — JustDummies must -not reference any FirstClassErrors project, and is deliberately *error-agnostic*, because it is -referenced by its consumers' test projects and must not impose an error model on them. What crosses -that boundary is the discipline, not the types. Error codes are declined with it: these failures are -read once by a developer fixing a test, and a stable, documented code would carry a documentation -cost no reader of theirs would ever collect. - -## Consequences - -### Positive - -* The call site states which failure occurred and nothing else, so a method about constraints reads - as a method about constraints. The message it produces addresses a different reader at a different - moment, and separating them lets both be good. -* The wording of a failure has one home. `ConflictingAnyConstraintException` holds the sentence shape - for every conflict in the library, which makes it the only file to read when a message must change. -* The rule is checkable by inspection: "does this file throw one of our exceptions with `new`?" has a - yes/no answer, which is what makes a convention hold. - -### Negative - -* A new failure requires a factory before it can be thrown. That friction is intended — naming the - case is the design step — but it is friction. -* The exception types grow, and a reader looking for a message must go to the exception rather than - to the code that raises it. -* Converting the existing sites touches most of the library, one functional slice at a time. - -### Risks - -* A factory named after a *shape of sentence* rather than a failure would satisfy the letter of this - rule and defeat it; the first attempt did exactly that and had to be undone. The test is whether - the call site reads as a statement of fact without its arguments. -* Messages are observable behaviour. The unit suites assert their content, so a conversion that - altered one would fail — the mitigation is that the suites must stay green through every slice, not - merely at the end. - -## References - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) — JustDummies is standalone and - error-agnostic; it must not reference any FirstClassErrors project. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.md) — argument guards, and the - exemption of exception types on which this decision rests. -* [ADR-0046](0046-make-the-per-pull-request-mutation-gate-advisory.md) — the per-PR mutation check - is advisory. -* [ADR-0049](0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md) — - records that Stryker's `--since` selects per file, and drops the JustDummies generator from the - per-pull-request matrix because of what that costs. diff --git a/doc/handwritten/for-maintainers/adr/0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.fr.md b/doc/handwritten/for-maintainers/adr/0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.fr.md deleted file mode 100644 index cd39eaac..00000000 --- a/doc/handwritten/for-maintainers/adr/0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.fr.md +++ /dev/null @@ -1,127 +0,0 @@ -# ADR-0064 | Exempter tout le chemin de report d'échec de la convention de garde null - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md) - -**Statut :** Accepté -**Proposé :** 2026-07-30 -**Accepté :** 2026-07-30 -**Décideurs :** Reefact - -Remplace [ADR-0045](0045-guard-public-and-internal-arguments-against-null.fr.md). - -## Contexte - -ADR-0045 impose à tout membre public et interne de refuser un argument référence non-nullable `null` -par une `ArgumentNullException`, et l'applique via une convention par réflexion dans -`JustDummies.UnitTests` qui découvre les membres au lieu de les nommer. Elle exemptait les types -d'exception, pour une raison qui mérite d'être répétée : leurs constructeurs s'exécutent pendant -qu'une erreur est traitée, donc y lever une `ArgumentNullException` remplacerait l'échec rapporté par -un échec sur le fait de le rapporter, et l'original serait perdu. - -Cette exemption est indexée sur le fait d'**être** une `Exception`. Le danger, lui, ne l'est pas. - -ADR-0063 a fait passer les levées par des factories nommées d'après l'échec, et l'une d'elles devait -dire laquelle de deux contraintes un conflit doit blâmer. Cinq chaînes éparses dans un ordre que rien -ne vérifie était la mauvaise signature ; la paire — une contrainte et ce qu'elle affirme — est donc -devenue un petit type, `ConstraintClaim`. Il est construit au site de levée, en argument de la -factory : - -```csharp -throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_exactConstraint!, $"already fixes the count at {V(exact)}"), - ConstraintClaim.Of(_minConstraint!, $"already requires at least {Elements(_min)}")); -``` - -`ConstraintClaim` n'est pas une `Exception` : la convention l'a donc inspecté et a fait échouer la -construction tant qu'il ne gardait pas ses arguments. Ajouter ces gardes a satisfait la convention et -recréé exactement ce qu'ADR-0045 interdit, une trame d'appel plus tôt : un `null` surgirait -désormais en `ArgumentNullException` depuis un helper, au lieu du conflit que le code rapportait. - -La convention avait raison : la règle telle qu'écrite s'appliquait. La règle telle qu'écrite était -tracée une trame trop étroite. - -## Décision - -La règle d'ADR-0045 tient intégralement, son exemption étant élargie des types d'exception à tout type -qui n'existe que pour construire une exception de la bibliothèque, déclaré par un marqueur interne que -la convention par réflexion lit, plutôt qu'inféré depuis l'usage. - -## Justification - -**Le danger appartient au chemin, pas au type de base.** Ce qui rend une garde nuisible ici, c'est -*quand* elle s'exécute — pendant qu'un échec est rapporté — et cela tient à l'usage du type, non à ce -dont il hérite. Une règle indexée sur `: Exception` attrape le cas courant et rate le reste ; et le -reste est précisément ce que créent les factories d'ADR-0063. - -**Un marqueur garde l'exemption honnête.** L'alternative est l'inférence, et l'inférence devrait -deviner : « n'est utilisé que par des factories d'exception » n'est pas une propriété qu'un test par -réflexion peut établir, et toute approximation raterait des cas ou exempterait en silence des types -qui devraient être gardés. Un marqueur tient en une ligne, se cherche au grep, et n'est faux que si -quelqu'un l'écrit à tort — ce qu'un relecteur voit. - -**On n'abandonne rien en pratique.** Tous les sites d'appel de ce chemin passent des valeurs que le -compilateur a prouvées non-nulles. La garde défendait contre un cas que le compilateur refuse déjà, -au prix de masquer de vrais échecs si elle se déclenchait. - -## Alternatives considérées - -### Garder les gardes sur les types helpers - -Ce que le code faisait avant cet ADR, et ce que la convention imposait. Cela recrée le masquage -qu'ADR-0045 existe pour empêcher, à une trame de là où ADR-0045 l'interdit. Rejeté sur le fond, pas -par confort : la garde n'est pas seulement redondante, elle est nuisible dans la seule circonstance -où elle s'exécuterait. - -### Inférer l'exemption depuis l'usage - -« Exempter un type dont tous les appelants sont des factories d'exception » sonne rigoureux et n'est -pas implémentable depuis les métadonnées de réflexion, qui voient des signatures et non des graphes -d'appel. Tout substitut — nommage, espace de noms, assignabilité — serait une supposition, et une -supposition qui retire silencieusement une garde vaut moins que pas de règle. - -### Rendre le type helper privé à l'exception - -Un type imbriqué privé est déjà hors du périmètre de la convention : aucun ADR n'aurait été -nécessaire. Mais alors le site de levée ne peut plus en construire, et la factory revient à cinq -chaînes éparses dans un ordre que rien ne vérifie — la signature qu'ADR-0063 a rejetée. L'exemption -existe pour que le site d'appel reste lisible. - -### Replier la paire dans le message au site d'appel - -Composer la phrase au site de levée supprime le type et la question avec, et réinstalle la prose au -milieu du code métier qu'ADR-0063 a été écrit pour faire cesser. Rejeté là-bas, rejeté ici. - -## Conséquences - -### Positives - -* Le danger qu'ADR-0045 avait identifié est désormais couvert partout où il se produit, au lieu de - partout où un type hérite d'`Exception`. -* Un site de levée peut construire les arguments qui rendent son message lisible sans que le type - helper réintroduise le masquage une trame d'appel plus tôt. -* L'exemption se cherche au grep. Un marqueur, une raison écrite dessus, et un relecteur voit tous les - types qui s'en réclament. - -### Négatives - -* L'exemption devient quelque chose qu'un contributeur applique, là où elle découlait du seul système - de types. Elle coûte une décision au moment d'écrire le type. -* L'exigence de garde, qui ne souffrait aucune exception hors des types d'exception, en souffre - désormais une de plus — une règle à deux exemptions s'énonce un peu moins simplement qu'à une. - -### Risques - -* Un marqueur posé sur un type qui n'est *pas* confiné au chemin d'échec retirerait silencieusement - une vraie exigence de garde, et aucun test ne peut l'attraper : le marqueur est cru sur parole. - Parades : il est `internal`, il ne vise que classes et structures, et sa raison est écrite sur - l'attribut, si bien qu'un relecteur rencontre l'argument avant l'usage. -* Le compilateur porte désormais ce que portait la garde. C'est plus fort pour les appelants internes, - mais cela signifie qu'un appelant par réflexion, ou qui force un `null!`, atteindrait le - constructeur sans contrôle — compromis accepté, puisqu'un tel appelant a déjà quitté le contrat. - -## Références - -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.fr.md) — la règle que celui-ci - remplace et reprend, et l'exemption qu'il élargit. -* [ADR-0063](0063-throw-the-library-s-own-exceptions-through-named-factories.fr.md) — les factories - nommées dont les value objects ont rendu l'exemption étroite insuffisante. diff --git a/doc/handwritten/for-maintainers/adr/0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md b/doc/handwritten/for-maintainers/adr/0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md deleted file mode 100644 index 2c62911c..00000000 --- a/doc/handwritten/for-maintainers/adr/0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md +++ /dev/null @@ -1,127 +0,0 @@ -# ADR-0064 | Exempt the whole failure-reporting path from the null-guard convention - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-30 -**Accepted:** 2026-07-30 -**Decision Makers:** Reefact - -Supersedes [ADR-0045](0045-guard-public-and-internal-arguments-against-null.md). - -## Context - -ADR-0045 requires every public and internal member to reject a `null` non-nullable reference -argument with `ArgumentNullException`, and enforces it with a reflection convention in -`JustDummies.UnitTests` that discovers members rather than naming them. It exempted exception types, -for a reason worth repeating: their constructors run while an error is being handled, so throwing -an `ArgumentNullException` there would replace the failure being reported with a failure about -reporting it, and the original would be lost. - -That exemption is keyed on *being* an `Exception`. The hazard is not. - -ADR-0063 made the library throw through factories named after the failure, and one of those -factories needed to say which of two constraints a conflict should blame. Five loose strings in an -order nothing checks was the wrong signature, so the pair — a constraint and what it claims — became -a small type, `ConstraintClaim`. It is built at the throw site, as an argument to the exception -factory: - -```csharp -throw ConflictingAnyConstraintException.Contradicts(applying, - ConstraintClaim.Of(_exactConstraint!, $"already fixes the count at {V(exact)}"), - ConstraintClaim.Of(_minConstraint!, $"already requires at least {Elements(_min)}")); -``` - -`ConstraintClaim` is not an `Exception`, so the convention inspected it and failed the build until it -guarded its arguments. Adding those guards satisfied the convention and recreated precisely what -ADR-0045 forbids, one call frame earlier: a `null` would now surface as `ArgumentNullException` from -a helper instead of as the conflict the code was reporting. - -The convention was right that the rule as written applied. The rule as written was drawn one frame -too narrow. - -## Decision - -ADR-0045's rule stands in full, with its exemption widened from exception types to any type that -exists only to build one of the library's exceptions, declared by an internal marker the reflection -convention reads rather than inferred from usage. - -## Rationale - -**The hazard belongs to the path, not to the base type.** What makes a guard harmful there is *when* -it runs — while a failure is being reported — and that is a property of how the type is used, not of -what it derives from. A rule keyed on `: Exception` catches the common case and misses the rest, and -the rest is exactly what ADR-0063's factories create. - -**A marker keeps the exemption honest.** The alternative is inference, and inference on this would -have to guess: "is only used by exception factories" is not a property a reflection test can -establish, and any approximation of it would either miss cases or silently exempt types that should -be guarded. A marker is one line, greppable, and wrong only if someone writes it wrongly — which a -reviewer can see. - -**Nothing is actually given up.** Every call site on this path passes values the compiler has proven -non-null. The runtime guard was defending against a case the compiler already rejects, at the price -of masking real failures if it ever fired. - -## Alternatives Considered - -### Keep the guards on the helper types - -What the code did before this ADR, and what the convention forced. It recreates the masking ADR-0045 -exists to prevent, one frame away from where ADR-0045 forbids it. Rejected on the merits, not on -convenience: the guard is not merely redundant, it is harmful in the only circumstance it would ever -run. - -### Infer the exemption from usage - -"Exempt a type all of whose callers are exception factories" sounds principled and is not -implementable from reflection metadata, which sees signatures rather than call graphs. Any proxy — -naming, namespace, assignability — would be a guess, and a guess that silently removes a guard is -worse than no rule. - -### Make the helper type private to the exception - -A nested private type is already out of the convention's scope, so no ADR would have been needed. -But then the throw site cannot build one, and the factory is back to five loose strings in an order -nothing checks — the signature ADR-0063 rejected. The exemption exists so the call site can stay -readable. - -### Fold the pair into the message at the call site - -Composing the sentence at the throw site removes the type and the question with it, and reinstates -the prose-in-business-code ADR-0063 was written to end. Rejected there, rejected here. - -## Consequences - -### Positive - -* The hazard ADR-0045 identified is now covered wherever it occurs, instead of wherever a type - happens to derive from `Exception`. -* A throw site can build the arguments that make its message readable without the helper type - reintroducing the masking one call frame earlier. -* The exemption is greppable. One marker, one reason written on it, and a reviewer can see every - type that claims it. - -### Negative - -* The exemption is now something a contributor can apply, where before it followed from the type - system alone. It costs a decision at the point of writing the type. -* The guard requirement, which held with no exception outside exception types, now holds with one - more — a rule with two exemptions is marginally harder to state than a rule with one. - -### Risks - -* A marker on a type that is *not* confined to the failure path would silently drop a real guard - requirement, and no test can catch that — the marker is trusted by construction. Mitigations: it is - `internal`, it applies to classes and structs only, and its reason is written on the attribute so a - reviewer meets the argument before the usage. -* The compiler now carries what the guard carried. That is stronger for internal callers, but it does - mean a reflective or `null!`-defeated caller would reach the constructor unchecked — an accepted - trade, since such a caller has already left the contract. - -## References - -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.md) — the rule this supersedes - and restates, and the exemption this widens. -* [ADR-0063](0063-throw-the-library-s-own-exceptions-through-named-factories.md) — the named - factories whose value objects made the narrow exemption insufficient. diff --git a/doc/handwritten/for-maintainers/adr/0065-carry-a-declared-constraint-as-a-value-object.fr.md b/doc/handwritten/for-maintainers/adr/0065-carry-a-declared-constraint-as-a-value-object.fr.md deleted file mode 100644 index 2fab9a57..00000000 --- a/doc/handwritten/for-maintainers/adr/0065-carry-a-declared-constraint-as-a-value-object.fr.md +++ /dev/null @@ -1,192 +0,0 @@ -# ADR-0065 | Porter une contrainte déclarée comme objet-valeur, non comme son texte rendu - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0065-carry-a-declared-constraint-as-a-value-object.md) - -**Statut :** Accepté -**Proposé :** 2026-07-30 -**Accepté :** 2026-07-31 -**Décideurs :** Reefact - -## Contexte - -Une contradiction entre deux contraintes échoue à la déclaration, avec un message nommant les deux -côtés — `Cannot apply Between(0, 100) because GreaterThan(200) is already defined.` Nommer la -contrainte que l'appelant a écrite, dans l'orthographe qu'il a écrite, fait partie du contrat de la -bibliothèque : une contradiction dans l'`Arrange` d'un test est un défaut du test et doit se lire -comme tel. ADR-0063 fait passer ces levées par des factories nommées d'après l'échec. - -Jusqu'à cette décision, une contrainte atteignait ces messages sous forme de chaîne assemblée au site -qui la déclarait. Il y en avait environ 290, répartis sur une trentaine de fichiers : les méthodes -fluides des générateurs, les quatre moteurs d'intervalle, les spécifications de chaîne, de -collection, de comptage et d'URI. Trois formes revenaient — un nom seul, un nom avec ses arguments -rendus, et un nom dont la bibliothèque ne doit pas rendre les arguments parce que le type du pool est -opaque et que son `ToString` appartient à l'appelant. Chaque site écrivait ses propres parenthèses. - -Trois propriétés découlent de cet arrangement, et ce sont des faits sur le code tel qu'il était : - -* L'orthographe n'était pas liée à la méthode qu'elle nommait. Renommer une méthode publique laissait - ses diagnostics en arrière, et un nom mal orthographié était un littéral qui compilait. -* Une spécification ne fait pas que rendre une contrainte ; elle les **compare**. Une vingtaine de - comparaisons décident si une seconde déclaration est une redéclaration inoffensive — le même appel - avec les mêmes arguments, qui rend la spécification inchangée — ou un vrai conflit. Certaines - étaient écrites en comparaison ordinale de chaînes, d'autres avec `==`. -* Plusieurs points d'entrée de moteur prennent une contrainte à côté d'autres chaînes — un nom de - type, une borne rendue, une clause d'épuisement — sans rien pour les distinguer que leur position. - -Deux autres faits pèsent sur la forme de la solution. Construire une exception ne doit jamais lever, -ce que consigne ADR-0064 et pourquoi le chemin de report d'échec est exempté des gardes d'arguments. -Et le `ConstraintClaim` d'ADR-0063 apparie un sujet blâmé avec ce qu'il affirme : ce sujet est -généralement une contrainte que l'appelant a écrite, mais pas toujours — une partie d'une forme peut -être blâmée aussi, et ce sont des phrases que la bibliothèque compose. - -Le dépôt a déjà vécu ceci avec des règles que seul un lecteur applique : ADR-0056 consigne une règle -de type explicite qui avait dérivé à 203 violations tant qu'elle vivait dans un fichier de réglages -sur lequel rien ne pouvait agir. - -## Décision - -Une contrainte déclarée est portée dans toute la bibliothèque comme un objet-valeur qui se rend -lui-même, jamais comme le texte qu'il rend. - -## Justification - -La ponctuation qui fait lire une contrainte comme un appel appartient à un seul endroit. Écrite à 290 -sites, c'est 290 occasions de diverger, et une divergence dans un diagnostic est invisible jusqu'à ce -que quelqu'un lise le message qui s'est trompé. - -Lier le nom à la méthode par `nameof` convertit deux classes de défauts en échecs de compilation. Un -renommage emporte désormais ses diagnostics au lieu de les laisser périmés en silence, et une faute -d'orthographe cesse d'être un littéral qui compile. C'est le même geste qu'argumente ADR-0056 : une -règle que le compilateur peut exprimer doit l'être là plutôt que confiée à l'attention, puisque -l'attention est précisément ce dont on a montré qu'elle échouait. - -L'égalité doit appartenir au type plutôt qu'à chaque site de comparaison, parce que la comparaison -porte du comportement : c'est elle qui sépare une redéclaration qui doit être un no-op d'une qui doit -entrer en conflit. Définir `==` fait partie de la décision et non du confort — ces comparaisons sont -écrites avec, et un type référence sans opérateur compare des identités en silence, transformant -chaque redéclaration légitime en conflit sans rien dans le compilateur ni dans le système de types -pour l'attraper. C'est le seul mode de défaillance de ce domaine qu'aucune autre garde n'aurait -trouvé. - -Rendre au moment où la contrainte est déclarée, plutôt qu'au moment où un message est composé, est ce -qui rend le type sûr sur le chemin que protège ADR-0064. Une contrainte est citée pendant qu'une -exception se construit ; si la citer pouvait composer quoi que ce soit, elle pourrait échouer là. -Relire un texte produit sur le chemin qui a réussi, non. - -Typer la seule contrainte appliquée n'aurait pas suffi. Les comparaisons opposent la contrainte -appliquée à celle qu'une spécification a enregistrée : les deux côtés doivent donc être du même type, -sinon la comparaison se dégrade en quelque chose de plus faible sans le dire. Les épingles stockées -portent donc le type aussi, et les factories d'exception qui les citent l'acceptent — ce qui ferme la -surface : dès que tout paramètre signifiant « une contrainte » a le type, une contrainte ne peut plus -s'écrire en littéral nulle part dans la bibliothèque. - -L'emplacement du sujet de `ConstraintClaim` reste capable de porter une phrase, parce qu'un sujet -blâmé n'est réellement pas toujours une contrainte. Nommer ce cas plutôt que laisser une phrase -passer par l'emplacement de contrainte préserve le sens de cet emplacement, et une phrase ne porte -pas de contrainte — ce qui la rend précisément jamais égale à celle qu'on applique, la comparaison -dont dépend le choix du blâme. - -Le coût accepté est un changement large et mécanique : l'état stocké de chaque moteur et chaque -méthode fluide ont bougé d'un coup, parce que la signature d'un moteur partagé ne peut pas changer -pour un seul appelant. Il a été pris par tranches, chacune compilant et passant seule. - -## Alternatives considérées - -### Conserver les chaînes et ajouter une convention de nommage - -Considérée parce qu'elle ne coûte rien à adopter et laisse chaque site d'appel tel quel. - -Rejetée parce que c'est l'arrangement qui existait déjà, et que les propriétés qui lui manquent sont -celles qui comptent : une convention ne peut pas faire suivre un renommage, ni faire échouer le build -sur une faute, ni donner son sens à une comparaison. ADR-0056 consigne ce qu'il advient d'une règle -de ce genre dans ce dépôt quand rien ne peut agir dessus. - -### Ajouter un analyseur vérifiant la forme des littéraux - -Considérée parce que le dépôt livre déjà des analyseurs de première main (ADR-0044) et en emploie un -là où le système de types n'atteint pas (ADR-0059). - -Rejetée parce que le système de types *atteint* ici. Un analyseur vérifierait qu'un littéral ressemble -à un appel tout en le laissant littéral — il ne pourrait ni lier l'orthographe à la méthode, ni -donner sa sémantique à la comparaison de redéclaration. ADR-0059 emploie un analyseur là où aucun -type n'exprime la règle ; ici un type l'exprime. - -### En faire une structure - -Considérée pour l'allocation sur un chemin qui s'exécute une fois par contrainte déclarée. - -Rejetée sous la règle permanente du dépôt selon laquelle une valeur portant un invariant est une -classe : une structure expose un constructeur sans paramètre produisant une instance ayant contourné -toute factory. Le même raisonnement que `ConstraintClaim` énonce pour lui-même. - -### Rendre paresseusement, en composant le texte quand un message le demande - -Considérée parce qu'une contrainte n'atteignant jamais un conflit ne serait alors jamais rendue, et -la plupart ne l'atteignent pas. - -Rejetée parce que le moment où une contrainte *est* rendue est celui où une exception se construit, -soit le seul endroit où la bibliothèque ne doit pas faire un travail qui peut échouer (ADR-0064). -Échanger une garantie sur le chemin d'échec contre une allocation sur le chemin de succès va dans le -mauvais sens. - -### Ne typer que la contrainte appliquée, en laissant les stockées en chaînes - -Considérée comme un changement plus petit atteignant l'essentiel du bénéfice. - -Rejetée parce que les deux sont comparées l'une à l'autre. Laisser un côté en chaîne force soit un -rendu à chaque comparaison — réintroduisant le texte que la décision supprime — soit une comparaison -signifiant moins qu'avant. - -## Conséquences - -### Positives - -* L'orthographe d'une contrainte suit la méthode qu'elle nomme ; un renommage emporte les diagnostics. -* Un nom de contrainte mal orthographié ou inventé est un échec de build, non un message que - personne ne lit avant qu'il soit faux. -* Redéclaration contre conflit est une propriété du type, décidée une fois au lieu de vingt. -* Les parenthèses existent une fois. -* Des paramètres voisins jusque-là interchangeables sont maintenant distinguables par type. -* Citer une contrainte dans un message ne peut pas échouer, par construction et non par inspection. -* Un littéral de contrainte ne peut plus s'écrire nulle part dans la bibliothèque ; le compilateur le - refuse. - -### Négatives - -* Un changement large : chaque générateur, l'état stocké de chaque moteur et les factories - d'exception ont bougé. -* Un second petit objet-valeur vit à côté de `ConstraintClaim` dans le même domaine, et un lecteur - doit les distinguer — une contrainte, contre un sujet apparié à ce qu'il affirme. -* L'égalité, ses opérateurs et leur couverture sont désormais à la charge du type. - -### Risques - -* Un sujet blâmé n'est pas toujours une contrainte, donc une forme « phrase » subsiste. Un - contributeur pourrait y faire passer une vraie contrainte et perdre le typage pour ce message. - Atténué en nommant la factory de phrase pour le cas qui la justifie, plutôt que de laisser un - emplacement stringly-typed acceptant les deux. -* Les générateurs rendent encore leurs propres arguments via des helpers par type : les *arguments* - d'une contrainte restent donc des chaînes assemblées localement. La surface est plus petite qu'avant - et spécifique au type par nature, mais c'est là qu'une incohérence de rendu pourrait encore - apparaître. - -## Actions de suivi - -* Envisager de dédupliquer les rendus d'arguments par générateur, quasi identiques d'un générateur - scalaire à l'autre et ne divergeant que là où un type rend réellement différemment. -* Réexaminer si le sujet de `ConstraintClaim` et ce type doivent fusionner, une fois les cas de - phrase mieux compris. - -## Références - -* [ADR-0040](0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md) — quelle - suite possède la formulation d'un message. -* [ADR-0044](0044-ship-justdummies-analyzers.fr.md) — analyseurs de première main. -* [ADR-0056](0056-state-the-coding-rules-where-an-agent-can-act-on-them.fr.md) — une règle sur - laquelle rien ne peut agir dérive. -* [ADR-0059](0059-guard-the-recipe-versus-value-boundary-with-analyzers.fr.md) — analyseurs là où le - système de types n'atteint pas. -* [ADR-0063](0063-throw-the-library-s-own-exceptions-through-named-factories.fr.md) — factories de - levée nommées, et `ConstraintClaim`. -* [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.fr.md) — - construire un report d'échec ne doit pas échouer. diff --git a/doc/handwritten/for-maintainers/adr/0065-carry-a-declared-constraint-as-a-value-object.md b/doc/handwritten/for-maintainers/adr/0065-carry-a-declared-constraint-as-a-value-object.md deleted file mode 100644 index 90b972ba..00000000 --- a/doc/handwritten/for-maintainers/adr/0065-carry-a-declared-constraint-as-a-value-object.md +++ /dev/null @@ -1,185 +0,0 @@ -# ADR-0065 | Carry a declared constraint as a value object, not as its rendered text - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0065-carry-a-declared-constraint-as-a-value-object.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-30 -**Accepted:** 2026-07-31 -**Decision Makers:** Reefact - -## Context - -A contradiction between two constraints fails at declaration, with a message naming both sides — -`Cannot apply Between(0, 100) because GreaterThan(200) is already defined.` Naming the constraint the -caller wrote, in the spelling they wrote it, is part of the library's contract: a contradiction in a -test's `Arrange` is a defect of the test and must read as one. ADR-0063 routes those throws through -factories named after the failure. - -Until this decision, a constraint reached those messages as a string assembled at the site that -declared it. There were around 290 such sites across some thirty files: the generators' fluent -methods, the four interval engines, the string, collection, count and URI specifications. Three -shapes recurred — a name alone, a name with its arguments rendered, and a name whose arguments the -library must not render because the pooled type is opaque and its `ToString` belongs to the caller. -Each site wrote its own parentheses. - -Three properties follow from that arrangement, and they are facts about the code as it stood: - -* The spelling was not tied to the method it named. Renaming a public method left its diagnostics - behind, and a misspelled name was a string literal that compiled. -* A specification does not only render a constraint; it **compares** them. Around twenty comparisons - decide whether a second declaration is a harmless redeclaration — the same call with the same - arguments, which returns the specification untouched — or a genuine conflict. Some were written as - ordinal string comparison, some with `==`. -* Several engine entry points take a constraint next to other strings — a type name, a rendered - bound, an exhaustion clause — with nothing distinguishing them but their position. - -Two further facts bear on the shape of the solution. Building an exception must never throw, which -ADR-0064 records and which the failure-reporting path is exempted from argument guards for. -And ADR-0063's `ConstraintClaim` pairs a blamed subject with what it claims: that subject is usually -a constraint the caller wrote, but not always — a part of a shape can be blamed too, and those are -phrases the library composes. - -The repository has been here before with rules that only a reader enforces: ADR-0056 records an -explicit-type rule that drifted to 203 violations while it lived in a settings file nothing could -act on. - -## Decision - -A declared constraint is carried through the library as a value object that renders itself, never as -the text it renders to. - -## Rationale - -The punctuation that makes a constraint read as a call belongs in one place. Written at 290 sites it -is 290 chances to diverge, and divergence in a diagnostic is invisible until someone reads the -message that got it wrong. - -Tying the name to the method through `nameof` converts two classes of defect into build failures. A -rename now carries its diagnostics along instead of silently leaving them stale, and a misspelling -stops being a literal that compiles. This is the same move ADR-0056 argues for: a rule the compiler -can express should be expressed there rather than trusted to attention, because attention is exactly -what was shown to fail. - -Equality has to belong to the type rather than to each comparison site, because the comparison -carries behaviour: it is what separates a redeclaration that must be a no-op from one that must -conflict. Defining `==` is part of the decision rather than a convenience — those comparisons are -written with it, and a reference type without it compares identities in silence, turning every -legitimate redeclaration into a conflict with nothing in the compiler or the type system to catch -it. That is the one failure mode in this area that no other guard would have found. - -Rendering when the constraint is declared, rather than when a message is composed, is what makes the -type safe on the path ADR-0064 protects. A constraint is quoted while an exception is being built; -if quoting it could compose anything, it could fail there. Reading back text produced on the path -that succeeded cannot. - -Typing the applied constraint alone would not have been enough. The comparisons are between the -constraint being applied and the one a specification recorded, so both sides must be the same type -or the comparison degrades to something weaker without saying so. The stored pins therefore carry -the type too, and the exception factories that quote them accept it — which is what closes the -surface: once every parameter that means "a constraint" has the type, a constraint can no longer be -written as a literal anywhere in the library. - -`ConstraintClaim`'s subject slot stays able to hold a phrase, because a blamed subject genuinely is -not always a constraint. Naming that case rather than letting a phrase pass through the constraint -slot keeps the slot's meaning intact, and a phrase carries no constraint — which is what makes it -never compare equal to the one being applied, the comparison the blame choice turns on. - -The cost accepted is a wide, mechanical change: every engine's stored state and every fluent method -moved at once, because a shared engine's signature cannot change for one caller. It was taken in -tranches, each compiling and passing on its own. - -## Alternatives Considered - -### Keep the strings and add a naming convention - -Considered because it costs nothing to adopt and leaves every call site as it is. - -Rejected because it is the arrangement that already existed, and the properties it lacks are the -ones that matter: a convention cannot make a rename carry, cannot make a misspelling fail the build, -and cannot give a comparison its meaning. ADR-0056 records what happens to a rule of this kind in -this repository when nothing can act on it. - -### Add an analyzer that checks the literals' shape - -Considered because the repository already ships first-party analyzers (ADR-0044) and reaches for one -where the type system cannot (ADR-0059). - -Rejected because the type system *can* reach this. An analyzer would verify that a literal looks -like a call while leaving it a literal — it could not tie the spelling to the method, and it could -not give the redeclaration comparison its semantics. ADR-0059 reaches for an analyzer where no type -expresses the rule; here one does. - -### Make it a struct - -Considered for the allocation on a path that runs once per declared constraint. - -Rejected under the repository's standing rule that a value enforcing an invariant is a class: a -struct exposes a parameterless constructor yielding an instance that bypassed every factory. The -same reasoning `ConstraintClaim` states for itself. - -### Render lazily, composing the text when a message asks for it - -Considered because a constraint that never reaches a conflict would then never be rendered, and most -do not. - -Rejected because the moment a constraint *is* rendered is the moment an exception is being built, -which is the one place the library must not do work that can fail (ADR-0064). Trading a guarantee on -the failure path for an allocation on the success path is the wrong direction. - -### Type only the constraint being applied, leaving the stored ones as strings - -Considered as a smaller change reaching most of the benefit. - -Rejected because the two are compared against each other. Leaving one side a string either forces a -rendering at every comparison — reintroducing the text the decision removes — or lets the comparison -mean something weaker than it did. - -## Consequences - -### Positive - -* A constraint's spelling follows the method it names; renaming carries the diagnostics along. -* A misspelled or invented constraint name is a build failure rather than a message no one reads - until it is wrong. -* Redeclaration-versus-conflict is a property of the type, decided once instead of at twenty sites. -* The parentheses exist once. -* Adjacent parameters that used to be interchangeable strings are now distinguishable by type. -* Quoting a constraint into a message cannot fail, by construction rather than by inspection. -* A constraint literal cannot be written anywhere in the library; the compiler refuses it. - -### Negative - -* A wide change: every generator, every engine's stored state, and the exception factories moved. -* A second small value type lives beside `ConstraintClaim` in the same area, and a reader must tell - the two apart — a constraint, versus a subject paired with what it claims. -* Equality, its operators and their coverage are now something the type owns and must keep. - -### Risks - -* A blamed subject is not always a constraint, so a phrase form remains. A contributor could route a - real constraint through it and lose the typing for that message. Mitigated by naming the phrase - factory for the case it exists for, rather than leaving a stringly-typed slot that accepts both. -* The generators still render their own arguments through per-type helpers, so the *arguments* of a - constraint remain strings assembled locally. That is a smaller surface than before and is - type-specific by nature, but it is where a rendering inconsistency could still appear. - -## Follow-up Actions - -* Consider deduplicating the per-generator argument renderers, which are near-identical across the - scalar generators and diverge only where a type genuinely renders differently. -* Revisit whether `ConstraintClaim`'s subject and this type should merge once the phrase cases are - better understood. - -## References - -* [ADR-0040](0040-split-the-justdummies-test-bed-between-example-and-property-suites.md) — which - suite owns a message's wording. -* [ADR-0044](0044-ship-justdummies-analyzers.md) — first-party analyzers. -* [ADR-0056](0056-state-the-coding-rules-where-an-agent-can-act-on-them.md) — a rule nothing can act - on drifts. -* [ADR-0059](0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) — analyzers where the - type system cannot reach. -* [ADR-0063](0063-throw-the-library-s-own-exceptions-through-named-factories.md) — named throw - factories, and `ConstraintClaim`. -* [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md) — - building a failure report must not fail. diff --git a/doc/handwritten/for-maintainers/adr/0066-declare-a-value-object-and-enforce-its-identity.fr.md b/doc/handwritten/for-maintainers/adr/0066-declare-a-value-object-and-enforce-its-identity.fr.md deleted file mode 100644 index 8dd532e1..00000000 --- a/doc/handwritten/for-maintainers/adr/0066-declare-a-value-object-and-enforce-its-identity.fr.md +++ /dev/null @@ -1,151 +0,0 @@ -# ADR-0066 | Déclarer un objet-valeur par un attribut, et faire respecter son identité par convention - -🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0066-declare-a-value-object-and-enforce-its-identity.md) - -**Statut :** Accepté -**Proposé :** 2026-07-30 -**Accepté :** 2026-07-31 -**Décideurs :** Reefact - -## Contexte - -La bibliothèque porte trois valeurs faites pour être comparées ou transportées par leur contenu plutôt que par -l'instance qu'on tient : une contrainte déclarée (ADR-0065), la paire d'un sujet blâmé et de ce qu'il affirme, et ce -dont un tirage échoué a besoin pour être rejoué. Deux des trois se décrivent dans leurs propres remarques comme des -valeurs comme toutes les autres de ce dépôt, et sont immuables, à constructeur privé atteint par des factories. - -Une seule des trois portait une identité de valeur. Les deux autres répondaient « est-ce le même ? » par référence, -et en silence : un type référence compare par identité tant que personne n'écrit une autre réponse, ce qui ne lève -aucun avertissement du compilateur, ne fait échouer aucun test, et ne se lit pas du tout pour un relecteur. Celle qui -avait son identité l'avait parce que du code la comparait avec `==`, ce qui forçait la question ; rien ne la forçait -pour les deux autres, et le trou est parti en production. - -L'opérateur `==` est la moitié qui se dégrade le plus discrètement. Un type auquel manque `Equals` en manque au moins -visiblement pour qui lit le type ; un type auquel manquent les opérateurs compile encore à chaque `a == b`, et y -compare des références. - -L'immuabilité ne désigne pas une valeur ici. Les générateurs et les spécifications sont immuables aussi — ils sont -reconstruits plutôt que mutés à chaque contrainte — mais deux générateurs identiquement contraints sont deux -recettes, pas une valeur ; les comparer par contenu répondrait à une question qui n'a pas de sens pour eux. - -Le dépôt traite déjà une règle de cette forme par un marqueur plus une convention par réflexion : la convention de -garde null d'ADR-0045 découvre les membres au lieu de les nommer, et ADR-0064 déclare son exemption par -`[BuiltOnTheFailurePath]` plutôt que de l'inférer. ADR-0056 consigne ce qu'il advient d'une règle dans ce dépôt quand -rien ne peut agir dessus : une règle de type explicite a dérivé à 203 violations tant qu'elle vivait là où seul un -lecteur pouvait l'appliquer. - -## Décision - -Un type dont les instances sont des valeurs se déclare par `[ValueObject]`, et une convention par réflexion tient -chaque type marqué à une identité de valeur complète et à se rendre lui-même pour un lecteur. - -## Justification - -Le trou que cela ferme est invisible par construction, ce qui fait de la convention le bon instrument plutôt que -l'attention ou la relecture. Rien, chez une valeur privée de son égalité, ne paraît fautif : le type est immuable, -ses factories sont nommées, ses remarques disent que c'est une valeur. Seule la question posée révèle la réponse, et -deux valeurs sur trois sont parties sans que personne ne la pose. - -Le marqueur gagne sa place parce que la règle ne peut pas être déduite. Détecter les valeurs par l'immuabilité -embarquerait les générateurs et les spécifications et leur exigerait une égalité qui les décrirait mal. Les déduire -d'un motif de nommage serait pire : cela ferait reposer l'application sur une convention pas moins fragile que celle -qu'on applique. Déclarer est une décision qu'un humain prend une fois par type, et une décision est exactement ce -qu'un attribut consigne — le raisonnement même qu'ADR-0064 a appliqué à sa propre exemption plutôt que de l'inférer -de la forme d'un type. - -Faire respecter la paire d'opérateurs est ce qui rentabilise le mieux le coût. C'est le seul membre de l'ensemble -dont l'absence change le comportement sans changer le fait que le code compile : donc celui qu'un relecteur est le -moins capable d'attraper, et une convention le plus. - -La convention vérifie la structure, et s'y arrête délibérément. Savoir si deux instances égales hachent pareil, et -si les champs choisis pour l'égalité sont les bons, sont des questions sur le sens d'un type précis auxquelles -aucune réflexion sur sa forme ne peut répondre ; elles appartiennent aux tests de ce type. Ce que la réflexion peut -trancher — scellé, immuable, et l'ensemble des membres présent — est précisément la moitié qui disparaît quand -personne ne regarde, et elle ne peut pas être satisfaite par accident. - -Le scellement est exigé plutôt qu'encouragé parce qu'une valeur non scellée ne peut pas garder son égalité -symétrique : une sous-classe portant un champ de plus est égale à sa base dans un sens et inégale dans l'autre, ce -qui rompt le contrat dont dépend tout type de collection. Rejeter une structure marquée redit, là où c'est -applicable, la règle permanente selon laquelle une valeur gardant un invariant est une classe : une structure expose -un constructeur sans paramètre produisant une instance ayant contourné toute factory. - -## Alternatives considérées - -### Exiger l'identité de tout type immuable, sans marqueur - -Considérée parce qu'elle ne demande rien à déclarer et ne peut pas être oubliée sur un type neuf. - -Rejetée parce qu'elle n'est pas vraie de tout type immuable ici. Les générateurs et les spécifications sont -immuables et ne sont pas des valeurs : la règle leur imposerait donc une égalité dénuée de sens, ou exigerait une -liste d'exclusion — qui est un marqueur inversé, et qui grossit en silence à mesure que la bibliothèque grandit. - -### Déduire les valeurs d'une convention de nommage ou d'espace de noms - -Considérée parce qu'elle ne demanderait ni attribut ni liste. - -Rejetée parce qu'elle ferait reposer l'application sur une convention exactement aussi peu appliquée que celle -qu'elle remplace. Un type renommé hors du motif quitterait la convention en silence, ce qui est précisément -l'échec que cette décision existe pour empêcher. - -### S'appuyer sur un analyseur plutôt qu'un test - -Considérée parce que le dépôt livre des analyseurs de première main (ADR-0044) et en emploie un là où le système de -types ne peut pas exprimer une règle (ADR-0059). - -Rejetée parce que la règle porte sur les types propres de la bibliothèque, non sur la façon dont un consommateur -écrit son code. Un analyseur est le bon instrument quand le diagnostic doit atteindre le build d'un consommateur ; -ici le public est ce dépôt, et sa propre suite applique déjà des conventions de cette forme par réflexion. - -### Utiliser des `record` pour ces valeurs - -Considérée parce qu'un `record` génère tout l'ensemble d'identité, donc le trou ne pourrait pas survenir. - -Rejetée parce que l'égalité générée porte sur tous les membres, ce qui n'est pas toujours la bonne réponse : l'une -de ces valeurs compare une contrainte **en plus** du texte qui la rend, précisément pour qu'une phrase se lisant -comme une contrainte ne soit pas prise pour elle. Un `record` ferait de plus du constructeur primaire un point -d'entrée public, là où ces types font délibérément passer la construction par des factories nommées. - -## Conséquences - -### Positives - -* Une valeur qui oublie son identité fait échouer un test au lieu de partir en production. -* La paire d'opérateurs — le membre dont l'absence est silencieuse — est appliquée comme le reste. -* Ce qu'est un type est déclaré là où le type est, et un lecteur l'apprend du type lui-même. -* Les types marqués sont énumérables : l'ensemble des valeurs de la bibliothèque est désormais une question qui a - une réponse. - -### Négatives - -* Une valeur nouvelle doit être marquée pour être couverte ; oublier le marqueur la laisse non vérifiée, et seul un - relecteur l'attrape. -* La convention contraint ses types marqués au-delà de l'égalité — scellé, immuable, classe — donc une valeur future - légitime qui devrait être autrement devrait argumenter plutôt que simplement différer. - -### Risques - -* La vérification structurelle peut se lire comme suffisante. Un type peut porter tout l'ensemble et comparer - quand même les mauvais champs ; la convention ne dit rien là-dessus, et sa propre documentation le dit plutôt que - de laisser le lecteur supposer l'inverse. -* Le marqueur peut être posé sur ce qui n'est pas une valeur, ce qui exigerait une égalité la décrivant mal. - Atténué par la seule relecture — l'attribut est une affirmation, et une affirmation fausse est une décision - fausse, pas une règle cassée. - -## Actions de suivi - -* Examiner si les valeurs de `FirstClassErrors` — qui portent déjà leurs identités — doivent se déclarer de la même - façon ; les deux assemblages ne peuvent pas partager l'attribut, JustDummies étant autonome par ADR-0011. - -## Références - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.fr.md) — JustDummies ne dépend de rien dans ce dépôt. -* [ADR-0044](0044-ship-justdummies-analyzers.fr.md) — analyseurs de première main. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.fr.md) — une convention qui découvre les membres - au lieu de les nommer. -* [ADR-0056](0056-state-the-coding-rules-where-an-agent-can-act-on-them.fr.md) — une règle sur laquelle rien ne peut - agir dérive. -* [ADR-0059](0059-guard-the-recipe-versus-value-boundary-with-analyzers.fr.md) — quand l'analyseur est l'instrument. -* [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.fr.md) — un marqueur - consignant une décision plutôt que de l'inférer. -* [ADR-0065](0065-carry-a-declared-constraint-as-a-value-object.fr.md) — la valeur dont l'égalité a forcé la - question. diff --git a/doc/handwritten/for-maintainers/adr/0066-declare-a-value-object-and-enforce-its-identity.md b/doc/handwritten/for-maintainers/adr/0066-declare-a-value-object-and-enforce-its-identity.md deleted file mode 100644 index 5080ce14..00000000 --- a/doc/handwritten/for-maintainers/adr/0066-declare-a-value-object-and-enforce-its-identity.md +++ /dev/null @@ -1,153 +0,0 @@ -# ADR-0066 | Declare a value object with an attribute, and enforce its identity by convention - -🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0066-declare-a-value-object-and-enforce-its-identity.fr.md) - -**Status:** Accepted -**Proposed:** 2026-07-30 -**Accepted:** 2026-07-31 -**Decision Makers:** Reefact - -## Context - -The library holds three values built to be compared or carried by their content rather than by which instance one -holds: a declared constraint (ADR-0065), the pair of a blamed subject and what it claims, and what a failed draw -needs in order to be replayed. Two of the three describe themselves in their own remarks as values like every other -in this repository, and are immutable with private constructors reached through factories. - -Only one of the three carried a value identity. The other two answered "is this the same one?" by reference, and did -so silently: a reference type compares by identity when nobody writes another answer, which raises no compiler -warning, fails no test, and reads as nothing at all to a reviewer. The one that had its identity had it because code -happened to compare it with `==`, which forced the question; nothing forced it for the other two, and the gap -shipped. - -The `==` operator is the half of this that degrades quietest. A type missing `Equals` is at least visibly missing it -to anyone reading the type; a type missing the operators still compiles at every `a == b` and compares references -there. - -Immutability does not identify a value here. The generators and the specifications are immutable too — they are -rebuilt rather than mutated on every constraint — but two identically constrained generators are two recipes, not -one value; comparing them by content would answer a question that has no meaning for them. - -The repository already meets a rule of this shape with a marker plus a reflection convention: ADR-0045's null-guard -convention discovers members rather than naming them, and ADR-0064 declares its exemption with -`[BuiltOnTheFailurePath]` rather than inferring it. ADR-0056 records what becomes of a rule in this repository when -nothing can act on it: an explicit-type rule drifted to 203 violations while it lived where only a reader could -enforce it. - -## Decision - -A type whose instances are values declares itself with `[ValueObject]`, and a reflection convention holds every -marked type to a full value identity and to rendering itself for a reader. - -## Rationale - -The gap this closes is invisible by construction, which is what makes a convention the right instrument rather than -attention or review. Nothing about a value missing its equality looks wrong: the type is immutable, its factories -are named, its remarks say it is a value. Only asking the question reveals the answer, and two of three values -shipped without anyone asking. - -The marker earns its place because the rule cannot be derived. Detecting values by immutability would sweep in the -generators and the specifications and demand of them an equality that would misstate what they are. Deriving them -from a naming pattern would be worse: it would depend on a convention no less fragile than the one being enforced. -Declaring is a decision a human makes once per type, and a decision is exactly what an attribute records — the same -reasoning ADR-0064 applied to its own exemption rather than inferring it from a type's shape. - -Enforcing the operator pair is the part that most repays the cost. It is the only member of the set whose absence -changes behaviour without changing whether the code compiles, so it is the one a reviewer is least able to catch and -a convention is most able to. - -The convention checks structure, and stops there deliberately. Whether two equal instances hash alike, and whether -the fields chosen for equality are the right ones, are questions about a specific type's meaning that no reflection -over its shape can answer; they belong to that type's own tests. What reflection can settle — sealed, immutable, and -the full member set present — is precisely the half that goes missing when nobody is looking, and it cannot be -satisfied by accident. - -Rendering is part of the contract for the same reason the rest is: a value that does not override `ToString` shows a -debugger the one thing its reader already knows — its type name — and nothing about that looks wrong either. The -repository had already settled the form, `[DebuggerDisplay]` forwarding to `ToString`, on the values in -`FirstClassErrors`; it was followed there by attention alone, and the values added since did not follow it. That is -the same drift this decision exists to stop, so the convention carries it rather than a reader. - -Sealedness is required rather than encouraged because an unsealed value cannot keep its equality symmetric: a -subclass carrying an extra field compares equal to its base in one direction and unequal in the other, which breaks -the contract every collection type relies on. Rejecting a marked struct restates, where it can be enforced, the -standing rule that a value guarding an invariant is a class: a struct exposes a parameterless constructor yielding -an instance that bypassed every factory. - -## Alternatives Considered - -### Require the identity of every immutable type, with no marker - -Considered because it needs nothing declared and cannot be forgotten on a new type. - -Rejected because it is not true of every immutable type here. The generators and the specifications are immutable -and are not values, so the rule would either force a meaningless equality on them or need an exclusion list — which -is a marker, inverted, and one that grows silently as the library does. - -### Infer values from a naming or namespace convention - -Considered because it would need no attribute and no list. - -Rejected because it would rest the enforcement on a convention exactly as unenforced as the one it replaces. A type -renamed out of the pattern would leave the convention silently, which is the failure this decision exists to -prevent. - -### Rely on an analyzer instead of a test - -Considered because the repository ships first-party analyzers (ADR-0044) and reaches for one where the type system -cannot express a rule (ADR-0059). - -Rejected because the rule is about the library's own types rather than about how a consumer writes code. An analyzer -is the right instrument when the diagnostic must reach a consumer's build; here the audience is this repository, and -its own suite already enforces conventions of this shape by reflection. - -### Use records for the values - -Considered because a record generates the whole identity set, so the gap could not occur. - -Rejected because the generated equality is over all members, which is not always the right answer — one of these -values compares a constraint alongside the text that renders it, precisely so that a phrase reading like a -constraint is not mistaken for it. A record would also make the primary constructor a public entry point, where -these types deliberately route construction through named factories. - -## Consequences - -### Positive - -* A value that forgets its identity fails a test instead of shipping. -* The operator pair — the member whose absence is silent — is enforced like the rest. -* What a type is, is declared where the type is, and a reader learns it from the type itself. -* Marked types are enumerable, so the set of values in the library is now a question with an answer. - -### Negative - -* A new value must be marked to be covered; forgetting the marker leaves it unchecked, and only a reviewer catches - that. -* The convention constrains its marked types beyond equality — sealed, immutable, class — so a legitimate future - value that needed to be otherwise would have to argue the point rather than simply differ. - -### Risks - -* Structural checking can read as sufficient. A type can carry the whole member set and still compare on the wrong - fields; the convention says nothing about that, and its own documentation says so rather than leaving the reader - to assume otherwise. -* The marker can be applied to something that is not a value, which would demand an equality that misstates it. - Mitigated only by review — the attribute is a claim, and a wrong claim is a wrong decision, not a broken rule. - -## Follow-up Actions - -* Consider whether the values in `FirstClassErrors` — which carry their identities already — should declare - themselves the same way; the two assemblies cannot share the attribute, since JustDummies is standalone by - ADR-0011. - -## References - -* [ADR-0011](0011-host-dummies-as-a-standalone-package.md) — JustDummies depends on nothing in this repository. -* [ADR-0044](0044-ship-justdummies-analyzers.md) — first-party analyzers. -* [ADR-0045](0045-guard-public-and-internal-arguments-against-null.md) — a convention that discovers members rather - than naming them. -* [ADR-0056](0056-state-the-coding-rules-where-an-agent-can-act-on-them.md) — a rule nothing can act on drifts. -* [ADR-0059](0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) — when an analyzer is the instrument. -* [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md) — a marker declaring a - decision rather than inferring it. -* [ADR-0065](0065-carry-a-declared-constraint-as-a-value-object.md) — the value whose equality forced the question. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 655babb7..54bd568f 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -214,60 +214,26 @@ Optional supporting material: | [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 JustDummies 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 | 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 | 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 | -| [ADR-0025](0025-generate-strings-from-a-home-grown-regular-subset.md) | Generate matching strings from a home-grown regular subset | Accepted | | [ADR-0026](0026-rebase-testing-arbitrary-values-on-dummies.md) | Rebase the testing package's arbitrary values on JustDummies | Accepted | | [ADR-0027](0027-repair-dependabot-pull-requests-within-a-risk-boundary.md) | Repair Dependabot pull requests within a risk boundary | Accepted | | [ADR-0028](0028-bridge-throwing-code-into-outcomes-through-a-guarded-try.md) | Bridge throwing code into outcomes through a guarded Try | Accepted | | [ADR-0029](0029-complete-the-outcome-try-async-surface-with-token-less-overloads.md) | Complete the Outcome.Try async surface with token-less overloads | Accepted | -| [ADR-0030](0030-draw-arbitrary-strings-from-an-explicit-terminal-set.md) | Draw arbitrary strings from an explicit, terminal value set | Superseded | -| [ADR-0031](0031-name-any-factories-after-their-clr-type.md) | Name Any's scalar factories after their CLR type | Accepted | -| [ADR-0032](0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) | Draw arbitrary values from an explicit, top-level choice pool | Accepted | -| [ADR-0033](0033-meet-string-exclusions-with-a-bounded-redraw.md) | Meet string exclusions with a bounded redraw | Accepted | | [ADR-0034](0034-require-a-scope-on-the-version-driving-commit-types.md) | Require a scope on the version-driving commit types | Accepted | -| [ADR-0035](0035-enforce-structural-any-conflicts-at-compile-time.md) | Enforce structural Any conflicts at compile time, value-dependent ones at run time | Accepted | -| [ADR-0036](0036-draw-lattice-constrained-scalars-on-the-grid.md) | Draw lattice-constrained scalars on the grid | Accepted | -| [ADR-0037](0037-vary-the-datetimeoffset-offset-dimension.md) | Vary the DateTimeOffset offset dimension | Superseded | -| [ADR-0038](0038-open-the-ambient-seed-scope-to-adapters.md) | Open the ambient seed scope to test-framework adapters | Accepted | -| [ADR-0039](0039-adapt-dummies-to-xunit-v3-through-a-companion-package.md) | Adapt JustDummies to xUnit v3 through a companion package | Accepted | -| [ADR-0040](0040-split-the-justdummies-test-bed-between-example-and-property-suites.md) | Split the JustDummies test bed between an example suite and a property suite | Accepted | -| [ADR-0041](0041-draw-flag-enum-combinations-behind-an-opt-in.md) | Draw flag-enum combinations behind an opt-in | Accepted | -| [ADR-0042](0042-serialize-draws-on-a-random-source.md) | Serialize draws on a random source, and scope reproducibility to the draw sequence | Accepted | | [ADR-0043](0043-gate-pull-requests-on-the-mutation-score-of-the-diff.md) | Gate pull requests on the mutation score of what they changed | Accepted | -| [ADR-0044](0044-ship-justdummies-analyzers.md) | Ship first-party JustDummies analyzers, and guard the reproducible async surface with them | Accepted | -| [ADR-0045](0045-guard-public-and-internal-arguments-against-null.md) | Guard public and internal arguments against null, enforced by a reflection convention | Superseded | | [ADR-0046](0046-make-the-per-pull-request-mutation-gate-advisory.md) | Make the per-pull-request mutation gate advisory; the weekly full sweep is the enforced bar | Accepted | -| [ADR-0047](0047-measure-justdummies-mutation-against-the-unit-suite-only.md) | Measure JustDummies mutation against the deterministic unit suite only; drop the FsCheck property suite from the oracle | Accepted | -| [ADR-0048](0048-guarantee-a-generated-regex-value-matches-by-bounded-redraw.md) | Guarantee a generated regex value matches its pattern, by bounded redraw | Accepted | -| [ADR-0049](0049-drop-the-justdummies-generator-from-the-per-pull-request-mutation-matrix.md) | Drop the JustDummies generator from the per-pull-request mutation matrix; its adapter and analyzers keep theirs, the weekly sweep keeps measuring it | Accepted | -| [ADR-0050](0050-let-a-size-maximum-cap-without-steering-the-draw.md) | Let a size maximum cap without steering the draw, and ceiling an explicitly demanded size | Accepted | -| [ADR-0051](0051-filter-the-datetimeoffset-pool-by-the-declared-offset.md) | Filter the DateTimeOffset pool by the declared offset; supersedes ADR-0037 | Accepted | -| [ADR-0052](0052-draw-arbitrary-numbers-within-an-ordinary-magnitude.md) | Draw arbitrary numbers within an ordinary magnitude; the integer generators keep their full range | Accepted | -| [ADR-0053](0053-unify-discrete-generation-in-one-ordinal-space.md) | Unify discrete generation in one ordinal space, with a dedicated engine only where the arithmetic substrate forces one | Accepted | -| [ADR-0054](0054-decide-a-constraint-surface-by-constructive-versus-rejective.md) | Decide a generator's constraint surface by constructive versus rejective, not by terminality; supersedes ADR-0030 | Accepted | | [ADR-0055](0055-enforce-the-style-rules-the-compiler-can-express.md) | Enforce the style rules the compiler can express, and keep the DotSettings authoritative for the rest | Accepted | | [ADR-0056](0056-state-the-coding-rules-where-an-agent-can-act-on-them.md) | State the coding rules where an agent can act on them, and check them at the edit | Accepted | | [ADR-0057](0057-keep-one-dated-line-per-state-an-adr-reached.md) | Keep one dated line per state an ADR reached, and never overwrite one | Accepted | -| [ADR-0058](0058-suppress-ca1510-while-the-netstandard-floor-stands.md) | Suppress CA1510 while the pre-.NET-6 floor stands | Accepted | -| [ADR-0059](0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) | Guard the recipe-versus-value boundary with analyzers where the type system cannot reach it | Accepted | | [ADR-0060](0060-let-stated-intent-outrank-generic-analyzer-advice.md) | Let stated intent outrank generic analyzer advice, and record the refusal beside the rule | Accepted | | [ADR-0061](0061-run-the-justdummies-analyzers-on-the-repository-s-own-code.md) | Run the JustDummies analyzers on the repository's own code, so the rules are verified against code not written to please them | Accepted | | [ADR-0062](0062-derive-the-build-rule-set-from-the-quality-profile.md) | Derive the build's Sonar rule set from the quality profile: generated membership, hand-written exceptions, weekly drift check | Accepted | -| [ADR-0063](0063-throw-the-library-s-own-exceptions-through-named-factories.md) | Throw the library's own exceptions through named factories, and only those — the `System` types keep their guard clauses | Accepted | -| [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md) | Exempt the whole failure-reporting path from the null-guard convention, declared with `[BuiltOnTheFailurePath]`; supersedes ADR-0045 | Accepted | -| [ADR-0065](0065-carry-a-declared-constraint-as-a-value-object.md) | Carry a declared constraint as a value object, not as its rendered text | Accepted | -| [ADR-0066](0066-declare-a-value-object-and-enforce-its-identity.md) | Declare a value object with an attribute, and enforce its identity by convention | Accepted | | [ADR-0067](0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.md) | Treat the CLI's exit codes as a closed, published contract | Accepted | -| [ADR-0044](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0044-extract-justdummies-into-its-own-repository.md) | Extract JustDummies into its own repository *(recorded in `Reefact/just-dummies`; it was ADR-0068 when this row was written, before that repository renumbered)* | Accepted | | [ADR-0069](0069-consume-justdummies-from-its-own-repository.md) | Consume JustDummies from its own repository | Accepted | diff --git a/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.fr.md b/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.fr.md deleted file mode 100644 index f2039389..00000000 --- a/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.fr.md +++ /dev/null @@ -1,1049 +0,0 @@ -# JustDummies — Audit d'architecture et de conception - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./2026-07-20-dummies-architecture-and-design-audit.md) - -**Date :** 2026-07-20 -**Révision auditée :** `3bf89e3` (sommet de `main` au moment de l'audit) -**Périmètre :** la seule bibliothèque `JustDummies` — `JustDummies/`, `JustDummies.UnitTests/`, son outillage de -garde (`tools/justdummies-check/`, `.github/workflows/justdummies.yml`), sa documentation, et les ADR qui la -gouvernent. -**Statut :** consultatif. Conformément à la convention du dépôt (ADR-0004), cet audit produit des -recommandations, jamais des bloqueurs ; toute modification d'ADR proposée est un brouillon que -`@reefact` accepte ou rejette. - -**Méthode.** L'intégralité du code de la bibliothèque (~8 700 lignes réparties sur 54 fichiers C#) et de -la suite de tests (~2 500 lignes, 17 fichiers) a été lue ; les 26 ADR ont été classées par -applicabilité et les 8 applicables revues à la fois pour leur qualité intrinsèque et pour la conformité -de l'implémentation ; les constats ont été vérifiés de façon contradictoire face au code, et les trois -défauts de comportement rapportés ci-dessous ont été **reproduits indépendamment à l'exécution** contre -la bibliothèque compilée. La suite de tests unitaires complète a été exécutée : **222/222 réussis** -(`dotnet test JustDummies.UnitTests`, exécuteur net10.0). Les jugements sont calibrés sur les objectifs -affichés de la bibliothèque — des tests lisibles, des données de test expressives, un déterminisme -optionnel, une API fluide et découvrable, la simplicité — et délibérément *pas* sur les objectifs des -frameworks de test par propriétés ou de fuzzing, ce que cette bibliothèque n'est explicitement pas. - ---- - -## 1. Résumé exécutif - -JustDummies est une jeune bibliothèque construite avec un niveau d'exigence inhabituel. Son idée -architecturale centrale — projeter chaque type discret dans un espace ordinal 64 bits partagé pour -qu'un seul moteur possède les bornes, les exclusions, la détection de conflits et l'échantillonnage de -treize générateurs à la fois — est élégante et correctement exécutée. Sa discipline de messages -d'erreur (chaque contrainte contradictoire nomme les *deux* côtés, chaque échec de génération nomme la -graine qui le rejoue) surpasse celle de la plupart des bibliothèques matures du domaine. Sa base d'ADR -est exemplaire : les décisions sont consignées avec des contraintes honnêtes, de vraies alternatives et -des compromis chiffrés. - -L'audit a néanmoins trouvé **trois véritables défauts de comportement**, tous reproduits à l'exécution : - -1. **Critique — `AnyDecimal` ne peut jamais générer la moitié haute de sa plage.** Une fraction censée - être uniforme dans [0, 1) est construite à partir de trois tirages de 31 bits divisés par un - dénominateur de 96 bits, et plafonne près de 0,5 ; `Any.Decimal().Between(0m, 100m)` ne dépasse - jamais ~49,9999 (`DecimalIntervalSpec.cs:145`). -2. **Majeur — le « nudge » d'exclusion de `AnySingle`/`AnyHalf` se bloque.** La marche - d'évitement des collisions avance d'un ulp de *double* au lieu de l'ulp du type, si bien que la - quantification retombe sur la même valeur et qu'une spécification satisfiable comme - `Any.Half().Between((Half)1f, (Half)1.001f).DifferentFrom((Half)1f)` lève une - `AnyGenerationException` pour ~la moitié des graines (`ContinuousIntervalSpec.cs:189`). -3. **Majeur — une plage de classe regex se terminant à `￿` boucle à l'infini.** La boucle - d'expansion de la classe incrémente un `char` 16 bits qui reboucle à `0xFFFF`, de sorte que - `Any.StringMatching(@"[ -￿]")` ne rend jamais la main (`RegexParser.cs:398`). - -Les trois partagent une cause racine qu'il vaut la peine de nommer : **la suite de tests vérifie -l'appartenance, jamais l'atteignabilité.** Les tests vérifient que les valeurs générées satisfont les -contraintes ; aucun test ne vérifie que le domaine déclaré est atteignable dans son entier, ni qu'une -spécification déclarée satisfiable génère effectivement. C'est l'angle mort précis d'une suite par -ailleurs bien conçue, et le combler compte davantage que n'importe quel correctif isolé. - -Un fait de cadrage atténue considérablement tout cela : **JustDummies n'a jamais été publiée.** Il n'y a -aucun tag `dum-v*` ; le changelog ne contient qu'une section *Unreleased* vide. Chaque défaut ci-dessus -peut être corrigé, et chaque contrat décidé, à coût de compatibilité nul. La recommandation-phare de -cet audit est de traiter la fenêtre pré-1.0 comme l'a fait l'ADR-0020 — le moment le moins cher pour -décider — et de solder les points des §11–§12 avant la première publication. - -Au-delà des défauts, les constats significatifs sont : la surface `Any`/`AnyContext` recopiée à la main -et les quatorze générateurs numériques clonés ne portent **aucun garde-fou de parité** (et la dérive de -documentation a déjà commencé) ; le **contrat de déterminisme présente des lacunes de documentation** -(des tirages concurrents dans un même scope à graine annulent silencieusement la rejouabilité ; la -stabilité des graines entre versions n'est ni promise ni écartée ; l'ancrage ADR du contrat a été perdu -lorsque l'ADR-0006 a été remplacée) ; la **cible netstandard2.0 n'est jamais exécutée par la propre -suite de tests de JustDummies** (seulement transitivement, via le job de plancher de FirstClassErrors) ; et -il n'existe **aucune référence utilisateur de la surface de contraintes** — le README du dépôt ne -mentionne même pas le paquet. L'analyse des manques (§10) juge la couverture de types réellement -complète au regard de la philosophie de la bibliothèque ; les deux absences qui méritent la qualité de -surprenantes sont un combinateur de choix *de premier niveau* (`Any.OneOf(params T[])` / -`Any.ElementOf(...)`) et des contraintes d'exclusion sur `AnyString` — le seul générateur scalaire qui -en soit dépourvu. - -## 2. Évaluation globale - -**Verdict : une bibliothèque pré-publication très solide — l'architecture et le processus sont ses -forces ; le test de correction dans l'espace des valeurs est sa seule faiblesse systémique.** - -Jugée domaine par domaine face aux objectifs affichés : - -| Domaine | Évaluation | -|---|---| -| Architecture | Excellente. Découpage en couches propre (générateurs publics → specs internes → échantillonnage), un moteur ordinal partagé, une séparation de moteurs fondée, des points de composition sur une interface minuscule. | -| Conception d'API | Excellente, avec une poignée d'asymétries d'apparence délibérée mais non consignées (§8). | -| Diagnostics d'erreur | Exceptionnels — la force signature de la bibliothèque. | -| Déterminisme | Conception saine, correctement implémentée au niveau `AsyncLocal`/`ExecutionContext` ; contrat sous-documenté à ses bords (§7.3). | -| Correction | Trois défauts reproduits, deux d'entre eux exactement dans le code qu'une suite fondée sur la seule appartenance ne peut pas voir (§4.1). | -| Stratégie de test | Bien formée (comportement d'abord, boîte noire, adossée à un oracle pour la regex, à l'abri des tests instables) mais aveugle à l'atteignabilité (§9.3). | -| Documentation | Docs XML remarquables ; documentation utilisateur maigre et difficile à trouver (§4.4). | -| Maintenabilité | La duplication est importante mais disciplinée (zéro erreur de copier-coller trouvée dans les familles de clones) ; le risque est la dérive non gardée, pas la pourriture présente (§9). | -| Base d'ADR | Qualité exemplaire ; deux lacunes structurelles — le contrat de déterminisme et le moteur ordinal n'ont pas d'ADR propre (§5). | - -La forme d'ensemble est celle d'une bibliothèque écrite avec grand soin par un petit nombre de mains : -les *décisions* sont systématiquement justes et systématiquement consignées, tandis que les filets de -sécurité qui protègent ces décisions des mains futures (gardes de parité, tests d'atteignabilité, -références d'API) ne sont pas encore en place. Le pré-1.0 est le moment de les installer. - -## 3. Forces - -Elles sont méritées, vérifiées face au code, et à préserver délibérément. - -### 3.1 L'unification par l'espace ordinal - -Chaque type discret — les dix entiers de 64 bits ou moins, `DateTime`, `DateTimeOffset`, `TimeSpan`, -`DateOnly`, `TimeOnly` — se projette en préservant l'ordre dans un espace ordinal non signé 64 bits -(`OrdinalMapping.FromInt64` inverse le bit de signe ; `OrdinalIntervalSpec.cs:9-23`) et partage **un -seul** moteur pour les bornes, les listes d'autorisation, les exclusions, la détection de conflits, la -cardinalité et l'échantillonnage (`OrdinalIntervalSpec`). L'algorithme d'exclusion est exact — un index -tiré est projeté sur le k-ième ordinal non exclu en une seule passe sur une liste d'exclusions triée -(`OrdinalIntervalSpec.cs:194-202`) — de sorte que la génération est un tirage unique, jamais -tirer-puis-recommencer. Un correctif à un message de conflit ou à un cas limite atteint tous les -générateurs discrets simultanément. C'est le bon niveau où appliquer DRY : la *logique* est partagée -tandis que les fines façades par type restent simples et lisibles. - -### 3.2 Provenance des contraintes et validation immédiate - -Chaque borne se souvient de la chaîne de contrainte qui l'a posée (`"Between(1, 6)"`, `"Positive()"`), -de sorte qu'un conflit nomme **les deux** côtés au moment de la déclaration : - -``` -Cannot apply LessThan(10) because GreaterThan(100) already requires values greater than or equal to 101. -``` - -La discipline tient uniformément sur chaque générateur et chaque moteur de spécification, y compris des -validations transversales qu'on ne s'attendrait pas à trouver (un jeu de caractères `Numeric()` rejette -un préfixe contenant des lettres, en nommant le caractère fautif — `StringSpec.cs:254-275`). Combinée à -la vérification immédiate de satisfiabilité (« un générateur qui existe peut toujours générer »), un -`Arrange` impossible échoue à la ligne qui l'a écrit, pas à un tirage ultérieur. C'est la signature de -la bibliothèque, et elle est exécutée avec constance. - -### 3.3 La machinerie de déterminisme est bien faite au niveau difficile - -La pièce maîtresse — les générateurs stockent une `RandomSource` et ne résolvent `.Current` qu'au -moment de `Generate()` — est ce qui permet à une recette construite hors d'un `Any.Reproducibly(...)` -de générer de façon déterministe à l'intérieur (`RandomSource.cs:3-9`). La sémantique de scope -`AsyncLocal` a été examinée de près et tient : la restauration par `using` de la surcharge synchrone est -correcte ; la mutation `UseSeed` de la surcharge asynchrone ne peut pas fuir vers l'appelant (les -mutations d'`ExecutionContext` d'une méthode asynchrone ne reviennent pas en arrière) ; l'imbrication -restaure le scope externe intact ; `ConfigureAwait(false)` est sans effet sur la circulation de -l'`ExecutionContext`. La graine est rapportée de bout en bout : une fabrique utilisateur qui lève une -exception dans `.As(...)` produit une `AnyGenerationException` nommant la valeur générée *et* la graine -(`AnyDerivation.cs:59-73`) ; une saturation de collection distincte fait de même -(`CollectionState.cs:246-254`). - -Deux pièges subtils liés à la double cible ont été anticipés et documentés à l'endroit exact du danger : -l'échantillonneur inclusif de `RandomSampling` n'est délibérément *pas* nommé `NextInt64` parce que, sur -la cible net8.0, la méthode d'instance à borne exclusive du framework l'emporterait dans la résolution -de surcharge et changerait silencieusement la sémantique (`RandomSource.cs:129-137`) ; et l'extension -`OrNull` est scindée en deux classes parce que des surcharges d'un même nom contraintes l'une à -`struct` et l'autre à `class` entreraient en collision (`NullableExtensions.cs:47-52`). C'est le genre -de soin qui ne se rattrape pas après coup. - -### 3.4 Des échappatoires bornées partout — aucune reprise non bornée nulle part - -L'affirmation « construit pour satisfaire, jamais générer-puis-filtrer » résiste à l'examen avec trois -exceptions honnêtes, consignées en ADR, chacune *bornée* : le tirage dédupliqué des collections -distinctes (budgeté, généreux façon collectionneur de coupons, remis à zéro à chaque progrès — -`CollectionState.cs:236-244`), le nudge d'exclusion du domaine continu (marche jusqu'à la valeur -représentable voisine), et l'évitement de collision d'`AnyGuid` (une incrémentation avec retenue sur -toute la largeur qui se termine de façon prouvable — `AnyGuid.cs:27-36`). Chaque mode d'échec produit un -message actionnable, porteur de graine, plutôt qu'un blocage. - -### 3.5 Le soin aux bords - -De petites choses qui révèlent le niveau d'exigence : `AnyDateTime.OneOf` se souvient des valeurs -originales de l'appelant pour que l'aller-retour ordinal ne normalise pas silencieusement le -`DateTimeKind` (`AnyDateTime.cs:124-131`) ; l'agencement d'une collection est mélangé par Fisher-Yates -pour qu'une collection factice n'annonce jamais un invariant de position sur lequel un test pourrait -s'appuyer par accident (`CollectionState.cs:46-53`) ; `CountSpec` et `StringSpec` saturent au lieu de -déborder sur des minima déclarés énormes ; la covariance d'`IAny` fait que les interfaces de -collection en lecture seule (`IReadOnlyList`, etc.) sont servies gratuitement. - -### 3.6 Le sous-système regex est bien bâti pour le périmètre décidé - -L'analyseur syntaxique à descente récursive écrit à la main (`RegexParser.cs`, 457 lignes — le plus gros -bloc de logique unique de la bibliothèque) est bien structuré, commenté sur le « pourquoi » et -discipliné sur sa taxonomie de rejet à deux canaux : `ArgumentException` pour les motifs malformés, -`UnsupportedRegexException` nommant la construction et la position pour ceux qui sont bien formés mais -non réguliers. La suite de tests valide les chaînes générées contre le **vrai moteur regex de .NET -utilisé comme oracle** sur un corpus à graine fixe — exactement la bonne façon de tester un générateur. -(Les défauts trouvés à ses bords sont catalogués aux §4.1 et §4.2 ; ils ne changent rien à l'évaluation -selon laquelle l'approche de l'ADR-0025 était saine et honnêtement argumentée.) - -### 3.7 Empaquetage, frontière et processus - -La frontière zéro-dépendance, agnostique aux erreurs, est appliquée de trois façons : un commentaire de -`.csproj` énonçant la règle, un test d'architecture fondé sur l'intention qui échoue à toute référence -d'assemblage hors BCL (`ArchitectureTests.cs:27-37`), et le garde-fou d'artefact empaqueté -`justdummies-check` — un vrai programme consommateur, exécuté en CI contre l'*artefact packé* pour chaque -cible, qui prouve que l'asset net8.0 porte les générateurs modernes, que l'asset netstandard2.0 ne les -porte pas, que les contraintes et conflits se comportent bien, et que des contextes à même graine -rejouent (`tools/justdummies-check/Program.cs`). L'empaquetage lui-même est de niveau production : SourceLink -avec sources non suivies embarquées, builds CI déterministes, symboles snupkg, un SBOM SPDX embarqué au -moment du pack, des assets de release à provenance attestée (`Directory.Build.props:10-23`, -`JustDummies.csproj:60-66`, `release.yml`). La base d'ADR qui consigne tout cela est discutée au §5 — c'est -une force en soi. - -### 3.8 La philosophie d'API est cohérente et documentée là où l'utilisateur regarde - -L'idée « les contraintes expriment ce que le code environnant *requiert*, jamais ce que le test -assertit » est énoncée sur le point d'entrée, sur chaque générateur, dans le README du paquet et dans le -guide utilisateur — la même phrase, délibérément. Le parti pris de ne pas offrir de contraintes -relatives à l'horloge (`AnyDateTime` n'a pas d'`InThePast()`) est documenté à chaque endroit où -l'utilisateur le chercherait, avec la justification de reproductibilité attachée. Les quasi-synonymes -révélateurs d'intention sont expliqués honnêtement : `DifferentFrom(x)` est documenté comme -sémantiquement `Except(x)` avec un nom qui porte l'intention ; `Containing` (une valeur connue -maintenant) vs `ContainingAny` (un générateur tiré au moment de la construction) est une distinction -réellement utile. - -## 4. Faiblesses - -Classées par sévérité. Les points 4.1 et 4.3 sont ceux qui devraient conditionner une première -publication. - -### 4.1 Défauts de comportement reproduits - -**(a) `AnyDecimal` n'atteint jamais la moitié haute d'une plage — critique.** - -`DecimalIntervalSpec.cs:144-149` : - -```csharp -// A uniform-enough fraction in [0, 1): 93 random bits over the full decimal mantissa scale. -decimal fraction = new decimal(random.Next(), random.Next(), random.Next(), false, 28) / MaxFraction; -decimal mid = _min / 2 + _max / 2; -decimal half = _max / 2 - _min / 2; -decimal candidate = Clamped(mid + (fraction * 2 - 1) * half); -``` - -`Random.Next()` renvoie un `int` non négatif, si bien que le bit de poids fort de **chaque limbe de -32 bits** de la mantisse de 96 bits est toujours zéro, tandis que `MaxFraction` est le maximum *plein* -de la mantisse 96 bits (`7,9228…`, `DecimalIntervalSpec.cs:14`). La fraction vit donc dans -[0, ~0,49999986], pas [0, 1) ; `(fraction * 2 - 1)` vit dans [−1, ~0) ; et chaque candidat atterrit dans -`[min, mid)`. Le maximum inclusif documenté sur `AnyDecimal.Between` (`AnyDecimal.cs:112`) est -inatteignable — tout comme tout ce qui est au-dessus du milieu. Reproduit indépendamment pour cet -audit : le maximum de 200 000 tirages de `Any.Decimal().Between(0m, 100m)` valait **49,99992…**. - -Pourquoi cela compte au-delà de l'évidence : un test utilisant `Any.Decimal().Between(0m, 100m)` pour -exercer « n'importe quel pourcentage valide » n'exerce silencieusement jamais 50–100 — la promesse -centrale de la bibliothèque (« arbitraire mais valide, pour que les hypothèses cachées ressortent ») est -retournée en une hypothèse cachée qui lui est propre. Le correctif est petit : construire la fraction à -partir de 96 bits véritablement uniformes, par exemple : - -```csharp -// after: 12 random bytes fill all three 32-bit limbs uniformly -// (the decimal ctor reads the int limbs as raw 32-bit patterns) -byte[] limbs = new byte[12]; -random.NextBytes(limbs); -decimal fraction = new decimal( - BitConverter.ToInt32(limbs, 0), - BitConverter.ToInt32(limbs, 4), - BitConverter.ToInt32(limbs, 8), - false, 28) / MaxFraction; -``` - -(toute construction remplissant les 96 bits de mantisse de façon uniforme convient — les trois appels -`Next()` actuels fixent à zéro le bit de poids fort de chaque limbe et ne peuvent jamais tirer un limbe -de `2^31−1`), puis ajouter le test d'atteignabilité du §11 point 2. À noter : le commentaire lui-même -(« 93 random bits over the full mantissa scale ») documente une intention que le code n'honore pas — et -même 93 bits bien placés n'atteindraient pas l'octant supérieur d'un dénominateur de 96 bits. - -**(b) Le nudge d'exclusion de `AnySingle`/`AnyHalf` se bloque sur des specs satisfiables — majeur.** - -`ContinuousIntervalSpec.cs:188-198` : quand une valeur tirée entre en collision avec un point exclu, la -marche avance avec le `NextUp` **statique, en espace double** (ligne 189) au lieu du lambda `_nextUp` -*conscient du type* que `AnySingle`/`AnyHalf` fournissent précisément pour avancer dans leur propre -échelle de valeurs représentables (`AnySingle.cs:20`, `AnyHalf.cs:22`) — et que les chemins à borne -exclusive utilisent déjà correctement (lignes 120, 125). Un ulp de double au-dessus d'un `float`/`Half` -représentable se re-quantifie vers la même valeur, l'échappatoire `next > _max` de la ligne 190 est -inatteignable (`Quantized` borne d'abord à `_max`, lignes 203-209), si bien que le budget de 128 pas se -consume et qu'une spec *satisfiable* lève. Reproduit indépendamment : -`Any.Half().Between((Half)1f, (Half)1.001f).DifferentFrom((Half)1f).Generate()` a levé une -`AnyGenerationException` pour **250 graines sur 500** ; le scénario `AnyDouble` identique ne lève jamais -(sa quantification est l'identité). Le correctif tient en un jeton — `Quantized(_nextUp(candidate))` — -plus un test de non-régression par type continu. - -Ce défaut mérite une note de conception : c'est exactement la classe d'échec que prédit l'architecture -de la bibliothèque. Le moteur a été paramétré par des lambdas `quantize`/`nextUp` *parce que* les types -étroits doivent avancer dans leur propre échelle ; un site d'appel dans le même fichier a oublié le -paramètre. Une suite de scénarios paramétrée et transverse aux moteurs (§9.3) est la réponse -structurelle. - -**(c) Une plage de classe se terminant à `￿` boucle à l'infini — majeur.** - -`RegexParser.cs:398` : - -```csharp -for (char character = low; character <= high; character++) { set.Add(character); } -``` - -Quand `high == '￿'` (atteignable via l'échappement supporté `\uHHHH`), le `char` 16 bits reboucle à -`0x0000` et `character <= high` est toujours vrai. Reproduit indépendamment : -`Any.StringMatching(@"[ -￿]")` n'a pas rendu la main en cinq secondes (blocage dur), alors que le même -motif est une regex .NET valide. Un blocage au moment de la déclaration est le pire mode d'échec que -cette bibliothèque puisse exhiber — son identité est d'*échouer vite avec une cause nommée*. Correctif : -garder le rebouclage (`if (character == high) break;` dans la boucle, ou itérer sur un `int`), et -répliquer le contrôle dans la boucle jumelle privée `RegexAlphabet.Range` (`RegexAlphabet.cs:66-71`) -par défense en profondeur. - -**(d) Les groupes d'équilibrage et les noms de groupe invalides sont acceptés silencieusement — majeur, -dans le sens qui rompt le contrat.** - -`SkipGroupName` (`RegexParser.cs:295-300`) balaie jusqu'au terminateur sans aucune validation. En -conséquence `(?<-a>x)` — un *groupe d'équilibrage* (balancing group), non régulier, de la même famille -que les références arrières que la bibliothèque rejette fièrement — est traité comme un groupe nommé -ordinaire : `Any.StringMatching(@"(?y)?(?<-a>x)")` génère `"x"`, que le vrai moteur ne reconnaît -**pas** (vérifié : le langage du motif est exactement `{"yx"}`). Les noms de groupe invalides -(`(?x)`) sont de même acceptés là où .NET les rejette. C'est le seul endroit trouvé par l'audit où -la promesse signature de la bibliothèque — *« une erreur claire vaut mieux qu'une valeur qui ne -correspond pas réellement »* (ADR-0025) — est rompue. Le correctif est local : valider le nom capturé -(rejeter `-` en `Unsupported("a balancing group …")`, rejeter les caractères non-mot en -`Malformed(...)`). - -**(e) Défauts mineurs dans le même sous-système.** L'exception de limite de génération accuse « a nested -unbounded quantifier » même quand la vraie cause est un quantificateur *borné* de grande taille -(`(a{1000}){1000}` — le message affirme un diagnostic faux ; `RegexNode.cs:31-37`) ; quelques motifs que -le vrai moteur accepte sont refusés par prudence (`^*`, `abc$$` — tandis que `^^abc` est accepté, une -asymétrie évitable ; un `-[` en tête de classe est mal lu comme une soustraction) ; et une classe -négative bien formée dont les membres sortent de l'univers imprimable est mal classée en *malformée* au -lieu de *non supportée*. Tous ces cas échouent dans le sens sûr (refus, jamais mauvaise génération) et -sont cosmétiques à côté de (c) et (d). - -### 4.2 L'affirmation « ASCII imprimable » est exagérée en trois endroits - -`RegexAlphabet.cs:3-9`, `AnyPattern.cs:21` et `Any.Pattern.cs:23` affirment tous que chaque terminal se -résout en ASCII imprimable (0x20–0x7E). Le code — correctement — émet exactement les caractères que le -motif exige : `\t`, `\a`, `\cA`, `\0` et les littéraux `\uHHHH` peuvent être non imprimables ou -non-ASCII, et le propre test de la bibliothèque l'assertit (`AnyPatternTests` — `\a` → U+0007). La -restriction ne s'applique vraiment que là où le motif laisse le caractère *libre* (raccourcis, le point, -classes négatives). Comme l'ADR-0025 déclare explicitement l'univers de caractères comme un comportement -sur lequel les consommateurs peuvent s'appuyer, les trois emplacements de doc devraient le dire -précisément (§11 point 6). - -### 4.3 Des surfaces recopiées à la main sans garde-fou de parité, et la dérive a déjà commencé - -Deux structures miroir doivent s'accorder méthode par méthode, et rien ne contrôle ni l'une ni l'autre : - -* **`Any` ↔ `AnyContext`** : chaque point d'entrée scalaire existe deux fois (21 sur la cible - netstandard2.0, 26 sur net8.0, en comptant les deux surcharges de `StringMatching`) — - `Any.Primitive.cs:11-224`, `Any.Pattern.cs:34-52` et `Any.Uri.cs:13` face à `AnyContext.cs:51-305`. - Le miroir est une conception légitime (la composition - et les collections ne sont délibérément *pas* recopiées — elles héritent d'un contexte via les sources - de leurs opérandes, ce qui est élégant), mais un nouveau type scalaire ajouté à `Any` et oublié sur - `AnyContext` compilerait, passerait les 222 tests, et livrerait un trou dans la surface déterministe. - La dérive de formulation est déjà visible à l'intérieur d'`AnyContext` lui-même (deux tournures - différentes du déterminisme selon les fabriques ; sa doc de `Guid()` mentionne `Any.Reproducibly`, - qu'un contexte fixe ignore par conception). -* **Les quatorze générateurs numériques** sont des clones identiques à l'octet près modulo substitution - de type (~2 450 lignes ; le quatuor signé, le quatuor non signé, le trio continu et la paire large ; - les cinq générateurs temporels suivent le même patron pour ~800 lignes de plus). Au crédit des - familles de clones, un balayage scripté n'a trouvé **aucune erreur de copier-coller** dans le code - lui-même — mais trois résumés XML disent « Same constraint algebra as `AnyInt32` » sur des générateurs - où c'est littéralement faux (les types non signés n'ont pas `Positive`/`Negative` ; les types - temporels renomment la famille de bornes), et trois DisplayName de test affirment encore que les - générateurs « convert implicitly to their value type » - (`AnyContinuousTests.cs:108`, `AnySignedIntegerTests.cs:87`, `AnyUnsignedIntegerTests.cs:76`) — des - conversions que l'ADR-0020 a supprimées. Un commentaire périmé dans `SeedReproducibilityTests.cs:17-18` - explique du code par ces mêmes conversions supprimées. - -L'absence de gardes est le constat ; l'analyse d'atténuation et la recommandation (tests de parité par -réflexion, *pas* une classe de base générique) sont au §9.2. - -### 4.4 La documentation n'atteint ni celui qui découvre ni l'utilisateur avancé - -* Le **README du dépôt ne mentionne jamais JustDummies** (vérifié : zéro occurrence), alors que le README du - paquet renvoie au dépôt pour la « documentation complète ». Celui qui découvre le paquet sur NuGet - arrive sur une page d'accueil portant sur une autre bibliothèque ; ce qui ressemble le plus à un guide - JustDummies (`ArbitraryTestValues.en.md`) est un guide d'intégration de FirstClassErrors.Testing qui - renvoie lui-même à « documented with JustDummies itself » — une référence circulaire. -* **Aucune référence utilisateur ne documente la surface de contraintes par générateur.** Où - l'utilisateur apprend-il que `Except`/`OneOf`/`DifferentFrom` existent sur les numériques, que - `WithLengthBetween` existe, que `ContainingAny` diffère de `Containing`, ou quel dialecte regex - `StringMatching` supporte ? Aujourd'hui : seulement IntelliSense, un générateur à la fois. Le propre - suivi de l'ADR-0025 (« documenter le dialecte supporté ») est toujours ouvert. -* La **surprise du vide-par-défaut** (une collection non contrainte peut avoir 0 élément, une chaîne non - contrainte peut être vide) est bien documentée dans les remarques XML mais absente du README du paquet, - là où un utilisateur qui survole en profiterait le plus — c'est un choix délibéré, porteur de - philosophie (« un test qui itère zéro fois sur une collection non contrainte, c'est une hypothèse - cachée qui ressort ») et il mérite d'être annoncé comme tel. - -### 4.5 Lacunes du contrat de déterminisme (documentation, pas implémentation) - -Détaillé au §7.3 : des tirages concurrents dans un même scope à graine annulent silencieusement la -rejouabilité (et exposent un `System.Random` non thread-safe à une course) — documenté nulle part ; les -rapports de graine peuvent nommer une graine erronée ou inapplicable pour les compositions à contexte -fixe et à sources mixtes ; la stabilité de la séquence de graines entre versions et entre TFM n'est ni -promise ni écartée ; et le contrat entier a perdu son ancrage ADR lorsque l'ADR-0006 a été remplacée. - -### 4.6 La cible netstandard2.0 n'est jamais exécutée par la propre suite de JustDummies - -`JustDummies.UnitTests` ne cible que net10.0. L'assemblage netstandard2.0 — celui que chargeront les -consommateurs .NET Framework — n'est exercé que *transitivement* : le job de plancher de -FirstClassErrors (`ci.yml:98-115`) exécute `FirstClassErrors.UnitTests` sur net472, qui prépare ses -`Arrange` avec `JustDummies.Any` via référence de projet et via les fabriques de Testing, si bien que -JustDummies se charge et génère bien sur le vrai CLR .NET Framework — mais sa propre suite de contrat de -222 tests (oracle regex, détection de conflits, gating de distinction, reproductibilité de graine) n'y -tourne jamais, et l'égalité même-graine-mêmes-valeurs entre les deux assets empaquetés n'est assertée -nulle part. Le dépôt possède déjà exactement la machinerie nécessaire (`build/Net472TestFloor.props`, -utilisée par `FirstClassErrors.UnitTests`) ; l'étendre à `JustDummies.UnitTests` (en conditionnant hors -scope les tests net8-only) est mécanique. Voir la conformité à l'ADR-0022, §6. - -### 4.7 Garde-fous d'ingénierie de release pas encore installés - -Aucune référence d'API publique (`Microsoft.CodeAnalysis.PublicApiAnalyzers`), aucun -`EnablePackageValidation`/ApiCompat. Le changelog engage JustDummies vers la gestion sémantique de version -tandis que l'audit lui-même démontre que la surface d'API est recopiée à la main et dérive déjà dans la -documentation ; la détection de changements cassants contre une référence publiée est le mécanisme -complémentaire que les tests de parité ne peuvent pas remplacer (une surcharge supprimée ou un type de -retour rétréci passe un test de miroir). L'avant-première-publication est le moment le moins cher pour -installer les deux. Un commentaire périmé trouvé ici : `Directory.Build.props:3-9` dit que le dépôt -livre « FirstClassErrors and FirstClassErrors.Testing » — il omet JustDummies, le paquet même que ces -propriétés de pack gouvernent désormais aussi. - -## 5. Revue des ADR - -Dix-huit des vingt-six ADR ne concernent pas JustDummies (elles nomment les analyseurs, le request binder, -l'outillage GenDoc/CLI, l'API Outcome, ou le processus du dépôt). Huit s'appliquent, et leur qualité a -été revue individuellement. Le niveau d'ensemble est assez élevé pour le dire simplement : cette base -d'ADR est un modèle du genre. Les décisions portent des contraintes honnêtes, des alternatives -réellement considérées, des inconvénients chiffrés, et des suivis qui ont effectivement été exécutés. - -### ADR-0006 — Fournir des valeurs de test arbitraires depuis une source unique semable *(Remplacée)* - -**Qualité : exemplaire, historiquement.** Les contraintes étaient réelles (promesse zéro-dépendance, -sûreté des tests parallèles netstandard2.0 sans `Random.Shared`), les quatre alternatives ont été -pesées équitablement, et ses suivis (extraire le moteur quand un second consommateur apparaît ; envisager -un adaptateur xUnit) ont été honorés ou consciemment différés. Son analyse du risque de collision du -défaut non semé est exactement à la bonne profondeur. **Problème :** sa mise en remplacement a créé une -lacune — voir « lacunes structurelles » plus bas. - -### ADR-0011 — Héberger JustDummies comme paquet autonome *(Acceptée)* - -**Qualité : bonne.** Le raisonnement nom/identité/frontière est sain et la règle de non-référence est -vérifiée par machine. Deux points de précision. Premièrement, l'invariant *appliqué* est plus fort que -celui *consigné* : le test d'architecture interdit **toute** référence hors BCL -(`ArchitectureTests.cs:27-37`), et l'ADR-0025 s'appuie sur une « identité zéro-dépendance … la frontière -est vérifiée par machine (ADR-0011) » — mais le texte de décision de l'ADR-0011 n'interdit que de -référencer des *projets FirstClassErrors*. La règle du zéro-dépendance-*tierce*, porteuse pour tout -l'argument de l'ADR-0025, n'est consignée nulle part comme décision. Deuxièmement, les alternatives ne -pèsent jamais les risques de l'identifiant NuGet ultra-générique `JustDummies` (squattage/collision/ -recherchabilité) — une identité de paquet que l'ADR elle-même qualifie de coûteuse à renommer. Aucun de -ces points ne change la décision ; les deux méritent une ligne au dossier. - -### ADR-0013 — Gater les collections distinctes par cardinalité, sinon par tirage borné *(Acceptée)* - -**Qualité : remarquable.** L'argument de solidité — ne compter que les éléments que le générateur doit -fournir, créditer les valeurs `Containing` hors de son domaine, traiter les tirages opaques -`ContainingAny` de façon conservatrice, laisser le tirage borné être le filet de sécurité final — est -énoncé dans le document et reflété de façon prouvable dans le code -(`CollectionState.Validate`/`CardinalityCap`/`FixedOutsideCount`). La section des risques anticipe même -un mauvais réglage du budget et enjoint de « réviser sur preuves plutôt que de décrire l'échec comme -impossible ». **Problème (partagé avec l'ADR-0015) :** elle diffère « l'interface exacte de l'indice, -l'état de collection, le budget de tirage, la charge utile d'exception et la propagation de graine » vers -la référence d'implémentation — mais la section JustDummies de cette référence -(`adr-implementation-reference.md:58-68`) ne consigne aucune de ces spécificités (pas de chiffres de -budget, pas de charge utile d'exception, pas de règle de propagation de graine). Le renvoi promet plus -que la destination ne contient ; soit enrichir la référence, soit adoucir le renvoi. - -### ADR-0015 — Plafonner Any.Combine à l'arité huit *(Acceptée)* - -**Qualité : bonne.** Honnête sur le caractère heuristique du plafond, avec une échappatoire définie -(ajouter des arités de façon compatible via une nouvelle décision sur preuves). Les alternatives sont -réelles. Le même point sur le renvoi à la référence d'implémentation que pour l'ADR-0013 s'applique. - -### ADR-0020 — Matérialiser les dummies uniquement via Generate() *(Acceptée)* - -**Qualité : exemplaire — le meilleur document de la base.** Preuves concrètes (les formes syntaxiques où -la conversion se comportait mal en silence, tirées de la suite elle-même), trois alternatives pesées -équitablement dont la voie analyseur qu'elle décline délibérément, coûts honnêtes, et l'argument de -timing pré-1.0 énoncé comme tel. Elle a de plus manifestement orienté des travaux ultérieurs (l'ADR-0026 -réutilise à la fois son patron de raisonnement et son cadrage de risque). Aucune modification -recommandée. - -### ADR-0022 — Fixer le plancher de support .NET Framework à 4.7.2 *(Acceptée)* - -**Qualité : politique saine ; formulation de périmètre vieillie.** « Une promesse de compatibilité qui -n'est pas exercée ne peut pas fournir une frontière de support fiable » est le bon principe. Mais l'ADR -précède JustDummies et parle des « bibliothèques `netstandard2.0` livrées » sans les nommer ; savoir si -JustDummies est dans son périmètre relève désormais de l'inférence, et le job de plancher ne l'inclut pas -(§6). Quand le mainteneur touchera à nouveau à ce domaine, une clarification d'une ligne des paquets -couverts lèverait l'ambiguïté — ou la décision de plancher propre à JustDummies pourrait chevaucher la -nouvelle ADR de déterminisme proposée ci-dessous. - -### ADR-0025 — Générer des chaînes correspondantes depuis un sous-ensemble régulier maison *(Proposée)* - -**Qualité : un dossier construire-ou-acheter d'une honnêteté inhabituelle.** Le rejet de Fare est -argumenté sur des motifs d'identité et de contrat d'erreur (abandon silencieux des constructions non -régulières vs refus de première classe), pas sur du FUD ; le cadrage « les constructions non régulières -sont impossibles pour *tout* générateur fini, donc le sous-ensemble n'est pas une coupe de confort » est -exactement juste ; la décision de générateur terminal est bien argumentée. **Problèmes :** (1) Elle est -toujours **Proposée** alors qu'elle est entièrement implémentée, livrée dans le README du paquet, et -*porteuse pour l'ADR-0026 Acceptée* (dont `ErrorCodeFactory` est bâti sur `StringMatching`) — tant que -le statut n'a pas basculé, une décision acceptée repose formellement sur une décision indécise. Le rôle -de l'audit est de le signaler ; seul `@reefact` bascule un statut. (2) La phrase de justification « les -terminaux tirent de l'ASCII imprimable » est imprécise — `\s` inclut la tabulation (0x09) et les -échappements explicites émettent exactement le caractère qu'ils nomment (§4.2) ; la formulation devrait -être corrigée *avant* l'acceptation, puisque l'ADR elle-même déclare l'univers comme un comportement -pertinent pour la compatibilité. (3) Elle cite « un test par propriété » contre le vrai moteur ; ce qui -existe est un test-oracle à graine fixe et corpus fixe dans le projet de tests unitaires — excellent, -mais pas par propriété ; le texte devrait dire ce qu'est le filet de sécurité. - -### ADR-0026 — Rebaser les valeurs arbitraires du paquet de test sur JustDummies *(Acceptée)* - -**Qualité : un dossier de consolidation approfondi** — six vraies alternatives, la justification de -l'histoire à graine unique, un risque d'empaquetage intermédiaire honnête. **Deux dérives de -précision :** (1) le texte de décision dit que chaque fabrique expose « an `IAny` generator through a -distinct method where composition is needed » — aucune fabrique n'expose une telle méthode aujourd'hui -(vérifié : zéro occurrence d'`IAny` dans les sources de `FirstClassErrors.Testing`). YAGNI défendable, -mais le texte se lit comme une forme d'API décidée, et un contrôle de conformité dans un an ne pourra pas -distinguer le report délibéré de la migration inachevée. (2) Sa clause de risque dit que le danger de -double assemblage existe « precisely because JustDummies types appear in Testing's public API » — -aujourd'hui aucun n'y apparaît ; la prémisse est mal énoncée (le danger est réel pour d'autres raisons -tant que JustDummies est livrée dans l'artefact). Comme les ADR acceptées ne sont jamais éditées en place, -les deux relèvent d'une courte note dans la référence d'implémentation. - -### Lacunes structurelles de la base (recommandations de création) - -1. **Le contrat de déterminisme de JustDummies n'a pas d'ADR acceptée.** La source ambiante `AsyncLocal`, - le `Reproducibly` optionnel, l'épinglage paresseux, le rapport de graine à l'échec — la garantie - joyau de la couronne — a été décidée dans l'ADR-0006, désormais Remplacée *et* cadrée sur - FirstClassErrors.Testing ; la décision de l'ADR-0026 porte sur le rebasage de Testing, pas sur le - contrat propre de JustDummies. Un futur mainteneur qui demande « pourquoi `AsyncLocal` et pas un - paramètre ? pourquoi un `System.Random` mis en course est-il acceptable ? » ne trouve le raisonnement - que dans un dossier remplacé. **Recommandation : rédiger une ADR Proposée** (« JustDummies fournit des - valeurs arbitraires depuis une source ambiante, semable, locale au contexte d'exécution, avec - reproductibilité optionnelle ») reprenant la justification de l'ADR-0006 et réglant, dans le même - document, les bords ouverts que cet audit a fait remonter : la sémantique de concurrence à flux - logique unique, le point de couture fermé `IHasRandomSource`, et la politique de stabilité de graine - entre versions (§7.3). -2. **L'architecture du moteur ordinal n'a pas d'ADR.** Un espace ordinal 64 bits partagé avec quatre - moteurs par substrat arithmétique est une décision durable, questionnable-par-un-futur-mainteneur - (pourquoi quatre moteurs ? pourquoi `decimal` n'est-il pas projeté en ordinal ?) qui ne vit - aujourd'hui que dans des docs XML internes. Elle passe le propre test d'ADR du dépôt (« si - l'implémentation changeait mais que la décision tenait… »). Une courte ADR Proposée corrigerait - l'asymétrie avec des décisions bien plus petites (plafonds d'arité) qui, elles, ont eu des dossiers. - -## 6. Conformité aux ADR - -| ADR | Statut | Conformité de l'implémentation | -|---|---|---| -| 0006 (historique) | Remplacée | **Conforme et dépassée.** Le contrat de graine hérité (local au contexte, déterminisme optionnel, rapport de graine) est implémenté fidèlement ; JustDummies ajoute l'`AnyContext` isolé que l'ADR d'origine ne faisait qu'anticiper. | -| 0011 | Acceptée | **Conforme.** Aucune référence à FirstClassErrors ; frontière vérifiée par machine (`ArchitectureTests`) ; identité autonome, train de release et docs en place. Note : l'application est *plus forte* que la décision consignée (§5). | -| 0013 | Acceptée | **Conforme, vérifiée en détail** — gate immédiat net des crédits `Containing` hors domaine, comptage conservateur de `ContainingAny`, arithmétique à l'abri du débordement, budget borné, les deux canaux d'échec. **Une déviation mineure :** le message de saturation promet *inconditionnellement* le rejeu `Any.Reproducibly({seed}, …)` (`CollectionState.cs:246-254` ; la garde `seed is not null` est du code mort — la graine n'y peut jamais être nulle). Pour un générateur d'éléments **étranger** dont les tirages ignorent la source ambiante, cette promesse est fausse ; l'ADR dit que les échecs sont « explicites et reproductibles ». Qualifier le message quand le générateur d'éléments ne porte aucune source de la bibliothèque. | -| 0015 | Acceptée | **Conforme exactement** — arités 2–8, pas plus ; suppressions localisées avec justifications renvoyant à l'ADR (`Any.Combine.cs:214-215`, `266-267`) ; plafond documenté sur la surcharge d'arité 8. | -| 0020 | Acceptée | **Pleinement conforme.** Aucune conversion implicite nulle part ; `Generate()` est la seule matérialisation ; générateurs vérifiés immuables (chaque méthode fluide renvoie une nouvelle instance). Résidu : trois DisplayName de test et un commentaire *décrivent* encore les conversions supprimées (§4.3). | -| 0022 | Acceptée | **Partielle pour JustDummies.** L'asset netstandard2.0 n'est chargé et exercé sur net472 que transitivement via le job de plancher de FirstClassErrors ; la propre suite de JustDummies n'y tourne jamais, et le README du paquet n'énonce aucun plancher .NET Framework (celui de FirstClassErrors le fait). À solder avant la première publication (§11 point 5). | -| 0025 | Proposée | **Conforme sur chaque clause majeure** (analyseur maison, refus de première classe, générateur terminal, zéro dépendance, univers ASCII imprimable *par défaut*, spread borné des quantificateurs non bornés). Les défauts §4.1(c)/(d) sont des bugs de qualité *à l'intérieur* du périmètre décidé, pas des déviations — avec la réserve que (d) rompt la *promesse* de refus que l'ADR consigne. Un bord de taxonomie : une classe négative bien formée hors de l'univers imprimable lève `ArgumentException` (« malformée ») au lieu d'`UnsupportedRegexException`. | -| 0026 | Acceptée | **Conforme sur chaque clause exécutée** — moteur unique, scope de graine unique, `Testing.Any` supprimé, fabriques livrées, horloge/ids sur le contexte ambiant, docs mises à jour EN/FR. La moitié « distinct `IAny` method » non implémentée et la prémisse de risque mal énoncée sont consignées au §5. | - -## 7. Revue de l'architecture - -### 7.1 Découpage en couches et forme - -La bibliothèque compte trois couches propres : **générateurs fluides publics** (fins, par type, scellés, -immuables) → **moteurs de spécification internes** (`OrdinalIntervalSpec`, `WideIntervalSpec`, -`ContinuousIntervalSpec`, `DecimalIntervalSpec`, `StringSpec`, `CountSpec`, `CollectionState`) → -**primitives d'échantillonnage** (`RandomSampling`). La surface publique ne laisse jamais fuir de type -interne ; les moteurs internes ne touchent jamais directement l'état ambiant (les sources sont passées -vers le bas). Les points de composition — `.As(factory)`, `Any.Combine(...)`, les fabriques de -collection — sont tous définis sur l'`IAny` à un seul membre, qui est aussi petit qu'une interface -peut l'être (ISP par construction) et covariant, si bien que les générateurs dérivés et étrangers -traversent chaque point de couture uniformément. - -La **séparation en quatre moteurs est fondée, pas accidentelle** : les types discrets projetables sur -64 bits partagent `OrdinalIntervalSpec` ; les entiers 128 bits ont besoin de `WideIntervalSpec` seulement -parce que netstandard2.0 n'a pas `UInt128` (les deux sont des jumeaux mot pour mot — l'unique -duplication regrettable, forcée par le TFM) ; les flottants IEEE ont besoin d'un échantillonnage continu -avec quantification consciente du type ; `decimal` n'est ni projetable en ordinal (mantisse 96 bits × -échelle) ni IEEE. L'existence de chaque moteur est justifiée par son substrat arithmétique. Ce qui -*manque*, c'est l'ADR qui le consigne (§5), et — comme l'a montré §4.1(b) — une suite de tests paramétrée -exerçant chaque moteur à travers chacune de ses façades de type. - -La **hiérarchie de collections** est un CRTP propre comme un manuel : `AnyCollection` porte la surface fluide partagée de compte/contenance renvoyant `TSelf` (sans le classique cast -non sûr `(TSelf)this` — les types concrets implémentent une fabrique `With(state)`), et les cinq -générateurs concrets n'ajoutent que la mise en forme des éléments et la conversion `Build(List)`. -L'exception est `AnyDictionary`, qui ne peut pas hériter de la base (son élément est une paire) et donc -**duplique toute la façade de compte mot pour mot** (~60 lignes, `AnyDictionary.cs:51-113`) et n'offre -aucune contrainte de contenance — le seul endroit de la famille collection où le partage a échoué. -Extraire la façade de compte au-dessus de `CollectionState` (ou ajouter `ContainingKey`, qui -chevaucherait gratuitement la machinerie d'état de clés existante) refermerait à la fois la duplication -et le trou de test reconnu (`AnyCollectionTests.cs:161-163` le commente). - -### 7.2 Extensibilité - -**Pour les utilisateurs, la conception est fermée, et c'est un choix légitime mais non documenté.** -`IAny` est public, donc n'importe qui peut implémenter un générateur et le composer via -`As`/`Combine`/collections. Mais `RandomSource`, `IHasRandomSource` et `ICardinalityHint` sont tous -internes, si bien qu'un générateur étranger (a) ne peut pas tirer de la source semée ambiante — sous -`Any.Reproducibly` ses valeurs ne rejouent pas, et (b) ne peut pas annoncer un domaine fini — une -collection distincte au-dessus de lui emprunte toujours le chemin du tirage borné (sûr, et exactement ce -que promet l'ADR-0013). La dégradation est gracieuse partout (vérifié : `OrNull` retombe sur la source -ambiante pour le tirage à pile ou face du null ; `Combine` propage les sources `null` sans échouer). Ce -qui manque est un paragraphe honnête sur la doc XML d'`IAny` disant aux implémenteurs où ils se -situent — aujourd'hui le contrat n'est découvrable qu'en lisant du code interne. Si le point de couture -doit s'ouvrir un jour, un `ISeedableAny` dans une release mineure est la forme naturelle ; rien ne -demande à être décidé maintenant, sinon la documentation. - -**Pour les mainteneurs**, ajouter un nouveau type scalaire touche 6 à 9 fichiers (générateur, `Any`, -`AnyContext`, tests, docs utilisateur EN/FR, README du paquet, `justdummies-check` si net8-only, -éventuellement un moteur de spec). Le processus est mécanique mais réel, et n'est que partiellement gardé -(§9.2). - -### 7.3 La machinerie de déterminisme — plongée en profondeur - -L'implémentation est correcte au niveau qu'il est difficile de bien faire (§3.3). Les risques restants -sont tous des risques de *documentation de contrat*, et ils se regroupent en quatre : - -**(a) La concurrence dans un même scope à graine annule silencieusement la rejouabilité — non -documenté.** Un `AsyncLocal` copie la *référence* : les enfants `Task.Run`/`Parallel.ForEach` à -l'intérieur d'un corps `Reproducibly` voient tous la même instance `SeededRandom`. Deux conséquences. -Premièrement, même avec un entrelacement bénin, l'*ordre* des tirages devient dépendant de -l'ordonnanceur, si bien que la graine rapportée ne rejoue plus le run — la garantie même pour laquelle la -fonctionnalité existe. Deuxièmement, `System.Random` n'est pas thread-safe, et netstandard2.0 n'offre -aucune alternative thread-safe ; un tirage mis en course peut corrompre l'état (sur .NET Framework, un -`Random` mis en course peut dégénérer et renvoyer des zéros). Les docs expliquent soigneusement que la -source « ne fuit jamais *entre* les tests qui tournent en parallèle » (vrai — flux logiques différents) -mais ne disent rien du parallélisme *à l'intérieur* d'un corps. Le correctif est un paragraphe honnête -sur `Reproducibly` (« un run semé est à flux logique unique ; les tirages concurrents dans le corps ne -sont ni rejouables ni sûrs ») — plus, optionnellement, consigner dans la nouvelle ADR de déterminisme -pourquoi un forkage par flux (une source enfant par `Task.Run`) n'a pas été tenté (il changerait toute -séquence et compliquerait `WithSeed` ; la restriction honnête est la bonne V1). - -**(b) Les rapports de graine peuvent nommer une graine erronée ou inapplicable dans une composition à -source mixte/fixe.** `Combine` propage la source d'opérande **première non nulle** pour le rapport -d'échec (`Any.Combine.cs:33` et al.). `Any.Combine(Any.WithSeed(1).Int32(), Any.WithSeed(2).Int32(), throwing)` -échoue avec « seeded with 1; reproduce with `Any.Reproducibly(1, …)` » — doublement faux : la graine 2 -n'est pas rapportée, et l'instruction est inapplicable parce que `Reproducibly` épingle la source -*ambiante*, que les générateurs adossés à `FixedRandomSource` ignorent par conception. C'est un cas -limite (mélanger des contextes semés dans une même composition est inhabituel), mais le mode d'échec est -un *diagnostic trompeur avec assurance* dans la bibliothèque dont la signature est l'honnêteté -diagnostique. Un petit correctif l'atteint : laisser le type de source produire l'indice de rejeu -(ambiante → « reproduce with `Any.Reproducibly({seed}, …)` » ; fixe → « ce générateur tire de -`Any.WithSeed({seed})`, qui rejoue déjà de lui-même »), et faire collecter à `Combine` les sources -distinctes plutôt que la première. - -**(c) La stabilité de graine entre versions et entre runtimes n'est ni promise ni écartée.** La -description du paquet dit « any run is reproducible from a reported seed » sans réserve. Dans un même -processus, cela tient. Entre *versions de la bibliothèque*, tout changement d'ordre ou de nombre de -tirages change silencieusement chaque séquence — et l'ADR-0025 reconnaît déjà que les consommateurs -peuvent s'appuyer sur les formes générées. Entre *runtimes*, un `new Random(seed)` semé conserve -l'algorithme historique sur .NET moderne précisément par compatibilité, de sorte que la surface commune -devrait s'accorder entre les assets netstandard2.0 et net8.0 — mais rien ne le teste (§4.6), et la -documentation de `Random` se réserve explicitement le droit que les implémentations diffèrent entre -versions du framework. La politique mature, avant la v1 : **promettre la stabilité au sein d'une version -de paquet, l'écarter entre versions**, une phrase dans le README et dans la nouvelle ADR de déterminisme. -(Pour comparaison : FsCheck et AutoFixture ont tous deux appris à l'écarter explicitement.) - -**(d) L'épinglage ambiant paresseux rend un échec *non enveloppé* seulement approximativement -rejouable.** Hors de `Reproducibly`, le premier tirage dans un flux logique épingle une graine -mémorisée. Les tirages survenus *avant* le bloc fautif dans le même flux (une fixture, un `Arrange` -antérieur) consomment de la même séquence, de sorte que rejouer « juste le corps du test » avec la graine -rapportée peut diverger. La conception est juste (c'est pour cela que `Reproducibly` existe) ; le récit -de rejeu du guide utilisateur pourrait porter une phrase disant que la fidélité de rejeu commence à la -frontière du scope. - -Des non-problèmes vérifiés qu'il vaut la peine de consigner pour ne pas les rejuger : la sémantique -d'`ExecutionContext` de la surcharge asynchrone (correcte — voir §3.3) ; -`NewSeed() = Guid.NewGuid().GetHashCode()` (usage tolérant aux collisions, analysé dans l'ADR-0006) ; -l'étendue de graine sous xUnit (chaque invocation de test est son propre cadre asynchrone ; un -constructeur de classe partagé participe au flux de son test, ce qui est le bon scope) ; la -ré-énumération de `SequenceOf` (matérialisée une fois, ne re-tire jamais). - -### 7.4 SOLID, brièvement et seulement là où cela vaut la peine - -SRP : les générateurs portent la surface fluide, les moteurs portent la sémantique — propre. OCP : -ajouter une *contrainte* à un type discret est l'ajout d'une méthode de façade au-dessus d'une opération -de moteur existante ; ajouter un *type* est délibérément fermé (générateurs scellés, moteurs internes) — -le bon compromis pour une bibliothèque riche en invariants. LSP : la base de collection CRTP est saine -(pas d'astuce d'auto-cast, borne `TSelf` imposée). ISP : `IAny` à membre unique ; les deux membres -d'`ICardinalityHint` voyagent ensemble par conception explicite et documentée (une cardinalité sans -appartenance serait fausse — la doc d'interface l'argumente). DIP est intentionnellement absent au point -de couture utilisateur (aucune abstraction d'aléatoire injectable) — c'est *cela* la décision -d'extensibilité fermée du §7.2, acceptable mais méritant son paragraphe de documentation. - -## 8. Revue de l'API - -### 8.1 L'algèbre de contraintes est uniforme là où cela compte - -La matrice vérifiée : les cinq générateurs d'entiers signés et les quatre générateurs continus/décimaux -exposent exactement `Positive · Negative · Zero · NonZero · GreaterThan[OrEqualTo] · LessThan[OrEqualTo] -· Between · OneOf · Except · DifferentFrom` ; les cinq générateurs non signés retirent exactement -`Positive`/`Negative` (sans objet ici — `NonZero` couvre l'intention) ; les quatre générateurs de type -instant (`DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`) renomment la famille de bornes en -vocabulaire métier (`After`/`AfterOrEqualTo`/`Before`/`BeforeOrEqualTo`/`Between`) avec une sémantique -inclusive/exclusive identique, tandis qu'`AnyTimeSpan` — une magnitude, pas un instant — garde -correctement l'algèbre numérique complète, y compris `Positive`/`Negative`/`Zero` ; `AnyChar` porte les -familles de caractères plus le trio d'exclusion ; `AnyGuid` a -`NonEmpty`/`Empty`/`OneOf`/`Except`/`DifferentFrom` ; `AnyEnum` a le trio d'exclusion avec validation des -membres déclarés ; les collections partagent `NonEmpty · Empty · WithCount · WithMinCount · WithMaxCount -· WithCountBetween · Containing · ContainingAny` (+ variantes `Distinct` là où c'est pertinent). Les -bornes sont uniformément inclusives pour `Between`/`…OrEqualTo` et exclusives pour -`GreaterThan`/`LessThan`/`After`/`Before` — aucune surprise sémantique n'a été trouvée nulle part dans la -matrice. Ce niveau de cohérence sur dix-neuf générateurs d'intervalle écrits à la main — plus leurs -frères chaîne, char, guid, enum, bool et collection — est un accomplissement en soi. - -### 8.2 Les asymétries qui valent d'être corrigées ou consignées - -* **`AnyString` est le seul générateur scalaire sans contraintes d'exclusion** — pas d'`OneOf`, pas - d'`Except`, pas de `DifferentFrom`. « Un nom différent de celui que je détiens déjà » est l'un des - besoins de chaîne factice les plus courants (c'est exactement pourquoi `DifferentFrom` existe partout - ailleurs, selon sa propre doc XML). La raison honnête du trou : les chaînes ne sont pas projetées en - ordinal, donc les exclusions ne peuvent pas chevaucher le moteur d'intervalle ; `DifferentFrom` - nécessiterait soit un retirage borné (collisions attendues ≈ 0 pour toute spec non triviale — cohérent - avec les autres échappatoires bornées de la bibliothèque) soit un ajustement d'agencement conscient de - la spec. Recommandé (§10 Indispensable) : - - ```csharp - // Aujourd'hui — aucun moyen d'exprimer ceci : - string other = Any.String().NonEmpty().Generate(); // pourrait être égal à l'existant ! - // Proposé : - string other = Any.String().NonEmpty().DifferentFrom(existing).Generate(); - ``` - -* **`AnyDictionary` abandonne `Containing`/`ContainingAny`** et duplique la façade de compte (§7.1). - `ContainingKey(TKey)` chevaucherait la machinerie d'état de clés existante sans changement. -* **`Any.Bool()` est la seule déviation de la convention de fabriques aux noms CLR** (`Int32`, `SByte`, - `Single`, … sont tous des noms CLR ; le nom CLR ici est `Boolean`). La forme courte est sans doute la - meilleure ergonomie — mais alors la convention devient « noms CLR, sauf un », et après la 1.0 le - renommage est cassant dans les deux sens. Décider délibérément et consigner une ligne, avant la - publication (le dépôt a des ADR précisément pour cette classe de décision de nommage). -* **`PairOf`/`TripleOf` s'arrêtent à l'arité 3** tandis que `Combine` va jusqu'à 8. Défendable (les - tuples au-delà de 3 se lisent mal ; `Combine` les couvre), mais le point d'arrêt n'est consigné nulle - part — une phrase de doc le referme. - -### 8.3 Découvrabilité et cérémonie - -Le point d'entrée statique `Any.` rend toute la surface scalaire découvrable en une frappe, et les -méthodes fluides de chaque générateur énumèrent tout son vocabulaire de contraintes dans IntelliSense — -bien. Deux points de couture sont moins découvrables : `As` et `OrNull` sont des méthodes d'extension -dans des classes statiques séparées (invisibles tant que le `using` n'existe pas — bien que l'espace de -noms soit partagé, donc en pratique elles apparaissent), et `As` est le `Select` de la bibliothèque sous -un nom d'intention métier ; une ligne de doc faisant le pont depuis le vocabulaire LINQ (« `As` est le -`Select` des générateurs — nommé pour son usage dominant : passer par la fabrique d'un objet-valeur ») -aiderait les lecteurs familiers de LINQ. La cérémonie terminale `Generate()` est le compromis de -l'ADR-0020, consciemment chiffré là-bas ; l'audit confirme que le coût est réel mais petit (un appel par -matérialisation), que le bénéfice (aucune conversion cachée à effet de bord) est structurel, et que la -décision doit tenir. `AnyContext` ne recopie que les scalaires — la composition hérite du contexte via -les sources d'opérandes, ce qui est *plus* élégant que le recopiage et correctement documenté. - -### 8.4 Nommage - -`StartingWith`/`EndingWith`/`Containing`, `After`/`Before`, `DifferentFrom` vs `Except`, `Containing` vs -`ContainingAny` — le vocabulaire est révélateur d'intention et se lit sur le site d'appel comme la -philosophie l'entend. Les fabriques aux noms de type CLR (`Any.Int32()`, pas `Any.Int()`) sont cohérentes -avec les noms de type des générateurs (`AnyInt32`) et contournent les restrictions de mots-clés C# ; -c'est défendable et, plus important, uniforme (le cas `Bool` du §8.2 mis à part). - -## 9. Revue de la maintenabilité - -### 9.1 Duplication, mesurée - -Quatre familles de clones parmi les générateurs numériques (quatuor signé, quatuor non signé, trio -continu, paire large — identiques à l'octet près modulo substitution de type ; ~2 450 lignes), les cinq -générateurs temporels sur le même patron (~800 lignes), la logique de contrainte-et-conflit quadruplée à -travers les quatre moteurs (~910 lignes), et le miroir scalaire `Any`/`AnyContext` (~300 lignes riches en -doc). Une comparaison scriptée n'a trouvé **aucune erreur de copier-coller comportementale** à travers -les familles de clones — preuve d'une vraie discipline — tandis que toute la dérive trouvée jusqu'ici est -une dérive de *documentation* (§4.3), exactement le genre pour lequel les gardes n'existent pas encore. - -### 9.2 Atténuation : des gardes, pas de génériques - -Le refactoring évident — une base générique CRTP (`AnyOrdinal`) — bute sur les contraintes de -ce projet : C# exige une classe de base publique pour un générateur public scellé (CS0060), si bien que -le point de couture du moteur interne fuirait dans l'API publique ; netstandard2.0 n'a pas de -mathématiques génériques (`INumber` est net7+), donc les lambdas `Ord`/`Val`/d'affichage par type -demeurent ; et la barre affichée de la bibliothèque est la simplicité de maintenance, que 14 fichiers -plats, ennuyeux et « greppables » servent mieux qu'une base astucieuse. Les générateurs de source/T4 -achètent la déduplication au prix d'une machinerie de build et d'une débogabilité — mauvais compromis ici -aussi. **Recommandé à la place : des gardes de parité exécutables**, ~3 courts tests par réflexion : - -1. *Parité du miroir :* chaque méthode statique publique `Any` renvoyant un type de générateur a une - contrepartie d'instance `AnyContext` de nom/signature/type de retour identiques, par TFM (~20 lignes ; - tue d'un coup la classe de dérive du §4.3). -2. *Parité de l'algèbre :* chaque famille de générateurs expose son ensemble exact de noms de méthodes - attendus (la matrice du §8.1, encodée une fois comme donnée) — un nouveau générateur privé de - `DifferentFrom`, ou une méthode renommée, échoue avec un diff nommé. -3. *Suite de scénarios transverse aux moteurs :* un fichier de test paramétré passe la même batterie de - scénarios (un tirage pleine-plage touche les deux moitiés ; les bornes de `Between` sont atteignables ; - `DifferentFrom` sur un domaine étroit ; interaction `OneOf`+`Except` ; messages de conflit) contre - **chaque** générateur via de petits adaptateurs par type. C'est la suite qui aurait attrapé §4.1(a) et - §4.1(b) avant toute revue humaine. - -En complément, installer les gardes d'ingénierie de release du §4.7 (référence d'API publique + -validation de paquet) — ils attrapent la classe de changements cassants que les tests de parité ne -peuvent pas. - -### 9.3 Stratégie de test - -Ce qui existe est bien formé : un nommage comportement-d'abord qui se lit comme documentation vivante ; -les *messages* d'exception testés comme des contrats de première classe ; l'oracle regex du vrai moteur ; -des tests de non-régression qui encodent l'historique des bugs (le test de course d'`AnyGuid` qui court -contre une échéance au lieu de bloquer la suite) ; des assertions façon-propriété à l'abri des tests -instables (les tirages non semés assertés seulement contre leur domaine déclaré) ; et une posture -strictement boîte noire — aucun `InternalsVisibleTo` n'existe, si bien que les 222 tests n'exercent que -la surface publique. Ce dernier fait coupe dans les deux sens et devrait être tenu pour un choix -délibéré : il prouve que l'API publique suffit à spécifier la bibliothèque (et rend les refactorings de -moteur transparents aux tests), *et* il est cohérent avec la façon dont les deux défauts d'atteignabilité -ont survécu — aucun test ne regarde directement la couverture de l'espace de valeurs d'un moteur. Les -ajouts qui referment l'écart, par ordre de levier : la suite de scénarios transverse ci-dessus ; des -**assertions d'atteignabilité** (pour chaque générateur, une boucle semée sur `Between(lo, hi)` doit -observer des valeurs dans les deux moitiés et toucher les deux bornes — peu coûteux, déterministe sous -`WithSeed`) ; un test de limite de génération pour `AnyPattern` (actuellement non testée) ; des tests -dédiés aux contrats documentés-mais-non-testés (enum vide, capturabilité de la base `AnyException`, -chemin du comparateur de clés de `DictionaryOf`) ; et l'assertion même-graine inter-TFM dans -`justdummies-check` (étendre `SeedBatch` d'une séquence de référence comparée entre les cibles consommatrices -net8.0 et net6.0, et étendre le test de fumée pour couvrir les tirages -`OrNull`/`SequenceOf`/`PairOf`/`StringMatching`/enum, que le garde-fou d'artefact empaqueté ne touche -actuellement jamais). - -### 9.4 Organisation et hygiène - -La racine plate de 54 fichiers est acceptable aujourd'hui parce que la discipline de nommage fait office -de dossiers (`Any*` = générateurs, `*Spec` = moteurs, `Regex*` = sous-système de motifs) ; regrouper en -dossiers est un polissage optionnel, à ne faire qu'à l'occasion d'un autre changement structurel. Points -d'hygiène trouvés : le membre mort `RegexCharacters.Count` ; la garde-null morte dans -`CollectionState.Exhausted` (ligne §6/ADR-0013) ; les commentaires et DisplayName périmés du §4.3 ; -l'en-tête périmé de `Directory.Build.props` (§4.7). - -## 10. Analyse des manques fonctionnels - -Méthode : chaque proposition a été passée au crible de (i) la philosophie de la bibliothèque (les -contraintes expriment des invariants ; pas de fausses données réalistes, pas de graphes d'objets, pas de -couplage à l'horloge), (ii) le test de composition — *`As`/`Combine`/`StringMatching` peuvent-ils déjà -exprimer ceci en une ligne lisible ?* — et (iii) le coût complet d'un nouveau générateur (générateur + -`Any` + `AnyContext` + données de parité + tests + docs EN/FR + README du paquet + éventuellement -`justdummies-check`). La barre de l'**Indispensable** est celle du mandat : une absence véritablement -surprenante. La conception composition-d'abord de la bibliothèque garde cette liste courte — la plupart -des types BCL sont déjà à un `As` de distance, ce qui est la conception fonctionnant comme prévu. - -### Indispensable - -**1. Un combinateur de choix de premier niveau : `Any.OneOf(params T[])` et -`Any.ElementOf(IReadOnlyList)`.** -Choisir un élément arbitraire dans un ensemble fourni par l'appelant est parmi les besoins factices les -plus courants dans les vraies suites (« l'une des trois devises configurées », « l'un des états de cette -table »). Aujourd'hui `OneOf` n'existe qu'*à l'intérieur* des générateurs typés — il n'y a aucun moyen de -tirer d'un ensemble d'objets métier ou de chaînes. Chaque utilisateur recode les mêmes trois lignes (et -oublie la source semée, cassant silencieusement `Reproducibly` pour ce tirage — un piège que la -bibliothèque existe pour prévenir) : - -```csharp -// Aujourd'hui — codé à la main, et non conscient de la graine : -var currencies = new[] { eur, usd, gbp }; -var currency = currencies[new Random().Next(currencies.Length)]; // graine ambiante ignorée ! - -// Proposé — conscient de la graine, cohérent avec la philosophie, validé immédiatement (ensemble vide → lève) : -Currency currency = Any.OneOf(eur, usd, gbp).Generate(); -Order order = Any.ElementOf(existingOrders).Generate(); -``` - -Constructif (tirage unique), trivialement implémentable sur la source ambiante avec un -`ICardinalityHint` (compte distinct du réservoir — il se compose gratuitement avec les collections -distinctes), recopié sur `AnyContext`. Qui en bénéficie : chaque consommateur, chaque semaine. Coût : un -petit générateur. C'est l'ajout au plus fort levier disponible. - -**2. `AnyString.DifferentFrom(string)` / `Except(params string[])`.** -L'asymétrie du §8.2 : le générateur le plus utilisé est le seul scalaire qui ne puisse pas exclure de -valeurs. Coût honnête : un retirage borné (le patron d'échappatoire établi de la bibliothèque) ou une -exclusion consciente des fragments ; l'un comme l'autre s'inscrit dans le modèle de validation -`StringSpec` existant. Qui en bénéficie : quiconque teste des chemins d'égalité/inégalité avec des -identifiants chaîne — un cas très courant. (`OneOf` sur les chaînes est alors gratuit via la -proposition 1.) - -### Souhaitable - -* **Générateur `Uri`** (`Any.Uri().UsingHttps().WithHost("example.com")`) — le seul type BCL de type - valeur à la fois couramment nécessaire dans les tests et réellement pénible à composer à la main - (règles de validité schéma/hôte/chemin/query). Intégré aux deux TFM. Coût modéré (sa propre mini - algèbre de contraintes) ; un timing guidé par la demande convient. -* **`WithChars(string pool)` / alphabet personnalisé sur `AnyString`** — aujourd'hui le texte non-ASCII - (accents, i18n) n'est atteignable que via des littéraux `StringMatching` ; un réservoir personnalisé - est une petite extension composable du mécanisme de jeu de caractères existant, et débloque le cas - d'usage du code sensible à l'i18n sans aucune machinerie de tables Unicode. -* **`MultipleOf(int)` sur les entiers / `WithScale(int)` sur decimal** — « un montant valide en - centimes », « une quantité en douzaines » : de véritables invariants (pas des assertions) qui forcent - aujourd'hui des contournements `As(x => x * 100)` qui déforment la plage déclarée. Constructif à - implémenter (tirer dans l'espace du quotient). -* **`ContainingKey(TKey)` sur `AnyDictionary`** (§7.1/§8.2) — referme d'un coup un trou d'API, une - duplication et un trou de test. -* **Combinaisons d'enum [Flags], en opt-in** (`Any.Enum().AllowingCombinations()`) — - aujourd'hui les valeurs combinées non déclarées sont inatteignables *par conception* (membres-déclarés- - seulement est le bon défaut) ; un opt-in explicite respecte le défaut tout en servant les domaines - riches en drapeaux. Nécessite une position documentée sur ce que « valide » signifie pour les drapeaux - (union des membres déclarés). -* **`WithOffset`/contrôle d'offset sur `AnyDateTimeOffset`** — la dimension d'offset est actuellement - dégénérée (toujours zéro, documenté) ; les tests qui exercent l'arithmétique d'offset ne peuvent pas la - faire varier. Un tirage d'offset borné (±14 h en minutes, selon les propres règles du type) préserve la - validité. -* **Granularité temporelle** (`WholeSeconds()`/`WholeDays()` ou `WithGranularity(TimeSpan)`) — les - instants à précision de tick sont presque jamais ronds, ce qui surprend les tests qui sérialisent des - horodatages ; constructif via le moteur ordinal (tirer dans l'espace des granules, multiplier). - Referme aussi entretemps le trou de documentation (« les valeurs sont à précision de tick »). -* **Terminal `GenerateMany(int)`** — sucre pour « N valeurs sans la cérémonie `ListOf` » ; une *méthode - nommée* renvoyant `IReadOnlyList`, donc elle reste dans la lettre et l'esprit de l'ADR-0020. -* **Un adaptateur de graine pour framework de test** (`[ReproducibleFact]`) — anticipé par les suivis de - l'ADR-0006, abandonné lors du rebasage, remplacé par rien. JustDummies zéro-dépendance ne peut pas - référencer xUnit, c'est donc une décision de *paquet compagnon* (`JustDummies.Xunit`) — méritant une ADR - explicite oui/non plutôt que le silence, car chaque consommateur re-dérive aujourd'hui à la main - l'habitude d'envelopper dans `Reproducibly`. - -### Idées optionnelles - -`Version` (composable aujourd'hui : -`Combine(Any.Int32().Between(0,99), …, (ma,mi,pa) => new Version(ma,mi,pa))` ; faible fréquence) ; -`IPAddress`/`IPEndPoint` (intégrés, de niche ; une recette de doc d'abord) ; `Encoding` et `CultureInfo` -(faisables **seulement** depuis un réservoir fixe embarqué — l'ensemble des cultures installées est un -danger de reproductibilité entre machines que la bibliothèque ne doit pas hériter ; les deux sont -subsumés par la proposition 1 + un réservoir documenté) ; `MailAddress`, chemins de système de fichiers, -`Stream`, blobs `byte[]` (toutes des recettes d'une ligne sur la surface existante — `ArrayOf(Any.Byte())` -*est* déjà le générateur de blob ; les documenter dans la section recettes du guide utilisateur plutôt -que de livrer des générateurs) ; sucre `KeyValuePair` ; collections `Queue`/`Stack`/`LinkedList` et -`Sorted*` (conversions `As` d'une ligne ; un `Sorted()` de première classe nécessite un gate de -comparabilité analogue à l'indice de cardinalité — la conception existe si la demande apparaît) ; -`BigInteger` (intégré aux deux TFM mais rompt la symétrie « pleine plage sauf contrainte » — il n'y a pas -de pleine plage ; nécessite sa propre position de défaut borné) ; `Rune` (cible net8 ; en conflit avec le -modèle de texte délibérément ASCII-centrique tant que `WithChars` n'a pas atterri) ; sucre -`ContainingAll(params T[])`. - -### Hors périmètre (recommandé de rester absent, avec les raisons) - -* **Filtrage `Where(predicate)`** — générer-puis-filtrer est l'exact opposé du modèle constructif de la - bibliothèque ; des prédicats insatisfiables réintroduisent la classe de reprise non bornée que toute la - conception existe pour exclure. La réponse existante (exprimer l'invariant en contraintes, ou - construire via `As` depuis un tirage contraint) est la philosophie. -* **Enregistrement de générateurs / graphes d'objets façon AutoFixture** — le remplissage automatique - piloté par réflexion est le produit voisin que le README écarte explicitement ; de simples membres C# - sont le mécanisme de réutilisation. -* **Collections immuables** — `System.Collections.Immutable` est un paquet externe sur la cible - netstandard2.0, donc un générateur romprait l'identité zéro-dépendance là-bas ; côté consommateur, - `.As(ImmutableList.CreateRange)` est une ligne. (Une surface net8-only fracturerait l'API entre TFM - pour un gain marginal — ne vaut pas la peine.) -* **`Index`/`Range`** — la validité est contextuelle (dépend de la longueur de la séquence), donc - « arbitraire mais valide » ne peut pas tenir de façon autonome. -* **`RegionInfo`**, **`Complex`** — dépendant de l'environnement resp. de niche scientifique ; les deux - échouent au test de fréquence. -* **Fausses données réalistes** (noms, e-mails, adresses) — explicitement écartées ; Bogus existe. - -## 11. Améliorations recommandées - -Par ordre de priorité ; les points 1–7 sont la porte pré-publication recommandée. - -1. **Corriger les trois défauts reproduits** — construction de la fraction décimale - (`DecimalIntervalSpec.cs:145`), nudge conscient du type (`ContinuousIntervalSpec.cs:189` → - `_nextUp`), garde de débordement du char (`RegexParser.cs:398` + `RegexAlphabet.Range`) ; et la - validation de groupe d'équilibrage/nom dans `SkipGroupName` (§4.1 d). Chacun avec un test de - non-régression. -2. **Ajouter des tests d'atteignabilité et la suite de scénarios transverse aux moteurs** (§9.3) — la - réponse structurelle à la classe de défauts, pas seulement aux instances. -3. **Ajouter les gardes de parité** (§9.2) : test de miroir `Any`↔`AnyContext`, test de matrice - d'algèbre. -4. **Solder le contrat de déterminisme** (§7.3) : documenter le semis à flux logique unique sur - `Reproducibly` ; des indices de rejeu conscients du type de source (et le rapport multi-source de - `Combine`) ; la phrase de politique de stabilité entre versions ; la qualification générateur-étranger - dans le message de saturation (garde-null morte retirée). Rédiger l'**ADR de déterminisme** et l'**ADR - du moteur ordinal** (§5, lacunes structurelles) en `Proposée` pour `@reefact`. -5. **Exécuter JustDummies sur ses planchers** : importer `build/Net472TestFloor.props` dans - `JustDummies.UnitTests` (tests net8-only conditionnés hors scope), l'ajouter à la boucle de plancher de - ci.yml ; ajouter l'assertion de séquence de référence inter-TFM à `justdummies-check` ; énoncer le - plancher .NET Framework dans le README du paquet (suivi de l'ADR-0022). -6. **Passe de documentation** : faire apparaître JustDummies dans le README du dépôt (table des paquets + - sommaire) ; écrire le guide utilisateur JustDummies avec la référence de contraintes par générateur et le - dialecte `StringMatching` (refermant le suivi de l'ADR-0025) ; corriger les trois emplacements « ASCII - imprimable » (§4.2) ; annoncer le comportement vide-par-défaut dans le README du paquet ; corriger les - commentaires/DisplayName périmés (§4.3) et l'en-tête de `Directory.Build.props`. -7. **Gardes d'ingénierie de release** : référence d'API publique (`PublicApiAnalyzers`) et - `EnablePackageValidation` ; décider `Bool()` vs `Boolean()` et le consigner ; demander à `@reefact` - de trancher le statut de l'ADR-0025 (après sa correction de formulation) ; consigner les deux - clarifications de l'ADR-0026 dans la référence d'implémentation ; enrichir ou adoucir les renvois à la - référence d'implémentation des ADR-0013/0015. -8. **Livrer les deux fonctionnalités Indispensables** (§10) : `Any.OneOf`/`Any.ElementOf`, et les - exclusions de chaîne (`DifferentFrom`/`Except` sur `AnyString`). -9. **`AnyDictionary`** : extraire la façade de compte partagée ; ajouter `ContainingKey`. -10. **Ensuite, guidé par la demande** : la liste Souhaitable (§10), chacune sur preuve de besoin, avec les - données de garde de parité mises à jour comme partie du « terminé » de chaque ajout. - -## 12. Feuille de route proposée - -**Phase 0 — avant la première release `dum-v*` (correction et contrat).** Points 1–7 ci-dessus. La -justification est celle de l'ADR-0020 : chacun de ces points est bon marché maintenant et coûteux après -adoption — le correctif décimal change chaque séquence semée (un non-événement aujourd'hui, un événement -de compatibilité après la v1) ; la politique de déterminisme, le nommage `Bool`, la référence d'API et -les statuts d'ADR sont tous des décisions d'une ligne ou d'un fichier qui deviennent des migrations plus -tard. Critère de sortie : la table des faiblesses du §4 est vide, sauf les points explicitement différés -par décision consignée. - -**Phase 1 — premier cycle stable (complétude au sein de la philosophie).** Point 8 (les deux -Indispensables, additifs et à faible risque), point 9, la section recettes du guide utilisateur (blobs, -chemins, Version, Uri-via-Combine — transformant les types de la liste Optionnelle en documentation -plutôt qu'en surface), et la décision de paquet compagnon `JustDummies.Xunit` (oui ou non, en ADR). - -**Phase 2 — croissance guidée par la demande.** Les Souhaitables au fur et à mesure que de vraies -demandes arrivent (`Uri` et `WithChars` d'abord, au vu des preuves actuelles), chaque ajout portant son -entrée de matrice de parité, ses tests et ses docs EN/FR comme une seule unité. Revisiter la liste -Optionnelle chaque année ; résister à la liste Hors périmètre en permanence — c'est ce qui garde cette -bibliothèque telle qu'elle est. - -## 13. Conclusion - -JustDummies est ce à quoi ressemble une bibliothèque focalisée quand les auteurs savent exactement à quoi -elle sert et — tout aussi important — à quoi elle ne sert pas. Le moteur en espace ordinal, les -diagnostics à provenance de contraintes, la discipline d'échappatoires bornées et la trace d'ADR sont -tous meilleurs que la norme de la catégorie, et la conception composition-d'abord garde honnête la -surface de fonctionnalités future : la plupart des « types manquants » sont correctement à un `As` de -distance, pas à un générateur de distance. - -Les constats de l'audit se concentrent en un seul endroit : l'espace entre le comportement *déclaré* et -le comportement *atteignable*. Deux des trois défauts reproduits vivent exactement là, invisibles à une -suite fondée sur la seule appartenance ; les surfaces miroir dérivent exactement là où aucune garde ne -regarde ; la promesse de déterminisme est saine précisément jusqu'aux bords qu'aucun document ne décrit. -Tout cela est corrigeable de ce côté-ci de la première publication, la plupart en quelques jours, et les -points de plus grande valeur ne sont pas les correctifs mais les gardes — la suite d'atteignabilité, les -tests de parité, la référence d'API — qui rendent impossible de livrer silencieusement le prochain défaut -de chaque classe. - -La Phase 0 faite, c'est une bibliothèque qui peut promettre de façon crédible ce que dit son README : -arbitraire mais valide, des conflits nommés à la ligne qui les a causés, et tout run rejouable depuis une -graine rapportée — sur chaque cible pour laquelle elle est livrée. - -## 14. Suivi des issues - -Les recommandations de la §11 ont été ouvertes en issues GitHub le 2026-07-20, sur le gabarit d'issue -JustDummies du dépôt. Cette table est un **instantané figé** : l'état vivant de chaque issue (ouverte, fermée, -en cours) vit dans le tracker, pas ici — ne pas maintenir de statut dans ce document. - -| Point §11 | Issue(s) | Phase (§12) | -|---|---|---| -| 1 — Corriger les défauts reproduits | [#206](https://github.com/Reefact/first-class-errors/issues/206) AnyDecimal moitié haute · [#207](https://github.com/Reefact/first-class-errors/issues/207) nudge Single/Half · [#208](https://github.com/Reefact/first-class-errors/issues/208) blocage U+FFFF · [#209](https://github.com/Reefact/first-class-errors/issues/209) groupes d'équilibrage · [#210](https://github.com/Reefact/first-class-errors/issues/210) bords regex mineurs | 0 | -| 2 — Atteignabilité + suite transverse | [#213](https://github.com/Reefact/first-class-errors/issues/213) | 0 | -| 3 — Gardes de parité | [#214](https://github.com/Reefact/first-class-errors/issues/214) | 0 | -| 4 — Solder le contrat de déterminisme | [#216](https://github.com/Reefact/first-class-errors/issues/216) doc contrat + ADR · [#217](https://github.com/Reefact/first-class-errors/issues/217) ADR moteur ordinal · [#211](https://github.com/Reefact/first-class-errors/issues/211) rapport de graine · [#212](https://github.com/Reefact/first-class-errors/issues/212) message de saturation | 0 | -| 5 — Exécuter sur les planchers | [#215](https://github.com/Reefact/first-class-errors/issues/215) | 0 | -| 6 — Passe de documentation | [#218](https://github.com/Reefact/first-class-errors/issues/218) README + guide utilisateur · [#219](https://github.com/Reefact/first-class-errors/issues/219) ASCII imprimable & docs périmées | 0 | -| 7 — Gardes d'ingénierie de release | [#221](https://github.com/Reefact/first-class-errors/issues/221) baseline API · [#222](https://github.com/Reefact/first-class-errors/issues/222) nommage Bool · [#220](https://github.com/Reefact/first-class-errors/issues/220) hygiène ADR | 0 | -| 8 — Livrer les Indispensables | [#223](https://github.com/Reefact/first-class-errors/issues/223) Any.OneOf/ElementOf · [#224](https://github.com/Reefact/first-class-errors/issues/224) exclusions AnyString | 1 | -| 9 — AnyDictionary | [#225](https://github.com/Reefact/first-class-errors/issues/225) | 1 | -| 10 — Souhaitables guidés par la demande | [#226](https://github.com/Reefact/first-class-errors/issues/226) backlog | 2 | - ---- - -*Produit par un audit mené par agents (revue multi-agents avec vérification contradictoire ; tous les -défauts rapportés reproduits indépendamment contre la bibliothèque compilée ; suite de tests complète -exécutée). Consultatif au sens de l'ADR-0004 : recommandations et brouillons seulement — chaque décision -demeure au mainteneur.* diff --git a/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md b/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md deleted file mode 100644 index 50b190be..00000000 --- a/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md +++ /dev/null @@ -1,981 +0,0 @@ -# JustDummies — Architecture & Design Audit - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./2026-07-20-dummies-architecture-and-design-audit.fr.md) - -**Date:** 2026-07-20 -**Audited revision:** `3bf89e3` (tip of `main` at audit time) -**Scope:** the `JustDummies` library only — `JustDummies/`, `JustDummies.UnitTests/`, its guard tooling -(`tools/justdummies-check/`, `.github/workflows/justdummies.yml`), its documentation, and the ADRs that govern it. -**Status:** advisory. Per the repository's own convention (ADR-0004), this audit produces -recommendations, never blockers; every proposed ADR change is a draft for `@reefact` to accept or reject. - -**Method.** The whole library source (~8,700 lines across 54 C# files) and test suite (~2,500 lines, -17 files) were read; all 26 ADRs were classified for applicability and the 8 applicable ones reviewed -for both intrinsic quality and implementation compliance; findings were adversarially verified against -the code, and the three behavioral defects reported below were **independently reproduced at runtime** -against the built library. The full unit-test suite was executed: **222/222 pass** (`dotnet test -JustDummies.UnitTests`, net10.0 runner). Judgments are calibrated against the library's stated goals — -readable tests, expressive test data, opt-in determinism, a fluent discoverable API, simplicity — and -deliberately *not* against the goals of property-based-testing or fuzzing frameworks, which this -library explicitly is not. - ---- - -## 1. Executive Summary - -JustDummies is a young library built to an unusually high standard. Its central architectural idea — map -every discrete type into a shared 64-bit ordinal space so that one engine owns bounds, exclusions, -conflict detection, and sampling for thirteen builders at once — is elegant and correctly executed. -Its error-message discipline (every conflicting constraint names *both* sides, every generation -failure names the seed that replays it) is better than that of most mature libraries in this space. -Its ADR base is exemplary: decisions are recorded with honest constraints, real alternatives, and -priced trade-offs. - -The audit nevertheless found **three genuine behavioral defects**, all reproduced at runtime: - -1. **Critical — `AnyDecimal` can never generate the upper half of its range.** A fraction intended - to be uniform in [0, 1) is constructed from three 31-bit draws against a 96-bit denominator and - tops out near 0.5; `Any.Decimal().Between(0m, 100m)` never exceeds ~49.9999 - (`DecimalIntervalSpec.cs:145`). -2. **Major — `AnySingle`/`AnyHalf` exclusion nudge stalls.** The exclusion-collision walk steps by a - *double* ulp instead of the type's own ulp, so quantization lands back on the same value and a - satisfiable spec such as `Any.Half().Between((Half)1f, (Half)1.001f).DifferentFrom((Half)1f)` - throws `AnyGenerationException` for ~half of all seeds (`ContinuousIntervalSpec.cs:189`). -3. **Major — a regex character-class range ending at `￿` hangs forever.** The class-expansion - loop increments a 16-bit `char` that wraps at `0xFFFF`, so - `Any.StringMatching(@"[ -￿]")` never returns (`RegexParser.cs:398`). - -All three share one root cause worth naming: **the test suite asserts membership, never -reachability.** Tests check that generated values satisfy the constraints; no test checks that the -whole declared domain is reachable, or that a declared-satisfiable spec actually generates. That is -the precise blind spot in an otherwise well-designed suite, and closing it matters more than any -individual fix. - -One framing fact softens all of this considerably: **JustDummies has never been released.** There is no -`dum-v*` tag; the changelog holds only an empty *Unreleased* section. Every defect above can be fixed, -and every contract decided, with zero compatibility cost. This audit's headline recommendation is to -treat the pre-1.0 window the way ADR-0020 did — as the cheapest moment to decide — and close the -items in §11–§12 before the first publication. - -Beyond the defects, the significant findings are: the hand-mirrored `Any`/`AnyContext` surface and -the fourteen cloned numeric builders carry **no parity guard** (and documentation drift has already -begun); the **determinism contract has documentation gaps** (concurrent draws inside one seeded scope -silently void replayability; cross-version seed stability is neither promised nor disclaimed; the -contract's ADR anchoring was lost when ADR-0006 was superseded); the **netstandard2.0 leg is never -executed by JustDummies' own test suite** (only transitively, via FirstClassErrors' floor job); and there -is **no user-facing reference of the constraint surface** — the repository README does not even -mention the package. The feature-gap analysis (§10) finds the type coverage genuinely complete for -the library's philosophy; the two absences that qualify as surprising are a *top-level* choice -combinator (`Any.OneOf(params T[])` / `Any.ElementOf(...)`) and exclusion constraints on -`AnyString` — the only scalar builder without them. - -## 2. Overall Assessment - -**Verdict: a very strong pre-release library — architecture and process are its strengths; value-space -correctness testing is its one systemic weakness.** - -Judged area by area against the stated goals: - -| Area | Assessment | -|---|---| -| Architecture | Excellent. Clean layering (public builders → internal specs → sampling), one shared ordinal engine, principled engine split, composition seams over one tiny interface. | -| API design | Excellent, with a handful of deliberate-looking but unrecorded asymmetries (§8). | -| Error diagnostics | Exceptional — the library's signature strength. | -| Determinism | Sound design, correctly implemented at the `AsyncLocal`/`ExecutionContext` level; contract under-documented at its edges (§7.3). | -| Correctness | Three reproduced defects, two of them in exactly the code a membership-only test suite cannot see (§4.1). | -| Testing strategy | Well-shaped (behavior-first, black-box, oracle-backed for regex, flake-safe) but reachability-blind (§9.3). | -| Documentation | XML docs outstanding; user-facing documentation thin and hard to discover (§4.4). | -| Maintainability | Duplication is large but disciplined (zero copy-paste slips found in the clone families); the risk is unguarded drift, not present-day rot (§9). | -| ADR base | Exemplary quality; two structural gaps — the determinism contract and the ordinal engine have no ADR of their own (§5). | - -The overall shape is characteristic of a library written with great care by a small number of hands: -the *decisions* are consistently right and consistently recorded, while the safety nets that protect -those decisions from future hands (parity guards, reachability tests, API baselines) are not yet in -place. Pre-1.0 is the moment to install them. - -## 3. Strengths - -These are earned, verified against the code, and worth preserving deliberately. - -### 3.1 The ordinal-space unification - -Every discrete type — all ten 64-bit-or-narrower integers, `DateTime`, `DateTimeOffset`, `TimeSpan`, -`DateOnly`, `TimeOnly` — maps order-preservingly into unsigned 64-bit ordinal space -(`OrdinalMapping.FromInt64` flips the sign bit; `OrdinalIntervalSpec.cs:9-23`) and shares **one** -engine for bounds, allow-lists, exclusions, conflict detection, cardinality, and sampling -(`OrdinalIntervalSpec`). The exclusion algorithm is exact — a drawn index is mapped onto the k-th -non-excluded ordinal in a single pass over a sorted exclusion list (`OrdinalIntervalSpec.cs:194-202`) -— so generation is one draw, never draw-and-retry. A fix to a conflict message or an edge case -reaches every discrete builder simultaneously. This is the right level at which to be DRY: the -*logic* is shared while the thin per-type facades stay simple and readable. - -### 3.2 Constraint provenance and eager validation - -Every bound remembers the constraint string that set it (`"Between(1, 6)"`, `"Positive()"`), so a -conflict names **both** sides at the moment of declaration: - -``` -Cannot apply LessThan(10) because GreaterThan(100) already requires values greater than or equal to 101. -``` - -The discipline holds uniformly across every builder and spec engine, including -cross-cutting validations one would not expect to find (a `Numeric()` charset rejects a prefix -containing letters, naming the offending character — `StringSpec.cs:254-275`). Combined with eager -satisfiability checking ("a generator that exists can always generate"), an impossible `Arrange` -fails at the line that wrote it, not at some later draw. This is the library's signature, and it is -executed consistently. - -### 3.3 The determinism machinery is done right at the hard level - -The linchpin — generators store a `RandomSource` and resolve `.Current` only at `Generate()` time — -is what lets a recipe built outside `Any.Reproducibly(...)` generate deterministically inside one -(`RandomSource.cs:3-9`). The `AsyncLocal` scope semantics were scrutinized closely and hold: the -sync overload's `using`-restore is correct; the async overload's `UseSeed` mutation cannot leak to -the caller (an async method's `ExecutionContext` mutations do not flow back); nesting restores the -outer scope untouched; `ConfigureAwait(false)` is immaterial to `ExecutionContext` flow. The seed is -reported end-to-end: a user factory that throws inside `.As(...)` produces an -`AnyGenerationException` naming the generated value *and* the seed (`AnyDerivation.cs:59-73`); a -distinct-collection exhaustion does the same (`CollectionState.cs:246-254`). - -Two subtle dual-target traps were caught in advance and documented at the exact point of danger: -`RandomSampling`'s inclusive sampler is deliberately *not* named `NextInt64` because on the net8.0 -leg the framework's own exclusive-bound instance method would win overload resolution and silently -change semantics (`RandomSource.cs:129-137`); and the `OrNull` extension is split into two classes -because `struct`- and `class`-constrained overloads of one name would collide -(`NullableExtensions.cs:47-52`). This is the kind of care that cannot be retrofitted. - -### 3.4 Bounded escapes everywhere — no unbounded retry anywhere - -The library's "built to satisfy, never generate-then-filter" claim survives scrutiny with three -honest, ADR-recorded exceptions, each *bounded*: the distinct-collection dedup draw (budgeted, -coupon-collector-generous, reset-on-progress — `CollectionState.cs:236-244`), the continuous-domain -exclusion nudge (walks to the neighbouring representable value), and `AnyGuid`'s collision escape (a -full-width carry increment that provably terminates — `AnyGuid.cs:27-36`). Each failure mode -produces an actionable, seed-bearing message instead of a hang. - -### 3.5 Care at the edges - -Small things that reveal the quality bar: `AnyDateTime.OneOf` remembers callers' original values so -the ordinal round-trip does not silently normalize `DateTimeKind` (`AnyDateTime.cs:124-131`); -collection layout is Fisher-Yates-shuffled so a dummy collection never advertises a positional -invariant a test might accidentally rely on (`CollectionState.cs:46-53`); `CountSpec` and -`StringSpec` saturate rather than overflow on huge declared minima; `IAny` covariance means -the read-only collection interfaces (`IReadOnlyList`, etc.) are served for free. - -### 3.6 The regex subsystem is well-built for its decided scope - -The hand-written recursive-descent parser (`RegexParser.cs`, 457 lines — the largest single piece of -logic in the library) is well-structured, why-commented, and disciplined about its two-channel -rejection taxonomy: `ArgumentException` for malformed patterns, `UnsupportedRegexException` naming -the construct and position for well-formed-but-non-regular ones. The test suite validates generated -strings against the **real .NET regex engine as an oracle** over a fixed-seed corpus — exactly the -right way to test a generator. (Defects found at its edges are cataloged in §4.1 and §4.2; they do -not change the assessment that the ADR-0025 approach was sound and honestly argued.) - -### 3.7 Packaging, boundary, and process - -The zero-dependency, error-agnostic boundary is enforced three ways: a `.csproj` comment stating the -rule, an intent-based architecture test that fails on any non-BCL assembly reference -(`ArchitectureTests.cs:27-37`), and the `justdummies-check` packaged-asset guard — a real consumer -program, run in CI against the *packed artifact* per target framework, that proves the net8.0 asset -carries the modern generators, the netstandard2.0 asset does not, constraints and conflicts behave, -and same-seed contexts replay (`tools/justdummies-check/Program.cs`). Packaging itself is -production-grade: SourceLink with embedded untracked sources, deterministic CI builds, snupkg -symbols, an SPDX SBOM embedded at pack time, provenance-attested release assets -(`Directory.Build.props:10-23`, `JustDummies.csproj:60-66`, `release.yml`). The ADR base recording all -of this is discussed in §5 — it is a strength in itself. - -### 3.8 The API philosophy is coherent and documented where users look - -The "constraints express what the surrounding code *requires*, never what the test asserts" idea is -stated on the entry point, on every builder, in the package README, and in the user guide — the same -sentence, deliberately. The no-clock-relative-constraints stance (`AnyDateTime` has no -`InThePast()`) is documented at every point a user would look for it, with the reproducibility -rationale attached. Intention-revealing near-synonyms are honestly explained: `DifferentFrom(x)` is -documented as semantically `Except(x)` with a name that carries intent; `Containing` (a value known -now) vs `ContainingAny` (a generator drawn at build time) is a genuinely useful distinction. - -## 4. Weaknesses - -Ordered by severity. Items 4.1 and 4.3 are the ones that should gate a first release. - -### 4.1 Reproduced behavioral defects - -**(a) `AnyDecimal` never reaches the upper half of any range — critical.** - -`DecimalIntervalSpec.cs:144-149`: - -```csharp -// A uniform-enough fraction in [0, 1): 93 random bits over the full decimal mantissa scale. -decimal fraction = new decimal(random.Next(), random.Next(), random.Next(), false, 28) / MaxFraction; -decimal mid = _min / 2 + _max / 2; -decimal half = _max / 2 - _min / 2; -decimal candidate = Clamped(mid + (fraction * 2 - 1) * half); -``` - -`Random.Next()` returns a non-negative `int`, so the top bit of **each 32-bit limb** of the 96-bit -mantissa is always zero, while `MaxFraction` is the *full* 96-bit mantissa maximum -(`7.9228…`, `DecimalIntervalSpec.cs:14`). The fraction therefore lives in [0, ~0.49999986], not -[0, 1); `(fraction * 2 - 1)` lives in [−1, ~0); and every candidate lands in `[min, mid)`. The -inclusive maximum documented on `AnyDecimal.Between` (`AnyDecimal.cs:112`) is unreachable — as is -everything above the midpoint. Independently reproduced for this audit: the maximum of 200,000 draws -of `Any.Decimal().Between(0m, 100m)` was **49.99992…**. - -Why it matters beyond the obvious: a test using `Any.Decimal().Between(0m, 100m)` to exercise "any -valid percentage" silently never exercises 50–100 — the library's core promise ("arbitrary yet -valid, so hidden assumptions surface") is inverted into a hidden assumption of its own. The fix is -small: build the fraction from 96 genuinely uniform bits, e.g. - -```csharp -// after: 12 random bytes fill all three 32-bit limbs uniformly -// (the decimal ctor reads the int limbs as raw 32-bit patterns) -byte[] limbs = new byte[12]; -random.NextBytes(limbs); -decimal fraction = new decimal( - BitConverter.ToInt32(limbs, 0), - BitConverter.ToInt32(limbs, 4), - BitConverter.ToInt32(limbs, 8), - false, 28) / MaxFraction; -``` - -(any construction that fills all 96 mantissa bits uniformly is fine — the current three `Next()` -calls fix the top bit of each limb at zero and can never draw a limb of `2^31−1`), then add the -reachability test from §11 item 2. Note the comment's own claim ("93 random bits over the full -mantissa scale") documents an intent the code does not meet — and even 93 well-placed bits would -not reach a 96-bit denominator's upper octant. - -**(b) `AnySingle`/`AnyHalf` exclusion nudge stalls on satisfiable specs — major.** - -`ContinuousIntervalSpec.cs:188-198`: when a drawn value collides with an excluded point, the walk -steps with the **static, double-space** `NextUp` (line 189) instead of the *type-aware* `_nextUp` -lambda that `AnySingle`/`AnyHalf` supply precisely for stepping in their own representable ladder -(`AnySingle.cs:20`, `AnyHalf.cs:22`) — and which the exclusive-bound paths already use correctly -(lines 120, 125). One double-ulp above a representable `float`/`Half` re-quantizes to the same -value, the `next > _max` escape at line 190 is unreachable (`Quantized` clamps to `_max` first, -lines 203-209), so the 128-step budget burns and a *satisfiable* spec throws. Independently -reproduced: `Any.Half().Between((Half)1f, (Half)1.001f).DifferentFrom((Half)1f).Generate()` threw -`AnyGenerationException` for **250 of 500 seeds**; the identical `AnyDouble` scenario never throws -(its quantize is identity). The fix is one token — `Quantized(_nextUp(candidate))` — plus a -regression test per continuous type. - -This defect is worth a design note: it is exactly the failure class the library's own architecture -predicts. The engine was parameterized by `quantize`/`nextUp` lambdas *because* narrow types must -step in their own ladder; one call site inside the same file forgot the parameter. A -cross-engine, parameterized scenario suite (§9.3) is the structural answer. - -**(c) A character-class range ending at `￿` hangs forever — major.** - -`RegexParser.cs:398`: - -```csharp -for (char character = low; character <= high; character++) { set.Add(character); } -``` - -When `high == '￿'` (reachable through the supported `\uHHHH` escape), the 16-bit `char` wraps -to `0x0000` and `character <= high` is always true. Independently reproduced: -`Any.StringMatching(@"[ -￿]")` did not return within five seconds (hard hang), while the -same pattern is valid .NET regex. A declaration-time hang is the worst failure mode this library -can exhibit — its identity is *failing fast with a named cause*. Fix: guard the wrap -(`if (character == high) break;` inside the loop, or iterate an `int`), and mirror the check in the -private twin loop `RegexAlphabet.Range` (`RegexAlphabet.cs:66-71`) for defense in depth. - -**(d) Balancing groups and invalid group names are silently accepted — major, contract-breaking -direction.** - -`SkipGroupName` (`RegexParser.cs:295-300`) scans to the terminator with no validation. Consequently -`(?<-a>x)` — a *balancing group*, non-regular, same family as the backreferences the library -proudly rejects — is treated as an ordinary named group: `Any.StringMatching(@"(?y)?(?<-a>x)")` -generates `"x"`, which the real engine does **not** match (verified: the pattern's language is -exactly `{"yx"}`). Invalid group names (`(?x)`) are likewise accepted where .NET rejects them. -This is the single place the audit found where the library's signature promise — *"a clear error -beats a value which does not actually match"* (ADR-0025) — is broken. The fix is local: validate -the captured name (reject `-` as `Unsupported("a balancing group …")`, reject non-word characters -as `Malformed(...)`). - -**(e) Minor defects in the same subsystem.** The generation-limit exception blames "a nested -unbounded quantifier" even when the true cause is a large *bounded* quantifier -(`(a{1000}){1000}` — message asserts a diagnosis that is false; `RegexNode.cs:31-37`); a few -patterns the real engine accepts are conservatively refused (`^*`, `abc$$` — while `^^abc` is -accepted, an avoidable asymmetry; a leading `-[` in a class is misread as subtraction); and a -well-formed negated class whose members lie outside the printable universe is misclassified as -*malformed* instead of *unsupported*. All of these fail in the safe direction (refusal, never -mis-generation) and are cosmetic next to (c) and (d). - -### 4.2 The "printable ASCII" claim is overstated in three places - -`RegexAlphabet.cs:3-9`, `AnyPattern.cs:21`, and `Any.Pattern.cs:23` all claim every terminal resolves to -printable ASCII (0x20–0x7E). The code — correctly — emits exactly the characters the pattern -demands: `\t`, `\a`, `\cA`, `\0`, and `\uHHHH` literals can be non-printable or non-ASCII, and the -library's own test asserts it (`AnyPatternTests` — `\a` → U+0007). The restriction genuinely -applies only where the pattern leaves the character *free* (shorthands, the dot, negated classes). -Since ADR-0025 explicitly declares the character universe a behavior consumers may rely on, the -three doc sites should say precisely that (§11 item 6). - -### 4.3 Hand-mirrored surfaces with no parity guard, and drift has already begun - -Two mirror structures must agree method-for-method, and nothing checks either: - -* **`Any` ↔ `AnyContext`**: every scalar entry point exists twice (21 on the netstandard2.0 leg, 26 - on net8.0, counting both `StringMatching` overloads) — `Any.Primitive.cs:11-224`, - `Any.Pattern.cs:34-52` and `Any.Uri.cs:13` vs `AnyContext.cs:51-305`. - The mirror is legitimate design (composition and collections are deliberately *not* mirrored — - they inherit a context through operand sources, which is elegant), but a new scalar type added to - `Any` and forgotten on `AnyContext` would compile, pass all 222 tests, and ship a hole in the - deterministic surface. Wording drift is already visible inside `AnyContext` itself (two different - determinism phrasings across its factories; its `Guid()` doc mentions `Any.Reproducibly`, which a - fixed context ignores by design). -* **The fourteen numeric builders** are byte-identical clones modulo type substitution (~2,450 - lines; the signed quartet, unsigned quartet, continuous trio, and wide pair; the five temporal - builders follow the same pattern for ~800 more). To the clone families' credit, a scripted scan - found **zero copy-paste slips** in the code itself — but three XML summaries say "Same constraint - algebra as `AnyInt32`" on builders where it is literally false (unsigned types lack - `Positive`/`Negative`; temporal types rename the bound family), and three test DisplayNames still - claim generators "convert implicitly to their value type" - (`AnyContinuousTests.cs:108`, `AnySignedIntegerTests.cs:87`, `AnyUnsignedIntegerTests.cs:76`) — - conversions ADR-0020 removed. A stale comment in `SeedReproducibilityTests.cs:17-18` explains - code by those same removed conversions. - -The absence of guards is the finding; the mitigation analysis and recommendation (reflection-based -parity tests, *not* a generic base class) is in §9.2. - -### 4.4 Documentation reaches neither the discoverer nor the power user - -* The **repository README never mentions JustDummies** (verified: zero occurrences), while the package - README points to the repository for "full documentation". A NuGet discoverer lands on a front - page about a different library; the closest thing to a JustDummies guide - (`ArbitraryTestValues.en.md`) is a FirstClassErrors.Testing integration guide that defers back to - "documented with JustDummies itself" — a circular reference. -* **No user-facing reference documents the per-builder constraint surface.** Where does a user - learn that `Except`/`OneOf`/`DifferentFrom` exist on numerics, that `WithLengthBetween` exists, - that `ContainingAny` differs from `Containing`, or which regex dialect `StringMatching` supports? - Today: only IntelliSense, one builder at a time. ADR-0025's own follow-up ("document the - supported dialect") is still open. -* The **empty-by-default surprise** (an unconstrained collection can have 0 elements, an - unconstrained string can be empty) is well-documented in XML remarks but absent from the package - README, where a skimming user would most benefit from it — it is a deliberate, - philosophy-bearing choice ("a test iterating an unconstrained collection zero times is a hidden - assumption surfacing") and deserves to be advertised as such. - -### 4.5 Determinism contract gaps (documentation, not implementation) - -Detailed in §7.3: concurrent draws inside one seeded scope silently void replayability (and race a -non-thread-safe `System.Random`) — documented nowhere; seed reports can name a wrong or -inapplicable seed for fixed-context and mixed-source compositions; cross-version and cross-TFM -seed-sequence stability is neither promised nor disclaimed; and the whole contract lost its ADR -anchor when ADR-0006 was superseded. - -### 4.6 The netstandard2.0 leg is never executed by JustDummies' own suite - -`JustDummies.UnitTests` targets net10.0 only. The netstandard2.0 assembly — the one .NET Framework -consumers will load — is exercised only *transitively*: the FirstClassErrors floor job -(`ci.yml:98-115`) runs `FirstClassErrors.UnitTests` on net472, which arranges with `JustDummies.Any` -via project reference and the Testing factories, so JustDummies does load and generate on the real -.NET Framework CLR — but its own 222-test contract suite (regex oracle, conflict detection, -distinctness gating, seed reproducibility) never runs there, and same-seed-same-values across the -two packaged assets is asserted nowhere. The repository already owns the exact machinery needed -(`build/Net472TestFloor.props`, used by `FirstClassErrors.UnitTests`); extending it to -`JustDummies.UnitTests` (with the net8-only tests conditioned out) is mechanical. See ADR-0022 -compliance, §6. - -### 4.7 Release-engineering guardrails not yet installed - -No public-API baseline (`Microsoft.CodeAnalysis.PublicApiAnalyzers`), no -`EnablePackageValidation`/ApiCompat. The changelog commits JustDummies to semantic versioning while the -audit itself demonstrates the API surface is hand-mirrored and already drifting in documentation; -breaking-change detection against a shipped baseline is the complementary mechanism parity tests -cannot replace (a removed overload or narrowed return type passes a mirror test). Pre-first-release -is the cheapest moment to install both. One stale comment found here: `Directory.Build.props:3-9` -says the repository ships "FirstClassErrors and FirstClassErrors.Testing" — it omits JustDummies, the -very package those pack-time properties now also govern. - -## 5. ADR Review - -Eighteen of the twenty-six ADRs do not concern JustDummies (they name the analyzers, the request -binder, GenDoc/CLI tooling, the Outcome API, or repository process). Eight apply, and their quality -was reviewed individually. The overall standard is high enough to say plainly: this ADR base is a -model of the form. Decisions carry honest constraints, genuinely-considered alternatives, priced -negatives, and follow-ups that were actually executed. - -### ADR-0006 — Supply arbitrary test values from a single seedable source *(Superseded)* - -**Quality: exemplary, historically.** The constraints were real (zero-dependency promise, -netstandard2.0 parallel-test safety without `Random.Shared`), the four alternatives were fairly -weighed, and its follow-ups (extract the engine when a second consumer appears; consider an xUnit -adapter) were honored or consciously deferred. Its collision-risk analysis of the unseeded default -is exactly the right depth. **Issue:** its supersession created a gap — see "structural gaps" below. - -### ADR-0011 — Host JustDummies as a standalone package *(Accepted)* - -**Quality: good.** The name/identity/boundary reasoning is sound and the no-reference rule is -machine-checked. Two precision nits. First, the *enforced* invariant is stronger than the *recorded* -one: the architecture test forbids **any** non-BCL reference (`ArchitectureTests.cs:27-37`), and -ADR-0025 leans on a "zero-dependency identity … the boundary is machine-checked (ADR-0011)" — but -ADR-0011's decision text only forbids referencing *FirstClassErrors projects*. The -zero-*third-party*-dependency rule, load-bearing for ADR-0025's whole argument, is written down -nowhere as a decision. Second, the alternatives never weigh the risks of the ultra-generic NuGet ID -`JustDummies` (squat/collision/searchability) — a package identity the ADR itself calls costly to -rename. Neither nit changes the decision; both deserve a line in the record. - -### ADR-0013 — Gate distinct collections by cardinality, else bounded draw *(Accepted)* - -**Quality: outstanding.** The soundness argument — count only the elements the generator must -supply, credit `Containing` values outside its domain, treat opaque `ContainingAny` draws -conservatively, let the bounded draw be the final safety net — is stated in the document and -provably mirrored in the code (`CollectionState.Validate`/`CardinalityCap`/`FixedOutsideCount`). -The risks section even anticipates budget mistuning and instructs "revise based on evidence rather -than describing failure as impossible." **Issue (shared with ADR-0015):** it defers "the exact hint -interface, collection state, draw budget, exception payload, and seed propagation" to the -implementation reference — but the reference's JustDummies section -(`adr-implementation-reference.md:58-68`) records none of those specifics (no budget numbers, no -exception payload, no seed-propagation rule). The pointer promises more than the destination holds; -either enrich the reference or soften the pointer. - -### ADR-0015 — Cap Any.Combine at arity eight *(Accepted)* - -**Quality: good.** Honest about the ceiling being heuristic, with a defined escape hatch (add -arities compatibly via a new decision on evidence). The alternatives are real. The same -implementation-reference pointer nit as ADR-0013 applies. - -### ADR-0020 — Materialize dummies only through Generate() *(Accepted)* - -**Quality: exemplary — the best document in the base.** Concrete evidence (the syntactic shapes -where the conversion silently misbehaved, drawn from the suite itself), three fairly-weighed -alternatives including the analyzer route it deliberately declines, honest costs, and the pre-1.0 -timing argument stated as such. It also demonstrably steered later work (ADR-0026 reuses both its -reasoning pattern and its risk framing). No changes recommended. - -### ADR-0022 — Floor the library's .NET Framework support at 4.7.2 *(Accepted)* - -**Quality: sound policy; scope wording aged.** "A compatibility promise that is not exercised -cannot provide a trustworthy support boundary" is the right principle. But the ADR predates JustDummies -and speaks of "the shipped `netstandard2.0` libraries" without naming them; whether JustDummies is -inside its scope is now a matter of inference, and the floor job does not include it (§6). When the -maintainer next touches this area, a one-line clarification of covered packages would close the -ambiguity — or the JustDummies-specific floor decision can ride the new determinism ADR proposed below. - -### ADR-0025 — Generate matching strings from a home-grown regular subset *(Proposed)* - -**Quality: an unusually honest build-vs-buy record.** The rejection of Fare is argued on identity -and error-contract grounds (silent dropping of non-regular constructs vs first-class refusal), not -on FUD; the "non-regular constructs are impossible for *any* finite generator, so the subset is not -a convenience cut" framing is exactly right; the terminal-generator decision is well-argued. -**Issues:** (1) It is still **Proposed** while fully implemented, shipped in the package README, -and *load-bearing for the Accepted ADR-0026* (whose `ErrorCodeFactory` is built on -`StringMatching`) — until the status flips, an accepted decision formally rests on an undecided -one. The audit's role is to flag it; only `@reefact` flips a status. (2) The "terminals draw from -printable ASCII" rationale sentence is imprecise — `\s` includes tab (0x09) and explicit escapes -emit exactly the character they name (§4.2); the wording should be corrected *before* acceptance, -since the ADR itself declares the universe a compatibility-relevant behavior. (3) It cites "a -property test" against the real engine; what exists is a fixed-seed, fixed-corpus oracle test in -the unit-test project — excellent, but not property-based; the text should say what the safety net -is. - -### ADR-0026 — Rebase the testing package's arbitrary values on JustDummies *(Accepted)* - -**Quality: a thorough consolidation record** — six real alternatives, the one-seed-story rationale, -honest interim-packaging risk. **Two precision drifts:** (1) the decision text says each factory -exposes "an `IAny` generator through a distinct method where composition is needed" — no factory -exposes any such method today (verified: zero `IAny` occurrences in `FirstClassErrors.Testing` -sources). Defensible YAGNI, but the text reads as a decided API shape, and a compliance check a -year from now cannot tell deliberate deferral from unfinished migration. (2) Its risk clause says -the double-assembly hazard exists "precisely because JustDummies types appear in Testing's public API" -— today none do; the premise is misstated (the hazard is real for other reasons while JustDummies ships -inside the artifact). Since accepted ADRs are never edited in place, both belong as a short note in -the implementation reference. - -### Structural gaps in the base (Create-recommendations) - -1. **JustDummies' determinism contract has no accepted ADR.** The `AsyncLocal` ambient source, opt-in - `Reproducibly`, lazy pinning, seed-on-failure reporting — the crown-jewel guarantee — was decided - in ADR-0006, which is now Superseded *and* was scoped to FirstClassErrors.Testing; ADR-0026's - decision is about rebasing Testing, not about JustDummies' own contract. A future maintainer asking - "why `AsyncLocal` and not a parameter? why is raced `System.Random` acceptable?" finds the - reasoning only in a superseded record. **Recommend drafting one Proposed ADR** ("JustDummies supplies - arbitrary values from an ambient, seedable, execution-context-local source with opt-in - reproducibility") carrying ADR-0006's rationale forward and settling, in the same document, the - open edges this audit surfaced: single-logical-flow concurrency semantics, the closed - `IHasRandomSource` seam, and the cross-version seed-stability policy (§7.3). -2. **The ordinal-engine architecture has no ADR.** One shared 64-bit ordinal space with four - arithmetic-substrate engines is a lasting, questionable-by-a-future-maintainer decision - (why four engines? why is `decimal` not ordinal-mapped?) that currently lives only in internal - XML docs. It passes the repository's own ADR test ("if the implementation changed but the - decision stood…"). A short Proposed ADR would fix the asymmetry with far smaller decisions - (arity caps) that did get records. - -## 6. ADR Compliance - -| ADR | Status | Compliance of the implementation | -|---|---|---| -| 0006 (historical) | Superseded | **Compliant and exceeded.** The inherited seeding contract (context-local, opt-in determinism, seed reporting) is implemented faithfully; JustDummies adds the isolated `AnyContext` the original ADR only anticipated. | -| 0011 | Accepted | **Compliant.** No FirstClassErrors reference; boundary machine-checked (`ArchitectureTests`); standalone identity, release train, and docs in place. Note: enforcement is *stronger* than the recorded decision (§5). | -| 0013 | Accepted | **Compliant, verified in detail** — eager gate net of outside-domain `Containing` credits, conservative `ContainingAny` accounting, overflow-safe arithmetic, bounded budget, both failure channels. **One minor deviation:** the exhaustion message *unconditionally* promises `Any.Reproducibly({seed}, …)` replay (`CollectionState.cs:246-254`; the `seed is not null` guard is dead code — the seed can never be null there). For a **foreign** element generator whose draws ignore the ambient source, that promise is false; the ADR says failures are "explicit and reproducible". Qualify the message when the element generator carries no library source. | -| 0015 | Accepted | **Compliant exactly** — arities 2–8, no more; suppressions localized with ADR-referencing justifications (`Any.Combine.cs:214-215`, `266-267`); ceiling documented on the arity-8 overload. | -| 0020 | Accepted | **Fully compliant.** No implicit conversions anywhere; `Generate()` is the sole materialization; builders verified immutable (every fluent method returns a new instance). Residue: three test DisplayNames and one comment still *describe* the removed conversions (§4.3). | -| 0022 | Accepted | **Partial for JustDummies.** The netstandard2.0 asset is loaded and driven on net472 only transitively through FirstClassErrors' floor job; JustDummies' own suite never runs there, and the package README states no .NET Framework floor at all (FirstClassErrors' README does). Close before first publication (§11 item 5). | -| 0025 | Proposed | **Compliant on every major clause** (home-grown parser, first-class rejection, terminal generator, zero dependencies, printable-ASCII *default* universe, bounded unbounded-quantifier spread). The §4.1(c)/(d) defects are quality bugs *within* the decided scope, not deviations — with the caveat that (d) breaks the rejection *promise* the ADR records. One taxonomy edge: a well-formed negated class outside the printable universe raises `ArgumentException` ("malformed") instead of `UnsupportedRegexException`. | -| 0026 | Accepted | **Compliant on every executed clause** — single engine, single seed scope, `Testing.Any` removed, factories shipped, clock/ids on the ambient context, docs updated EN/FR. The unimplemented "distinct `IAny` method" half and the misstated risk premise are recorded in §5. | - -## 7. Architecture Review - -### 7.1 Layering and shape - -The library is three clean layers: **public fluent builders** (thin, per-type, sealed, immutable) → -**internal spec engines** (`OrdinalIntervalSpec`, `WideIntervalSpec`, `ContinuousIntervalSpec`, -`DecimalIntervalSpec`, `StringSpec`, `CountSpec`, `CollectionState`) → **sampling primitives** -(`RandomSampling`). Public surface never leaks internal types; internal engines never touch the -ambient state directly (sources are passed down). The composition seams — `.As(factory)`, -`Any.Combine(...)`, the collection factories — are all defined over the one-member `IAny`, -which is as small as an interface can be (ISP by construction) and covariant, so derived and foreign -generators flow through every seam uniformly. - -The **four-engine split is principled, not accidental**: 64-bit-mappable discrete types share -`OrdinalIntervalSpec`; 128-bit integers need `WideIntervalSpec` only because netstandard2.0 has no -`UInt128` (the two are verbatim siblings — the one regrettable, TFM-forced duplication); IEEE floats -need continuous sampling with type-aware quantization; `decimal` is neither ordinal-mappable (96-bit -mantissa × scale) nor IEEE. Each engine's existence is justified by its arithmetic substrate. What -is *missing* is the ADR recording this (§5), and — as §4.1(b) showed — a parameterized test suite -exercising each engine through each of its type facades. - -The **collection hierarchy** is a textbook-clean CRTP: -`AnyCollection` holds the shared fluent count/containment surface returning -`TSelf` (without the classic unsafe `(TSelf)this` cast — concrete types implement a -`With(state)` factory), and the five concrete builders add only element shaping and the -`Build(List)` conversion. The exception is `AnyDictionary`, which cannot inherit the base -(its element is a pair) and therefore **duplicates the entire count facade verbatim** (~60 lines, -`AnyDictionary.cs:51-113`) and offers no containment constraint at all — the one place in the -collection family where sharing failed. Extracting the count facade over `CollectionState` (or -adding `ContainingKey`, which would ride the existing key-state machinery for free) would close -both the duplication and the acknowledged test hole (`AnyCollectionTests.cs:161-163` comments on -it). - -### 7.2 Extensibility - -**For users, the design is closed, and that is a legitimate but undocumented choice.** `IAny` is -public, so anyone can implement a generator and compose it through `As`/`Combine`/collections. But -`RandomSource`, `IHasRandomSource`, and `ICardinalityHint` are all internal, so a foreign -generator (a) cannot draw from the ambient seeded source — under `Any.Reproducibly` its values do -not replay, and (b) cannot advertise a finite domain — a distinct collection over it always takes -the bounded-draw path (safe, and exactly what ADR-0013 promises). The degradation is graceful -everywhere (verified: `OrNull` falls back to the ambient source for the null coin; `Combine` -propagates `null` sources without failing). What is missing is one honest paragraph on `IAny`'s -XML doc telling implementers where they stand — today the contract is discoverable only by reading -internal code. If the seam is ever to open, an `ISeedableAny` in a minor release is the natural -shape; nothing needs deciding now except the documentation. - -**For maintainers**, adding one new scalar type touches 6–9 files (builder, `Any`, `AnyContext`, -tests, user docs EN/FR, package README, `justdummies-check` if net8-only, possibly a spec engine). The -process is mechanical but real, and only partially guarded (§9.2). - -### 7.3 The determinism machinery — deep dive - -The implementation is correct at the level that is hard to get right (§3.3). The remaining risks -are all *contract-documentation* risks, and they cluster into four: - -**(a) Concurrency inside one seeded scope silently voids replayability — undocumented.** An -`AsyncLocal` copies the *reference*: `Task.Run`/`Parallel.ForEach` children inside one -`Reproducibly` body all see the same `SeededRandom` instance. Two consequences. First, even with -benign interleaving, the draw *order* becomes scheduler-dependent, so the reported seed no longer -replays the run — the exact guarantee the feature exists for. Second, `System.Random` is not -thread-safe, and netstandard2.0 offers no thread-safe alternative; a racing draw can corrupt state -(on .NET Framework, a raced `Random` can degrade to returning zeros). The docs carefully explain -that the source "never leaks *across* tests running in parallel" (true — different logical flows) -but say nothing about parallelism *within* a body. The fix is one honest paragraph on -`Reproducibly` ("a seeded run is single-logical-flow; concurrent draws inside the body are neither -replayable nor safe") — plus, optionally, recording in the new determinism ADR why per-flow -forking (a child source per `Task.Run`) was not attempted (it would change every sequence and -complicate `WithSeed`; the honest restriction is the right V1). - -**(b) Seed reports can name a wrong or inapplicable seed in mixed/fixed-source composition.** -`Combine` propagates the **first non-null** operand source for failure reporting -(`Any.Combine.cs:33` et al.). `Any.Combine(Any.WithSeed(1).Int32(), Any.WithSeed(2).Int32(), throwing)` -fails with "seeded with 1; reproduce with `Any.Reproducibly(1, …)`" — doubly wrong: seed 2 goes -unreported, and the instruction is inapplicable because `Reproducibly` pins the *ambient* source, -which `FixedRandomSource`-backed generators ignore by design. This is an edge case (mixing seeded -contexts inside one composition is unusual), but the failure mode is a *confidently misleading -diagnostic* in the library whose signature is diagnostic honesty. A small fix reaches it: let the -source kind produce the replay hint (ambient → "reproduce with `Any.Reproducibly({seed}, …)`"; -fixed → "this generator draws from `Any.WithSeed({seed})`, which already replays by itself"), and -have `Combine` collect distinct sources rather than the first. - -**(c) Cross-version and cross-runtime seed stability is neither promised nor disclaimed.** The -package description says "any run is reproducible from a reported seed" without qualification. -Within one process this holds. Across *library versions*, any change to draw order or count -silently changes every sequence — and ADR-0025 already acknowledges consumers may rely on generated -shapes. Across *runtimes*, seeded `new Random(seed)` retains the legacy algorithm on modern .NET -precisely for compatibility, so the common surface should agree between the netstandard2.0 and -net8.0 assets — but nothing tests it (§4.6), and `Random`'s documentation explicitly reserves the -right for implementations to differ across framework versions. The mature policy, before v1: -**promise stability within a package version, disclaim it across versions**, one sentence in the -README and the new determinism ADR. (For comparison: FsCheck and AutoFixture both learned to -disclaim this explicitly.) - -**(d) Lazy ambient pinning makes an *unwrapped* failure only approximately replayable.** Outside -`Reproducibly`, the first draw in a logical flow pins a remembered seed. Draws that happened -*before* the failing block in the same flow (a fixture, an earlier arrange) consume from the same -sequence, so replaying "just the test body" with the reported seed can diverge. The design is -right (this is why `Reproducibly` exists); the user guide's replay narrative could carry one -sentence saying replay fidelity starts at the scope boundary. - -Verified non-issues worth recording so they are not re-litigated: the async-overload -`ExecutionContext` semantics (correct — see §3.3); `NewSeed() = Guid.NewGuid().GetHashCode()` -(collision-tolerant use, analyzed in ADR-0006); xUnit seed-spanning (each test invocation is its -own async frame; a shared class constructor participates in its test's flow, which is the correct -scope); `SequenceOf` re-enumeration (materialized once, never re-draws). - -### 7.4 SOLID, briefly and only where it earns its keep - -SRP: builders carry fluent surface, engines carry semantics — clean. OCP: adding a *constraint* to -a discrete type is a one-method facade addition over an existing engine operation; adding a *type* -is deliberately closed (sealed builders, internal engines) — the right trade for an invariant-heavy -library. LSP: the CRTP collection base is sound (no self-cast trick, `TSelf` bound enforced). ISP: -`IAny` single-member; `ICardinalityHint`'s two members travel together by explicit, -documented design (cardinality without membership would be unsound — the interface doc argues it). -DIP is intentionally absent at the user seam (no injectable randomness abstraction) — that *is* the -closed-extensibility decision of §7.2, acceptable but deserving its paragraph of documentation. - -## 8. API Review - -### 8.1 The constraint algebra is uniform where it counts - -The verified matrix: all five signed integer builders and all four continuous/decimal builders -expose exactly `Positive · Negative · Zero · NonZero · GreaterThan[OrEqualTo] · LessThan[OrEqualTo] -· Between · OneOf · Except · DifferentFrom`; the five unsigned builders drop exactly -`Positive`/`Negative` (meaningless there — `NonZero` covers the intent); the four instant-like -builders (`DateTime`, `DateTimeOffset`, `DateOnly`, `TimeOnly`) rename the bound family to domain -vocabulary (`After`/`AfterOrEqualTo`/`Before`/`BeforeOrEqualTo`/`Between`) with identical -inclusive/exclusive semantics, while `AnyTimeSpan` — a magnitude, not an instant — correctly keeps -the full numeric algebra including `Positive`/`Negative`/`Zero`; `AnyChar` carries the character families -plus the exclusion trio; `AnyGuid` has `NonEmpty`/`Empty`/`OneOf`/`Except`/`DifferentFrom`; -`AnyEnum` has the exclusion trio with declared-members validation; collections share -`NonEmpty · Empty · WithCount · WithMinCount · WithMaxCount · WithCountBetween · Containing · -ContainingAny` (+ `Distinct` variants where meaningful). Bounds are consistently inclusive for -`Between`/`…OrEqualTo` and exclusive for `GreaterThan`/`LessThan`/`After`/`Before` — no semantic -surprises were found anywhere in the matrix. This level of consistency across nineteen hand-written -interval builders — plus their string, char, guid, enum, bool and collection siblings — is an -achievement in itself. - -### 8.2 The asymmetries worth fixing or recording - -* **`AnyString` is the only scalar builder with no exclusion constraints** — no `OneOf`, no - `Except`, no `DifferentFrom`. "A name different from the one I already hold" is one of the most - common dummy-string needs (it is exactly why `DifferentFrom` exists everywhere else, per its own - XML doc). The honest reason for the gap: strings are not ordinal-mapped, so exclusions cannot - ride the interval engine; `DifferentFrom` would need either a bounded redraw (expected collisions - ≈ 0 for any non-trivial spec — consistent with the library's other bounded escapes) or a - spec-aware layout tweak. Recommended (§10 Must-Have): - - ```csharp - // Today — no way to express this: - string other = Any.String().NonEmpty().Generate(); // might equal existing! - // Proposed: - string other = Any.String().NonEmpty().DifferentFrom(existing).Generate(); - ``` - -* **`AnyDictionary` drops `Containing`/`ContainingAny`** and duplicates the count facade (§7.1). - `ContainingKey(TKey)` would ride the existing key-state machinery unchanged. -* **`Any.Bool()` is the single deviation from the CLR-name factory convention** - (`Int32`, `SByte`, `Single`, … are all CLR names; the CLR name here is `Boolean`). The short form - is arguably the better ergonomics — but then the convention is "CLR names, except one", and after - 1.0 the rename is breaking in either direction. Decide deliberately and record one line, before - release (the repository has ADRs for precisely this class of naming decision). -* **`PairOf`/`TripleOf` stop at arity 3** while `Combine` runs to 8. Defensible (tuples beyond 3 - read poorly; `Combine` covers them), but the stopping point is recorded nowhere — one doc - sentence closes it. - -### 8.3 Discoverability and ceremony - -The static `Any.` entry point makes the whole scalar surface one keystroke discoverable, and each -builder's fluent methods enumerate its full constraint vocabulary in IntelliSense — good. Two -seams are less discoverable: `As` and `OrNull` are extension methods in separate static classes -(invisible until the `using` exists — though the namespace is shared, so in practice they appear), -and `As` is the library's `Select` under a domain-intent name; one doc line bridging from LINQ -vocabulary ("`As` is `Select` for generators — named for its dominant use: passing through a value -object's factory") would help LINQ-native readers. The `Generate()` terminal ceremony is the -ADR-0020 trade, consciously priced there; the audit confirms the cost is real but small (one call -per materialization), the benefit (no effectful hidden conversions) is structural, and the decision -should stand. `AnyContext` mirrors scalars only — composition inherits the context through operand -sources, which is *more* elegant than mirroring and correctly documented. - -### 8.4 Naming - -`StartingWith`/`EndingWith`/`Containing`, `After`/`Before`, `DifferentFrom` vs `Except`, -`Containing` vs `ContainingAny` — the vocabulary is intention-revealing and reads at the call site -the way the philosophy intends. CLR type-name factories (`Any.Int32()`, not `Any.Int()`) are -consistent with the builder type names (`AnyInt32`) and sidestep C# keyword restrictions; -this is defensible and, more importantly, uniform (§8.2's `Bool` aside). - -## 9. Maintainability Review - -### 9.1 Duplication, measured - -Four clone families among the numeric builders (signed quartet, unsigned quartet, continuous trio, -wide pair — byte-identical modulo type substitution; ~2,450 lines), the five temporal builders on -the same pattern (~800 lines), the constraint-and-conflict logic quadruplicated across the four -engines (~910 lines), and the `Any`/`AnyContext` scalar mirror (~300 doc-heavy lines). A scripted -comparison found **zero behavioral copy-paste slips** across the clone families — evidence of real -discipline — while all drift found so far is *documentation* drift (§4.3), which is exactly the -kind guards don't exist for yet. - -### 9.2 Mitigation: guards, not generics - -The obvious refactor — a CRTP generic base (`AnyOrdinal`) — fails this project's -constraints: C# requires a public base class for a public sealed builder (CS0060), so the internal -engine seam would leak into the public API; netstandard2.0 has no generic math (`INumber` is -net7+), so the per-type `Ord`/`Val`/display lambdas remain; and the library's stated bar is -simplicity of maintenance, which 14 flat, boring, greppable files serve better than one clever -base. Source generators/T4 buy deduplication at the cost of build machinery and debuggability — -also a poor trade here. **Recommended instead: executable parity guards**, ~3 short -reflection-based tests: - -1. *Mirror parity:* every public static `Any` method returning a builder type has an - `AnyContext` instance counterpart with identical name/signature/return type, per TFM (~20 - lines; kills the §4.3 drift class outright). -2. *Algebra parity:* each builder family exposes its exact expected method-name set (the §8.1 - matrix, encoded once as data) — a new builder missing `DifferentFrom`, or a renamed method, - fails with a named diff. -3. *Cross-engine scenario suite:* one parameterized test file runs the same scenario battery - (full-range draw touches both halves; `Between` endpoints reachable; `DifferentFrom` on a - narrow domain; `OneOf`+`Except` interplay; conflict messages) against **every** builder via - small per-type adapters. This is the suite that would have caught both §4.1(a) and §4.1(b) - before any human review. - -Complementarily, install the release-engineering guards of §4.7 (public-API baseline + package -validation) — they catch the breaking-change class parity tests cannot. - -### 9.3 Testing strategy - -What exists is well-shaped: behavior-first naming that reads as living documentation; exception -*messages* tested as first-class contracts; the real-engine regex oracle; regression tests that -encode bug history (the `AnyGuid` race test that races a deadline instead of hanging the suite); -flake-safe property-style assertions (unseeded draws asserted only against their declared domain); -and a strictly black-box posture — no `InternalsVisibleTo` exists, so all 222 tests exercise the -public surface only. That last fact cuts both ways and should be held as a deliberate choice: it -proves the public API is sufficient to specify the library (and makes engine refactors -test-transparent), *and* it is consistent with how both reachability defects survived — no test -looks at an engine's value-space coverage directly. The additions that close the gap, in order of -leverage: the cross-engine scenario suite above; **reachability assertions** (for each builder, a -seeded loop over `Between(lo, hi)` must observe values in both halves and hit both endpoints — -cheap, deterministic under `WithSeed`); a generation-limit test for `AnyPattern` (currently -untested); dedicated tests for the documented-but-untested contracts (empty enum, `AnyException` -base catchability, `DictionaryOf` key-comparer flow); and the cross-TFM same-seed assertion in -`justdummies-check` (extend `SeedBatch` with a golden sequence compared across the net8.0 and net6.0 -consumer legs, and extend the smoke to cover `OrNull`/`SequenceOf`/`PairOf`/`StringMatching`/enum -draws, which the packaged-asset guard currently never touches). - -### 9.4 Organization and hygiene - -The flat 54-file root is acceptable today because naming discipline does the foldering (`Any*` = -builders, `*Spec` = engines, `Regex*` = pattern subsystem); grouping into folders is optional -polish, worth doing only alongside another structural change. Hygiene nits found: dead member -`RegexCharacters.Count`; the dead null-guard in `CollectionState.Exhausted` (§6/ADR-0013 row); the -stale comments and DisplayNames of §4.3; the stale `Directory.Build.props` header (§4.7). - -## 10. Feature Gap Analysis - -Method: every proposal was screened against (i) the library's philosophy (constraints express -invariants; no realistic-fake-data, no object graphs, no clock coupling), (ii) the composition -test — *can `As`/`Combine`/`StringMatching` already express this in one readable line?* — and -(iii) the full cost of a new builder (builder + `Any` + `AnyContext` + parity data + tests + docs -EN/FR + package README + possibly `justdummies-check`). The bar for **Must Have** is the mandate's: -absence genuinely surprising. The library's composition-first design keeps this list short — most -BCL types are already one `As` away, which is the design working as intended. - -### Must Have - -**1. A top-level choice combinator: `Any.OneOf(params T[])` and `Any.ElementOf(IReadOnlyList)`.** -Picking an arbitrary element from a caller-supplied set is among the most common dummy needs in -real suites ("any of the three configured currencies", "one of the states in this table"). Today -`OneOf` exists only *inside* typed builders — there is no way to draw from a set of domain objects -or strings at all. Every user hand-rolls the same three lines (and forgets the seeded source, -silently breaking `Reproducibly` for that draw — a trap the library exists to prevent): - -```csharp -// Today — hand-rolled, and not seed-aware: -var currencies = new[] { eur, usd, gbp }; -var currency = currencies[new Random().Next(currencies.Length)]; // ambient seed ignored! - -// Proposed — seed-aware, philosophy-consistent, eagerly validated (empty set throws): -Currency currency = Any.OneOf(eur, usd, gbp).Generate(); -Order order = Any.ElementOf(existingOrders).Generate(); -``` - -Constructive (single draw), trivially implemented over the ambient source with an -`ICardinalityHint` (distinct count of the pool — it composes with distinct collections for free), -mirrored on `AnyContext`. Who benefits: every consumer, weekly. Cost: one small builder. This is -the highest-leverage addition available. - -**2. `AnyString.DifferentFrom(string)` / `Except(params string[])`.** -The §8.2 asymmetry: the most-used builder is the only scalar one that cannot exclude values. Honest -cost: a bounded redraw (the library's established escape pattern) or fragment-aware exclusion; -either fits in the existing `StringSpec` validation model. Who benefits: anyone testing -equality/inequality paths with string identifiers — a very common case. (`OneOf` on strings is then -free via proposal 1.) - -### Nice to Have - -* **`Uri` builder** (`Any.Uri().UsingHttps().WithHost("example.com")`) — the one BCL value-like - type that is both commonly needed in tests and genuinely awkward to compose by hand (scheme/host/ - path/query validity rules). In-box on both TFMs. Moderate cost (its own mini constraint algebra); - demand-driven timing is fine. -* **`WithChars(string pool)` / custom alphabet on `AnyString`** — today non-ASCII text (accents, - i18n) is reachable only through `StringMatching` literals; a custom pool is a small, composable - extension of the existing charset mechanism, and unlocks the i18n-sensitive-code use case without - any Unicode-table machinery. -* **`MultipleOf(int)` on integers / `WithScale(int)` on decimal** — "a valid amount in cents", "a - quantity in dozens": genuine invariants (not assertions) that today force `As(x => x * 100)` - workarounds that distort the declared range. Constructive to implement (draw in the quotient - space). -* **`ContainingKey(TKey)` on `AnyDictionary`** (§7.1/§8.2) — closes an API hole, a duplication, and - a test hole at once. -* **[Flags] enum combinations, opt-in** (`Any.Enum().AllowingCombinations()`) — today - undeclared combined values are unreachable *by design* (declared-members-only is the right - default); an explicit opt-in respects the default while serving flag-heavy domains. Requires a - documented stance on what "valid" means for flags (union of declared members). -* **`WithOffset`/offset control on `AnyDateTimeOffset`** — the offset dimension is currently - degenerate (always zero, documented); tests exercising offset math cannot vary it. A bounded - offset draw (±14 h in minutes, per the type's own rules) keeps validity. -* **Temporal granularity** (`WholeSeconds()`/`WholeDays()` or `WithGranularity(TimeSpan)`) — tick- - precision instants are almost never round, which surprises tests that serialize timestamps; - constructive via the ordinal engine (draw in the granule space, multiply). Also closes the - documentation gap ("values are tick-precision") in the meantime. -* **`GenerateMany(int)` terminal** — sugar for "N values without `ListOf` ceremony"; a *named - method* returning `IReadOnlyList`, so it stays inside ADR-0020's letter and spirit. -* **A test-framework seed adapter** (`[ReproducibleFact]`) — anticipated by ADR-0006's follow-ups, - dropped in the rebase, replaced by nothing. Zero-dependency JustDummies cannot reference xUnit, so - this is a *companion package* decision (`JustDummies.Xunit`) — worth an explicit yes/no ADR rather - than silence, because every consumer currently re-derives the `Reproducibly`-wrapping habit - by hand. - -### Optional Ideas - -`Version` (composable today: `Combine(Any.Int32().Between(0,99), …, (ma,mi,pa) => new Version(ma,mi,pa))`; -low frequency); `IPAddress`/`IPEndPoint` (in-box, niche; a doc recipe first); `Encoding` and -`CultureInfo` (feasible **only** from a fixed embedded pool — the installed-culture set is a -cross-machine reproducibility hazard the library must not inherit; both are subsumed by proposal 1 -+ a documented pool); `MailAddress`, file-system paths, `Stream`, `byte[]` blobs (all one-line -recipes over existing surface — `ArrayOf(Any.Byte())` already is the blob builder; document them -in the user guide's recipe section instead of shipping builders); `KeyValuePair` sugar; -`Queue`/`Stack`/`LinkedList` and `Sorted*` collections (one-line `As` conversions; a first-class -`Sorted()` needs a comparability gate analogous to the cardinality hint — design exists if demand -appears); `BigInteger` (in-box on both TFMs but breaks the "full range unless constrained" -symmetry — there is no full range; needs its own bounded-default stance); `Rune` (net8 leg; -conflicts with the deliberate ASCII-centric text model unless `WithChars` lands first); -`ContainingAll(params T[])` sugar. - -### Out of Scope (recommended to stay absent, with reasons) - -* **`Where(predicate)` filtering** — generate-and-filter is the exact opposite of the library's - constructive model; unsatisfiable predicates reintroduce the unbounded-retry class the whole - design exists to exclude. The existing answer (express the invariant as constraints, or build via - `As` from a constrained draw) is the philosophy. -* **Generator registration / AutoFixture-style object graphs** — reflection-driven auto-filling is - the adjacent product the README explicitly disclaims; plain C# helpers are the reuse mechanism. -* **Immutable collections** — `System.Collections.Immutable` is an external package on the - netstandard2.0 leg, so a builder would break the zero-dependency identity there; consumer-side - `.As(ImmutableList.CreateRange)` is one line. (A net8-leg-only surface would fracture the API - across TFMs for marginal gain — not worth it.) -* **`Index`/`Range`** — validity is contextual (depends on the sequence length), so "arbitrary yet - valid" cannot hold standalone. -* **`RegionInfo`**, **`Complex`** — environment-dependent resp. scientific-niche; both fail the - frequency test. -* **Realistic fake data** (names, emails, addresses) — explicitly disclaimed; Bogus exists. - -## 11. Recommended Improvements - -In priority order; items 1–7 are the recommended pre-release gate. - -1. **Fix the three reproduced defects** — decimal fraction construction - (`DecimalIntervalSpec.cs:145`), type-aware nudge (`ContinuousIntervalSpec.cs:189` → - `_nextUp`), char-overflow guard (`RegexParser.cs:398` + `RegexAlphabet.Range`); and the - balancing-group/name validation in `SkipGroupName` (§4.1 d). Each with a regression test. -2. **Add reachability tests and the cross-engine scenario suite** (§9.3) — the structural answer to - the defect class, not just the instances. -3. **Add the parity guards** (§9.2): `Any`↔`AnyContext` mirror test, algebra-matrix test. -4. **Close the determinism contract** (§7.3): document single-logical-flow seeding on - `Reproducibly`; source-kind-aware replay hints (and multi-source `Combine` reporting); the - cross-version stability policy sentence; the foreign-generator qualification in the exhaustion - message (dead null-guard removed). Draft the **determinism ADR** and the **ordinal-engine ADR** - (§5, structural gaps) as `Proposed` for `@reefact`. -5. **Run JustDummies on its floors**: import `build/Net472TestFloor.props` into `JustDummies.UnitTests` - (net8-only tests conditioned out), add it to the ci.yml floor loop; add the cross-TFM golden- - sequence assertion to `justdummies-check`; state the .NET Framework floor in the package README - (ADR-0022 follow-up). -6. **Documentation pass**: surface JustDummies in the repository README (packages table + TOC); write - the JustDummies user guide with the per-builder constraint reference and the `StringMatching` - dialect (closing ADR-0025's follow-up); correct the three "printable ASCII" sites (§4.2); - advertise the empty-by-default behavior in the package README; fix the stale - comments/DisplayNames (§4.3) and the `Directory.Build.props` header. -7. **Release-engineering guards**: public-API baseline (`PublicApiAnalyzers`) and - `EnablePackageValidation`; decide `Bool()` vs `Boolean()` and record it; ask `@reefact` to - resolve ADR-0025's status (after its wording fix); record the two ADR-0026 clarifications in the - implementation reference; enrich or soften the ADR-0013/0015 implementation-reference pointers. -8. **Ship the two Must-Have features** (§10): `Any.OneOf`/`Any.ElementOf`, and string - exclusions (`DifferentFrom`/`Except` on `AnyString`). -9. **`AnyDictionary`**: extract the shared count facade; add `ContainingKey`. -10. **Then, demand-driven**: the Nice-to-Have list (§10), each on evidence of need, with the - parity-guard data updated as part of each addition's definition of done. - -## 12. Suggested Roadmap - -**Phase 0 — before the first `dum-v*` release (correctness and contract).** Items 1–7 above. The -rationale is ADR-0020's own: every one of these is cheap now and expensive after adoption — the -decimal fix changes every seeded sequence (a non-event today, a compatibility event after v1); the -determinism policy, the `Bool` naming, the API baseline, and the ADR statuses are all -one-line-or-one-file decisions that become migrations later. Exit criterion: the §4 weaknesses -table is empty except items explicitly deferred by recorded decision. - -**Phase 1 — first stable cycle (completeness within the philosophy).** Item 8 (the two Must-Haves, -which are additive and low-risk), item 9, the user-guide recipe section (blobs, paths, Version, -Uri-via-Combine — turning Optional-list types into documentation instead of surface), and the -`JustDummies.Xunit` companion-package decision (yes or no, as an ADR). - -**Phase 2 — demand-driven growth.** Nice-to-Haves as real requests arrive (`Uri` and `WithChars` -first, on current evidence), each addition carrying its parity-matrix entry, tests, and EN/FR docs -as one unit. Revisit the Optional list yearly; resist the Out-of-Scope list permanently — it is -what keeps this library what it is. - -## 13. Conclusion - -JustDummies is what a focused library looks like when the authors know exactly what it is for and — -just as importantly — what it is not for. The ordinal-space engine, the constraint-provenance -diagnostics, the bounded-escape discipline, and the ADR trail are all better than the norm for this -category, and the composition-first design keeps the future feature surface honest: most "missing -types" are correctly one `As` away, not one builder away. - -The audit's findings concentrate in one place: the space between *declared* behavior and *reachable* -behavior. Two of the three reproduced defects live exactly there, invisible to a membership-only -test suite; the mirrored surfaces drift exactly where no guard looks; the determinism promise is -sound precisely up to the edges no document describes. All of it is fixable this side of the first -release, most of it in days, and the highest-value items are not the fixes but the guards — the -reachability suite, the parity tests, the API baseline — that make the next defect of each class -impossible to ship silently. - -With Phase 0 done, this is a library that can credibly promise what its README says: arbitrary yet -valid, conflicts named at the line that caused them, and any run replayable from one reported seed -— on every target it ships for. - -## 14. Issue tracking - -The §11 recommendations were opened as GitHub issues on 2026-07-20, mirroring the repository's JustDummies -issue template. This table is a **static snapshot**: the live state of each issue (open, closed, in -progress) lives in the issue tracker, not here — do not maintain status in this document. - -| §11 item | Issue(s) | Phase (§12) | -|---|---|---| -| 1 — Fix the reproduced defects | [#206](https://github.com/Reefact/first-class-errors/issues/206) AnyDecimal upper half · [#207](https://github.com/Reefact/first-class-errors/issues/207) Single/Half nudge · [#208](https://github.com/Reefact/first-class-errors/issues/208) U+FFFF hang · [#209](https://github.com/Reefact/first-class-errors/issues/209) balancing groups · [#210](https://github.com/Reefact/first-class-errors/issues/210) minor regex edges | 0 | -| 2 — Reachability + cross-engine suite | [#213](https://github.com/Reefact/first-class-errors/issues/213) | 0 | -| 3 — Parity guards | [#214](https://github.com/Reefact/first-class-errors/issues/214) | 0 | -| 4 — Close the determinism contract | [#216](https://github.com/Reefact/first-class-errors/issues/216) contract docs + ADR · [#217](https://github.com/Reefact/first-class-errors/issues/217) ordinal-engine ADR · [#211](https://github.com/Reefact/first-class-errors/issues/211) seed report · [#212](https://github.com/Reefact/first-class-errors/issues/212) exhaustion message | 0 | -| 5 — Run on the floors | [#215](https://github.com/Reefact/first-class-errors/issues/215) | 0 | -| 6 — Documentation pass | [#218](https://github.com/Reefact/first-class-errors/issues/218) README + user guide · [#219](https://github.com/Reefact/first-class-errors/issues/219) printable-ASCII & stale docs | 0 | -| 7 — Release-engineering guards | [#221](https://github.com/Reefact/first-class-errors/issues/221) API baseline · [#222](https://github.com/Reefact/first-class-errors/issues/222) Bool naming · [#220](https://github.com/Reefact/first-class-errors/issues/220) ADR hygiene | 0 | -| 8 — Ship the Must-Have features | [#223](https://github.com/Reefact/first-class-errors/issues/223) Any.OneOf/ElementOf · [#224](https://github.com/Reefact/first-class-errors/issues/224) AnyString exclusions | 1 | -| 9 — AnyDictionary | [#225](https://github.com/Reefact/first-class-errors/issues/225) | 1 | -| 10 — Demand-driven Nice-to-Haves | [#226](https://github.com/Reefact/first-class-errors/issues/226) backlog | 2 | - ---- - -*Produced by an agent-run audit (multi-agent review with adversarial verification; all reported -defects independently reproduced against the built library; full test suite executed). Advisory -per ADR-0004: recommendations and drafts only — every decision remains with the maintainer.* diff --git a/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.fr.md b/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.fr.md index 94224b3a..43ac1114 100644 --- a/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.fr.md +++ b/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.fr.md @@ -5,7 +5,7 @@ **Date :** 2026-07-20 **Révision auditée :** `3bf89e3fb568beb69329b12b2ec2be14553bb8d4` (`main` au moment de l'audit) -**Périmètre :** l'ensemble de l'écosystème FirstClassErrors — bibliothèque cœur, Analyzers, GenDoc (+ Worker), CLI, RequestBinder, Testing, JustDummies (en tant que membre de l'écosystème ; son audit dédié est l'[audit d'architecture et de conception de JustDummies](./2026-07-20-dummies-architecture-and-design-audit.fr.md)), exemples, tests, documentation (EN + FR), base d'ADR, CI/CD et ingénierie de release. +**Périmètre :** l'ensemble de l'écosystème FirstClassErrors — bibliothèque cœur, Analyzers, GenDoc (+ Worker), CLI, RequestBinder, Testing, JustDummies (en tant que membre de l'écosystème ; son audit dédié est l'[audit d'architecture et de conception de JustDummies](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.fr.md)), exemples, tests, documentation (EN + FR), base d'ADR, CI/CD et ingénierie de release. **Statut :** consultatif. Conformément à la convention du dépôt (ADR-0004), cet audit produit des recommandations, jamais des bloqueurs ; toute modification d'ADR proposée est un brouillon que `@reefact` accepte ou rejette. **Question posée :** *« Ce projet open source est-il cohérent, professionnel et maintenable, et pourrait-il raisonnablement devenir une référence dans son domaine ? »* diff --git a/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.md b/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.md index c9a113b3..8f516b64 100644 --- a/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.md +++ b/doc/handwritten/for-maintainers/audit/2026-07-20-firstclasserrors-architecture-and-design-audit.md @@ -5,7 +5,7 @@ **Date:** 2026-07-20 **Audited revision:** `3bf89e3fb568beb69329b12b2ec2be14553bb8d4` (`main` at audit time) -**Scope:** the whole FirstClassErrors ecosystem — core library, Analyzers, GenDoc (+ Worker), CLI, RequestBinder, Testing, JustDummies (as an ecosystem member; its dedicated audit is the [JustDummies architecture & design audit](./2026-07-20-dummies-architecture-and-design-audit.md)), samples, tests, documentation (EN + FR), ADR base, CI/CD and release engineering. +**Scope:** the whole FirstClassErrors ecosystem — core library, Analyzers, GenDoc (+ Worker), CLI, RequestBinder, Testing, JustDummies (as an ecosystem member; its dedicated audit is the [JustDummies architecture & design audit](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/audit/2026-07-20-dummies-architecture-and-design-audit.md)), samples, tests, documentation (EN + FR), ADR base, CI/CD and release engineering. **Status:** advisory. Per the repository's own convention (ADR-0004), this audit produces recommendations, never blockers; every proposed ADR change is a draft for `@reefact` to accept or reject. **Commissioned question:** *“Is this a coherent, professional, maintainable open-source project that could reasonably become a reference in its domain?”* 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 277063e5..76f61ca4 100644 --- a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md +++ b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.fr.md @@ -21,7 +21,7 @@ Lors d'un changement de plancher, il faut mettre à jour la propriété centrale ## 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). +Décisions liées : [ADR-0002](../adr/0002-floor-the-tooling-runtime.fr.md), [just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-floor-the-library-on-net-framework-4-7-2.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. @@ -57,7 +57,7 @@ La baseline n'est mise à jour par le processus de release qu'après une publica ## Contrats de génération de JustDummies -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). +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), [just-dummies ADR-0004](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0004-gate-distinct-collections-by-cardinality-else-bounded-draw.md), [just-dummies ADR-0005](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0005-cap-any-combine-at-arity-eight.md), [just-dummies ADR-0006](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0006-materialize-dummies-only-through-generate.md). JustDummies 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. diff --git a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md index 27f3f8cb..512b45dd 100644 --- a/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md +++ b/doc/handwritten/for-maintainers/specifications/adr-implementation-reference.md @@ -21,7 +21,7 @@ When the floor changes, update the central property, the floor SDK used by the w ## 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). +Related decisions: [ADR-0002](../adr/0002-floor-the-tooling-runtime.md), [just-dummies ADR-0007](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0007-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. @@ -57,7 +57,7 @@ The baseline is updated only by the release process after a successful compatibl ## JustDummies 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). +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), [just-dummies ADR-0004](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0004-gate-distinct-collections-by-cardinality-else-bounded-draw.md), [just-dummies ADR-0005](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0005-cap-any-combine-at-arity-eight.md), [just-dummies ADR-0006](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0006-materialize-dummies-only-through-generate.md). JustDummies 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. diff --git a/doc/handwritten/for-maintainers/workflows/changelog.en.md b/doc/handwritten/for-maintainers/workflows/changelog.en.md index f2456fee..e5a938d7 100644 --- a/doc/handwritten/for-maintainers/workflows/changelog.en.md +++ b/doc/handwritten/for-maintainers/workflows/changelog.en.md @@ -29,7 +29,6 @@ The trains version independently and keep **separate** changelog files: | --- | --- | --- | | `lib` | `core`, `analyzers`, `testing`, `binder` | [`CHANGELOG.md`](../../../../CHANGELOG.md) | | `cli` | `cli`, `gendoc` | [`FirstClassErrors.Cli/CHANGELOG.md`](../../../../FirstClassErrors.Cli/CHANGELOG.md) | -| `dum` | `justdummies` | [`JustDummies/CHANGELOG.md`](../../../../JustDummies/CHANGELOG.md) | ## When it runs diff --git a/doc/handwritten/for-maintainers/workflows/changelog.fr.md b/doc/handwritten/for-maintainers/workflows/changelog.fr.md index c450fb2f..d08d3e3a 100644 --- a/doc/handwritten/for-maintainers/workflows/changelog.fr.md +++ b/doc/handwritten/for-maintainers/workflows/changelog.fr.md @@ -30,7 +30,6 @@ changelog **distincts** : | --- | --- | --- | | `lib` | `core`, `analyzers`, `testing`, `binder` | [`CHANGELOG.md`](../../../../CHANGELOG.md) | | `cli` | `cli`, `gendoc` | [`FirstClassErrors.Cli/CHANGELOG.md`](../../../../FirstClassErrors.Cli/CHANGELOG.md) | -| `dum` | `justdummies` | [`JustDummies/CHANGELOG.md`](../../../../JustDummies/CHANGELOG.md) | ## Quand il s'exécute diff --git a/doc/handwritten/for-maintainers/workflows/mutation.en.md b/doc/handwritten/for-maintainers/workflows/mutation.en.md index a44db083..add40729 100644 --- a/doc/handwritten/for-maintainers/workflows/mutation.en.md +++ b/doc/handwritten/for-maintainers/workflows/mutation.en.md @@ -33,12 +33,12 @@ the three libraries (`FirstClassErrors`, `FirstClassErrors.Testing`, documentation generator, the Roslyn analyzers. What stays out, and why, is under *Handle with care* below. -**JustDummies is not measured here.** It has its own workflow with its own gate, -[`justdummies-mutation`](justdummies-mutation.en.md), because it is headed for a -repository of its own ([ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.md)). -The two are the same machine with different matrices; everything in this page -except the scope applies to both, and the JustDummies page links back here rather -than repeating it. +**JustDummies is not measured here.** It left for a repository of its own +([ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.md), executed by +[ADR-0069](../adr/0069-consume-justdummies-from-its-own-repository.md)), taking its +workflow with it — which is what that workflow was written to make possible: a file +move, not an edit. It is measured in +[`Reefact/just-dummies`](https://github.com/Reefact/just-dummies). ## When it runs @@ -168,7 +168,7 @@ for the odd equivalent mutant. **Five projects have no bar yet** — the analyzers, the documentation generator, the command line, `JustDummies`, and the JustDummies analyzers that shipped with -[ADR-0044](../adr/0044-ship-justdummies-analyzers.md). No full-sweep score was +[just-dummies ADR-0023](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0023-ship-justdummies-analyzers.md). No full-sweep score was ever measured for any of them — for most, because the sweep is too long to have been run interactively — and a bar was **not** guessed: their `break` is `0`. Their legs still run, still fail on a broken build or a failing suite, and still @@ -186,9 +186,9 @@ today, and a pull request touching one of its weaker files can still fall under it. That is the gate working, not misfiring — the report says which assertion is missing. -One library escapes this rule for now: `JustDummies`, whose sweep is too long to -have been calibrated against, ships with its score gate off. See -[`justdummies-mutation`](justdummies-mutation.en.md#justdummies-has-no-score-threshold-yet). +`JustDummies` used to escape this rule — its sweep was too long to have been +calibrated against, so it shipped with its score gate off. That exception left with +it; the repository that now owns it owns the calibration too. ## When the survivor is an equivalent mutant diff --git a/doc/handwritten/for-maintainers/workflows/mutation.fr.md b/doc/handwritten/for-maintainers/workflows/mutation.fr.md index 8f450473..4ca4771e 100644 --- a/doc/handwritten/for-maintainers/workflows/mutation.fr.md +++ b/doc/handwritten/for-maintainers/workflows/mutation.fr.md @@ -34,13 +34,12 @@ la ligne de commande `fce`, le générateur de documentation, les analyseurs Roslyn. Ce qui en reste dehors, et pourquoi, est sous *À manipuler avec précaution* plus bas. -**JustDummies n'est pas mesuré ici.** Il a son propre workflow et son propre -barrage, [`justdummies-mutation`](justdummies-mutation.fr.md), parce qu'il est -destiné à un dépôt à lui -([ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.fr.md)). Les deux -sont la même machine avec une matrice différente ; tout ce qui suit, hormis le -périmètre, vaut pour les deux, et la page JustDummies renvoie ici plutôt que de -le répéter. +**JustDummies n'est pas mesuré ici.** Il est parti dans un dépôt à lui +([ADR-0011](../adr/0011-host-dummies-as-a-standalone-package.fr.md), exécuté par +[ADR-0069](../adr/0069-consume-justdummies-from-its-own-repository.fr.md)), en +emportant son workflow — ce que ce workflow était précisément écrit pour rendre +possible : un déplacement de fichier, pas une édition. Il est mesuré dans +[`Reefact/just-dummies`](https://github.com/Reefact/just-dummies). ## Quand il s'exécute @@ -185,7 +184,7 @@ arrondi vers le bas, avec un peu de marge pour l'éventuel mutant équivalent. **Cinq projets n'ont pas encore de barre** : les analyseurs, le générateur de documentation, la ligne de commande, `JustDummies`, et les analyseurs JustDummies -arrivés avec l'[ADR-0044](../adr/0044-ship-justdummies-analyzers.fr.md). Aucun +arrivés avec l'[just-dummies ADR-0023](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-maintainers/adr/0023-ship-justdummies-analyzers.md). Aucun score de balayage complet n'a été mesuré pour eux — pour la plupart, parce que le balayage est trop long pour avoir été exécuté interactivement — et une barre n'a **pas** été devinée : leur `break` vaut `0`. Leurs branches tournent quand même, @@ -204,10 +203,10 @@ barre basse aujourd'hui, et une pull request qui touche l'un de ses fichiers les plus faibles peut quand même passer dessous. C'est le barrage qui fonctionne, pas qui se trompe — le rapport dit quelle assertion manque. -Une bibliothèque échappe pour l'instant à cette règle : `JustDummies`, dont le -balayage est trop long pour avoir servi de calibration, part avec son barrage sur -le score coupé. Voir -[`justdummies-mutation`](justdummies-mutation.fr.md#justdummies-na-pas-encore-de-seuil-de-score). +`JustDummies` échappait à cette règle — son balayage était trop long pour avoir +servi de calibration, il partait donc avec son barrage sur le score coupé. Cette +exception est partie avec lui ; le dépôt qui le porte désormais porte aussi sa +calibration. ## Quand le survivant est un mutant équivalent diff --git a/doc/handwritten/for-users/analyzers/JD001.en.md b/doc/handwritten/for-users/analyzers/JD001.en.md deleted file mode 100644 index 281a1639..00000000 --- a/doc/handwritten/for-users/analyzers/JD001.en.md +++ /dev/null @@ -1,44 +0,0 @@ -# JD001: AsyncBodyPassedToReproducibly - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD001.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🔴 Error | -| **Enabled by default** | Yes | - -`Any.Reproducibly` takes a synchronous `Action`. An `async` lambda bound to it becomes `async void`: the body runs to its first `await`, then its continuation — every assertion after that `await` — runs *after* the call has already returned, and the exception escapes the reproducible scope entirely. The test passes green even though the body failed. - -Pass the asynchronous body to `Any.ReproduciblyAsync(Func)` and `await` it. For a synchronous body, keep `Any.Reproducibly(() => { ... })`. - -## Noncompliant - -```csharp -[Fact] -public void Prices_are_positive() { - Any.Reproducibly(async () => { // JD001: the async body runs as 'async void' - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); // this failure never reaches the test runner - }); -} -``` - -## Compliant - -```csharp -[Fact] -public async Task Prices_are_positive() { - await Any.ReproduciblyAsync(async () => { - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); - }); -} -``` - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD001.fr.md b/doc/handwritten/for-users/analyzers/JD001.fr.md deleted file mode 100644 index 851364f4..00000000 --- a/doc/handwritten/for-users/analyzers/JD001.fr.md +++ /dev/null @@ -1,44 +0,0 @@ -# JD001 : AsyncBodyPassedToReproducibly - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD001.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🔴 Erreur | -| **Activée par défaut** | Oui | - -`Any.Reproducibly` prend une `Action` synchrone. Une lambda `async` qui s'y lie devient `async void` : le corps s'exécute jusqu'à son premier `await`, puis sa continuation — chaque assertion après cet `await` — s'exécute *après* le retour de l'appel, et l'exception échappe entièrement à la portée reproductible. Le test passe au vert alors même que le corps a échoué. - -Passez le corps asynchrone à `Any.ReproduciblyAsync(Func)` et faites `await`. Pour un corps synchrone, gardez `Any.Reproducibly(() => { ... })`. - -## Non conforme - -```csharp -[Fact] -public void Prices_are_positive() { - Any.Reproducibly(async () => { // JD001 : le corps async s'exécute en 'async void' - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); // cet échec n'atteint jamais le lanceur de tests - }); -} -``` - -## Conforme - -```csharp -[Fact] -public async Task Prices_are_positive() { - await Any.ReproduciblyAsync(async () => { - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); - }); -} -``` - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD002.en.md b/doc/handwritten/for-users/analyzers/JD002.en.md deleted file mode 100644 index d3b5e4ce..00000000 --- a/doc/handwritten/for-users/analyzers/JD002.en.md +++ /dev/null @@ -1,48 +0,0 @@ -# JD002: DiscardedReproduciblyAsyncResult - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD002.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🔴 Error | -| **Enabled by default** | Yes | - -`Any.ReproduciblyAsync` returns a `Task` that faults with the body's exception. Discarding it — as a standalone statement, or via `_ =` — lets a failing test pass green, because the failure is never observed. `await` the returned task. - -The compiler's own `CS4014` does not catch this in a synchronous (`void`) test method, which is exactly where the mistake is easiest to make. - -## Noncompliant - -```csharp -[Fact] -public void Prices_are_positive() { - Any.ReproduciblyAsync(async () => { // JD002: the returned task is discarded - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); - }); -} -``` - -```csharp -_ = Any.ReproduciblyAsync(async () => { /* ... */ }); // JD002: the returned task is discarded -``` - -## Compliant - -```csharp -[Fact] -public async Task Prices_are_positive() { - await Any.ReproduciblyAsync(async () => { - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); - }); -} -``` - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD002.fr.md b/doc/handwritten/for-users/analyzers/JD002.fr.md deleted file mode 100644 index 072bddbe..00000000 --- a/doc/handwritten/for-users/analyzers/JD002.fr.md +++ /dev/null @@ -1,48 +0,0 @@ -# JD002 : DiscardedReproduciblyAsyncResult - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD002.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🔴 Erreur | -| **Activée par défaut** | Oui | - -`Any.ReproduciblyAsync` retourne un `Task` qui échoue avec l'exception du corps. Le jeter — en instruction isolée, ou via `_ =` — laisse un test défaillant passer au vert, parce que l'échec n'est jamais observé. Faites `await` sur le `Task` retourné. - -Le `CS4014` natif du compilateur ne l'attrape pas dans une méthode de test synchrone (`void`), précisément là où l'erreur est la plus facile à commettre. - -## Non conforme - -```csharp -[Fact] -public void Prices_are_positive() { - Any.ReproduciblyAsync(async () => { // JD002 : le Task retourné est jeté - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); - }); -} -``` - -```csharp -_ = Any.ReproduciblyAsync(async () => { /* ... */ }); // JD002 : le Task retourné est jeté -``` - -## Conforme - -```csharp -[Fact] -public async Task Prices_are_positive() { - await Any.ReproduciblyAsync(async () => { - var price = Any.Decimal().Positive().Generate(); - await _repository.SaveAsync(price); - Assert.True(price > 0); - }); -} -``` - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD003.en.md b/doc/handwritten/for-users/analyzers/JD003.en.md deleted file mode 100644 index 7072cdc5..00000000 --- a/doc/handwritten/for-users/analyzers/JD003.en.md +++ /dev/null @@ -1,54 +0,0 @@ -# JD003: AwaitableBodyPassedToReproducibly - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD003.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🔴 Error | -| **Enabled by default** | Yes | - -[JD001](JD001.en.md) reports an `async` lambda passed to `Any.Reproducibly`. Two neighbouring shapes do the same damage and JD001 does not see either, because it reads the lambda's own `async` marker: - -* a **synchronous** lambda whose body produces a task — `Any.Reproducibly(() => sut.SaveAsync(x))`. The lambda binds to `Action`, so the task is created and dropped. `CS4014` does not fire, because the enclosing lambda is not itself `async`; -* an **`async void`** method passed as a method group — `Any.Reproducibly(Body)`. It binds to `Action` with no warning, and its post-`await` exception escapes the reproducible scope's `try`/`catch` entirely. - -In both cases `Any.Reproducibly` returns before the body's assertions run, and their failures never reach the test runner. The test passes green. - -Pass the asynchronous body to `Any.ReproduciblyAsync(Func)` and `await` it. - -## Noncompliant - -```csharp -[Fact] -public void Prices_are_saved() { - Any.Reproducibly(() => _repository.SaveAsync(price)); // JD003: the task is dropped -} - -[Fact] -public void Prices_are_saved_too() { - Any.Reproducibly(SaveAsync); // JD003: 'async void' bound to Action -} - -private static async void SaveAsync() { /* ... */ } -``` - -## Compliant - -```csharp -[Fact] -public async Task Prices_are_saved() { - await Any.ReproduciblyAsync(() => _repository.SaveAsync(price)); -} -``` - -## What it does not flag - -* A **nested** lambda or local function inside the body. Its binding and its author's intent are its own, so a deliberate fire-and-forget there is not this call's business. -* An `async` lambda — that is JD001's subject, and reporting both would raise two errors for one mistake. -* A body whose calls all return `void`. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD003.fr.md b/doc/handwritten/for-users/analyzers/JD003.fr.md deleted file mode 100644 index 9332ad59..00000000 --- a/doc/handwritten/for-users/analyzers/JD003.fr.md +++ /dev/null @@ -1,54 +0,0 @@ -# JD003 : AwaitableBodyPassedToReproducibly - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD003.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🔴 Erreur | -| **Activée par défaut** | Oui | - -[JD001](JD001.fr.md) signale une lambda `async` passée à `Any.Reproducibly`. Deux formes voisines causent le même dégât sans que JD001 les voie, car il lit le marqueur `async` de la lambda elle-même : - -* une lambda **synchrone** dont le corps produit une tâche — `Any.Reproducibly(() => sut.SaveAsync(x))`. La lambda se lie à `Action`, donc la tâche est créée puis abandonnée. `CS4014` ne se déclenche pas, la lambda englobante n'étant pas elle-même `async` ; -* une méthode **`async void`** passée en groupe de méthodes — `Any.Reproducibly(Body)`. Elle se lie à `Action` sans le moindre avertissement, et son exception post-`await` échappe entièrement au `try`/`catch` de la portée reproductible. - -Dans les deux cas, `Any.Reproducibly` retourne avant l'exécution des assertions du corps, et leurs échecs n'atteignent jamais le lanceur de tests. Le test passe au vert. - -Passez le corps asynchrone à `Any.ReproduciblyAsync(Func)` et faites `await`. - -## Non conforme - -```csharp -[Fact] -public void Prices_are_saved() { - Any.Reproducibly(() => _repository.SaveAsync(price)); // JD003 : la tâche est abandonnée -} - -[Fact] -public void Prices_are_saved_too() { - Any.Reproducibly(SaveAsync); // JD003 : 'async void' lié à Action -} - -private static async void SaveAsync() { /* ... */ } -``` - -## Conforme - -```csharp -[Fact] -public async Task Prices_are_saved() { - await Any.ReproduciblyAsync(() => _repository.SaveAsync(price)); -} -``` - -## Ce qui n'est pas signalé - -* Une lambda ou une fonction locale **imbriquée** dans le corps. Sa liaison et l'intention de son auteur lui appartiennent : un fire-and-forget délibéré à cet endroit ne regarde pas cet appel. -* Une lambda `async` — c'est le sujet de JD001, et signaler les deux lèverait deux erreurs pour une seule faute. -* Un corps dont tous les appels retournent `void`. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD004.en.md b/doc/handwritten/for-users/analyzers/JD004.en.md deleted file mode 100644 index 1bf41c3f..00000000 --- a/doc/handwritten/for-users/analyzers/JD004.en.md +++ /dev/null @@ -1,48 +0,0 @@ -# JD004: DiscardedSeedingResult - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD004.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🔴 Error | -| **Enabled by default** | Yes | - -Both seeding entry points return something the caller must keep, and both are silent when the result is thrown away. - -`Any.UseSeed(seed)` returns **the handle that closes the scope it opened**. Dropping it means the scope can never be closed: as the method's own documentation puts it, *"failing to dispose leaves the seed pinned for whatever runs next in the same execution context"*. Every later test flowing from that context silently stops being arbitrary — it replays one fixed sequence, and the tests become coupled to each other through draw order, so adding or reordering a test changes values in unrelated ones. - -`Any.WithSeed(seed)` returns an **isolated context** and pins nothing at all. A discarded call is dead code at a site that reads as if the run had been seeded: the ambient `Any.*` entry points keep drawing unseeded, exactly as if the line were not there. - -## Noncompliant - -```csharp -Any.UseSeed(1234); // JD004: the scope is never closed -string reference = Any.String().Generate(); - -Any.WithSeed(1234); // JD004: pins nothing — the draw below is still unseeded -int quantity = Any.Int32().Positive().Generate(); -``` - -## Compliant - -```csharp -using IDisposable scope = Any.UseSeed(1234); -string reference = Any.String().Generate(); - -AnyContext context = Any.WithSeed(1234); -int quantity = context.Int32().Positive().Generate(); -``` - -Inside a test body, prefer `Any.Reproducibly(() => { ... })`: it also reports the seed when the body fails, which a raw scope does not. - -## What it does not flag - -* A handle held by a `using` statement or a `using` declaration. -* A context captured in a variable, a field or a parameter. -* A test asserting that the call **rejects** its argument — `Expect.Throws(() => Any.UseSeed(seed, null!))`. No scope is opened there, so there is nothing to leak. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD004.fr.md b/doc/handwritten/for-users/analyzers/JD004.fr.md deleted file mode 100644 index b7a88977..00000000 --- a/doc/handwritten/for-users/analyzers/JD004.fr.md +++ /dev/null @@ -1,48 +0,0 @@ -# JD004 : DiscardedSeedingResult - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD004.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🔴 Erreur | -| **Activée par défaut** | Oui | - -Les deux points d'entrée de graine retournent quelque chose que l'appelant doit conserver, et tous deux sont silencieux lorsque le résultat est jeté. - -`Any.UseSeed(seed)` retourne **la poignée qui ferme la portée qu'il vient d'ouvrir**. L'abandonner rend la portée impossible à fermer : comme le dit la documentation de la méthode elle-même, *« ne pas disposer laisse la graine épinglée pour tout ce qui s'exécute ensuite dans le même contexte d'exécution »*. Chaque test ultérieur issu de ce contexte cesse silencieusement d'être arbitraire — il rejoue une séquence fixe — et les tests se couplent entre eux par l'ordre des tirages : ajouter ou réordonner un test change les valeurs de tests sans rapport. - -`Any.WithSeed(seed)` retourne un **contexte isolé** et n'épingle rien du tout. Un appel dont le résultat est jeté est du code mort à un endroit qui se lit comme si la graine avait été posée : les points d'entrée ambiants `Any.*` continuent de tirer sans graine, exactement comme si la ligne n'existait pas. - -## Non conforme - -```csharp -Any.UseSeed(1234); // JD004 : la portée n'est jamais fermée -string reference = Any.String().Generate(); - -Any.WithSeed(1234); // JD004 : n'épingle rien — le tirage ci-dessous reste sans graine -int quantity = Any.Int32().Positive().Generate(); -``` - -## Conforme - -```csharp -using IDisposable scope = Any.UseSeed(1234); -string reference = Any.String().Generate(); - -AnyContext context = Any.WithSeed(1234); -int quantity = context.Int32().Positive().Generate(); -``` - -Dans un corps de test, préférez `Any.Reproducibly(() => { ... })` : il rapporte aussi la graine lorsque le corps échoue, ce qu'une portée brute ne fait pas. - -## Ce qui n'est pas signalé - -* Une poignée tenue par une instruction `using` ou une déclaration `using`. -* Un contexte capturé dans une variable, un champ ou un paramètre. -* Un test qui vérifie que l'appel **rejette** son argument — `Expect.Throws(() => Any.UseSeed(seed, null!))`. Aucune portée n'y est ouverte, donc il n'y a rien à fuiter. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD005.en.md b/doc/handwritten/for-users/analyzers/JD005.en.md deleted file mode 100644 index 84d905eb..00000000 --- a/doc/handwritten/for-users/analyzers/JD005.en.md +++ /dev/null @@ -1,42 +0,0 @@ -# JD005: GeneratorRenderedAsText - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD005.fr.md) - -| | | -|---|---| -| **Category** | Usage (`JustDummies.Usage`) | -| **Severity** | 🔴 Error | -| **Enabled by default** | Yes | - -A generator is an immutable **recipe**, not a value — and no JustDummies generator overrides `ToString()`. Rendering one as text therefore yields the builder's CLR type name: `$"{Any.String()}"` produces the literal string `"JustDummies.AnyString"`. - -That string is non-empty, plausible-looking, and identical on every run. It flows into the message, the payload or the value object under test as if it were an arbitrary value, so the test goes green while exercising a constant that varies for nobody — the exact opposite of what `Any` is for. Nothing warns: the conversion is legal C#. - -Materialize the value with `Generate()`. - -## Noncompliant - -```csharp -string message = $"order {Any.String().NonEmpty()}"; // JD005: "order JustDummies.AnyString" -string reference = "ORD-" + Any.String().NonEmpty(); // JD005 -string quantity = Any.Int32().Positive().ToString(); // JD005: "JustDummies.AnyInt32" -``` - -## Compliant - -```csharp -string message = $"order {Any.String().NonEmpty().Generate()}"; -string reference = "ORD-" + Any.String().NonEmpty().Generate(); -string quantity = Any.Int32().Positive().Generate().ToString(); -``` - -## What it does not flag - -* A generated value — `Generate()` returns the value, and rendering it is the point. -* A consumer's own generator that meaningfully overrides `ToString()`: the call resolves to that override rather than to `object.ToString()`, and is deliberately left alone. -* A `ToString(...)` overload taking a format or a culture — that is never `object.ToString()`. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD005.fr.md b/doc/handwritten/for-users/analyzers/JD005.fr.md deleted file mode 100644 index e8fdbd5e..00000000 --- a/doc/handwritten/for-users/analyzers/JD005.fr.md +++ /dev/null @@ -1,42 +0,0 @@ -# JD005 : GeneratorRenderedAsText - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD005.en.md) - -| | | -|---|---| -| **Catégorie** | Usage (`JustDummies.Usage`) | -| **Sévérité** | 🔴 Erreur | -| **Activée par défaut** | Oui | - -Un générateur est une **recette** immuable, pas une valeur — et aucun générateur JustDummies ne surcharge `ToString()`. Le rendre sous forme de texte produit donc le nom de type CLR du constructeur : `$"{Any.String()}"` donne littéralement la chaîne `"JustDummies.AnyString"`. - -Cette chaîne est non vide, plausible, et identique à chaque exécution. Elle se propage dans le message, la charge utile ou l'objet valeur sous test comme s'il s'agissait d'une valeur arbitraire : le test passe au vert tout en exerçant une constante qui ne varie pour personne — exactement l'inverse de la raison d'être d'`Any`. Rien n'avertit : la conversion est du C# légal. - -Matérialisez la valeur avec `Generate()`. - -## Non conforme - -```csharp -string message = $"order {Any.String().NonEmpty()}"; // JD005 : « order JustDummies.AnyString » -string reference = "ORD-" + Any.String().NonEmpty(); // JD005 -string quantity = Any.Int32().Positive().ToString(); // JD005 : « JustDummies.AnyInt32 » -``` - -## Conforme - -```csharp -string message = $"order {Any.String().NonEmpty().Generate()}"; -string reference = "ORD-" + Any.String().NonEmpty().Generate(); -string quantity = Any.Int32().Positive().Generate().ToString(); -``` - -## Ce qui n'est pas signalé - -* Une valeur générée — `Generate()` retourne la valeur, et la rendre est précisément l'objectif. -* Un générateur écrit par le consommateur qui surcharge réellement `ToString()` : l'appel se résout vers cette surcharge et non vers `object.ToString()`, et il est délibérément laissé tranquille. -* Une surcharge `ToString(...)` prenant un format ou une culture — ce n'est jamais `object.ToString()`. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD006.en.md b/doc/handwritten/for-users/analyzers/JD006.en.md deleted file mode 100644 index 0d80dada..00000000 --- a/doc/handwritten/for-users/analyzers/JD006.en.md +++ /dev/null @@ -1,43 +0,0 @@ -# JD006: DiscardedGeneratorResult - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD006.fr.md) - -| | | -|---|---| -| **Category** | Usage (`JustDummies.Usage`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -A generator is an **immutable recipe**: every constraint returns a *new* generator rather than mutating the receiver. So `numbers.NonEmpty();` reads like it constrains `numbers` and constrains nothing at all — the declared invariant is silently dropped. - -What makes this worth a build-time diagnostic is the failure shape. The generator keeps drawing from the wider domain, so the test passes on most runs and fails on the one that draws outside it. An unconstrained `Any.String()` yields the empty string on roughly one run in sixteen: the test is green until it is not, and the value that broke it is gone. - -Assign the result back. - -## Noncompliant - -```csharp -AnyList numbers = Any.ListOf(Any.Int32()); -numbers.NonEmpty(); // JD006: the constrained generator is dropped - -List values = numbers.Generate(); // may still be empty -``` - -## Compliant - -```csharp -AnyList numbers = Any.ListOf(Any.Int32()).NonEmpty(); - -List values = numbers.Generate(); -``` - -## What it does not flag - -* An **explicit discard** — `_ = Any.StringMatching(pattern);`. The rule exists because the mistake is *silent*: a bare call reads as if it mutated the receiver. A discard cannot be misread that way, and it is how a test that only wants the construction to throw spells its intent. ([JD002](JD002.en.md) and [JD004](JD004.en.md) do report `_ =`, because discarding is never right there.) -* A test asserting that a constraint **throws**, when the illegal call is the whole body of a lambda argument — `Check.ThatCode(() => Any.String().WithLength(3).StartingWith("ORD-"))`. The exclusion is deliberately narrow: inside `Any.Reproducibly(() => { ... })` a dropped constraint is one statement of a block, not the body itself, and stays reported. -* A discarded generated *value* — `Generate()` returns the value, which is a different and much weaker smell. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD006.fr.md b/doc/handwritten/for-users/analyzers/JD006.fr.md deleted file mode 100644 index 24a36860..00000000 --- a/doc/handwritten/for-users/analyzers/JD006.fr.md +++ /dev/null @@ -1,43 +0,0 @@ -# JD006 : DiscardedGeneratorResult - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD006.en.md) - -| | | -|---|---| -| **Catégorie** | Usage (`JustDummies.Usage`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -Un générateur est une **recette immuable** : chaque contrainte retourne un *nouveau* générateur plutôt que de muter le receveur. Ainsi `numbers.NonEmpty();` se lit comme s'il contraignait `numbers` et ne contraint rien du tout — l'invariant déclaré est silencieusement perdu. - -Ce qui justifie un diagnostic à la compilation, c'est la forme de l'échec. Le générateur continue de tirer dans le domaine large : le test passe sur la plupart des exécutions et échoue sur celle qui tire en dehors. Un `Any.String()` non contraint produit la chaîne vide environ une exécution sur seize — le test est vert jusqu'à ce qu'il ne le soit plus, et la valeur qui l'a cassé a disparu. - -Réaffectez le résultat. - -## Non conforme - -```csharp -AnyList numbers = Any.ListOf(Any.Int32()); -numbers.NonEmpty(); // JD006 : le générateur contraint est perdu - -List values = numbers.Generate(); // peut toujours être vide -``` - -## Conforme - -```csharp -AnyList numbers = Any.ListOf(Any.Int32()).NonEmpty(); - -List values = numbers.Generate(); -``` - -## Ce qui n'est pas signalé - -* Un **discard explicite** — `_ = Any.StringMatching(pattern);`. La règle existe parce que la faute est *silencieuse* : un appel nu se lit comme s'il mutait le receveur. Un discard ne peut pas être mal lu ainsi, et c'est la façon dont un test qui veut seulement voir la construction échouer exprime son intention. ([JD002](JD002.fr.md) et [JD004](JD004.fr.md) signalent bien `_ =`, car y jeter le résultat n'est jamais correct.) -* Un test qui vérifie qu'une contrainte **lève**, lorsque l'appel illégal constitue tout le corps d'une lambda passée en argument — `Check.ThatCode(() => Any.String().WithLength(3).StartingWith("ORD-"))`. L'exclusion est volontairement étroite : dans `Any.Reproducibly(() => { ... })`, une contrainte perdue est une instruction d'un bloc et non le corps lui-même, et elle reste signalée. -* Une *valeur* générée dont le résultat est jeté — `Generate()` retourne la valeur, ce qui est une odeur différente et bien plus faible. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD007.en.md b/doc/handwritten/for-users/analyzers/JD007.en.md deleted file mode 100644 index 4899d3b6..00000000 --- a/doc/handwritten/for-users/analyzers/JD007.en.md +++ /dev/null @@ -1,58 +0,0 @@ -# JD007: DrawOutsideThePinnedScope - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD007.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -`[Reproducible]` pins the seed from an xUnit *before/after* hook. xUnit constructs the test-class instance — and awaits `IAsyncLifetime.InitializeAsync` — **before** running those hooks, so anything drawn during construction comes from the unseeded ambient source. - -The result is worse than no reproducibility at all: the test advertises it. The failure prints *"Reproduce this run with `[Reproducible(Seed = 1234)]`"*, the reader pins 1234, the arrangement still differs, the failure does not come back, and the seed looks broken. - -Draw inside the test body, or hold the **generator** in the field and materialize it per test. - -## Noncompliant - -```csharp -[Reproducible] -public class OrderTests { - private readonly string _reference; - - public OrderTests() { - _reference = Any.String().StartingWith("ORD-").Generate(); // JD007: drawn before the seed is pinned - } - - [Fact] - public void It_is_accepted() { /* uses _reference */ } -} -``` - -## Compliant - -```csharp -[Reproducible] -public class OrderTests { - // The recipe is safe to share: the random source is resolved at Generate(), not at construction. - private readonly IAny _reference = Any.String().StartingWith("ORD-"); - - [Fact] - public void It_is_accepted() { - string reference = _reference.Generate(); // drawn inside the pinned scope - } -} -``` - -## What it does not flag - -* A draw in the **test body**, which is inside the scope — verified against xunit.v3 by probe before this rule was written. -* A draw from an isolated `Any.WithSeed(...)` context: that context is unaffected by the ambient scope by design, so reporting it would be wrong. -* A generator reached through a local, a field or a parameter rather than written inline from `Any`. The rule only reports a chain it can prove starts at the ambient source, which under-reports rather than misfiring. -* An `IClassFixture` constructor. The fixture type does not itself carry `[Reproducible]`, so the rule cannot see the link; the draw is equally unpinned, and the help page is the only warning you get. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD007.fr.md b/doc/handwritten/for-users/analyzers/JD007.fr.md deleted file mode 100644 index 34d5d218..00000000 --- a/doc/handwritten/for-users/analyzers/JD007.fr.md +++ /dev/null @@ -1,58 +0,0 @@ -# JD007 : DrawOutsideThePinnedScope - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD007.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -`[Reproducible]` épingle la graine depuis un hook *before/after* d'xUnit. Or xUnit construit l'instance de la classe de test — et attend `IAsyncLifetime.InitializeAsync` — **avant** d'exécuter ces hooks : tout ce qui est tiré pendant la construction provient donc de la source ambiante non ensemencée. - -Le résultat est pire qu'une absence de reproductibilité : le test l'annonce. L'échec affiche « *Reproduce this run with `[Reproducible(Seed = 1234)]`* », le lecteur épingle 1234, l'arrangement diffère quand même, l'échec ne revient pas, et la graine paraît cassée. - -Tirez dans le corps du test, ou conservez le **générateur** dans le champ et matérialisez-le à chaque test. - -## Non conforme - -```csharp -[Reproducible] -public class OrderTests { - private readonly string _reference; - - public OrderTests() { - _reference = Any.String().StartingWith("ORD-").Generate(); // JD007 : tiré avant l'épinglage - } - - [Fact] - public void It_is_accepted() { /* utilise _reference */ } -} -``` - -## Conforme - -```csharp -[Reproducible] -public class OrderTests { - // La recette est partageable sans risque : la source aléatoire est résolue à Generate(), pas à la construction. - private readonly IAny _reference = Any.String().StartingWith("ORD-"); - - [Fact] - public void It_is_accepted() { - string reference = _reference.Generate(); // tiré dans la portée épinglée - } -} -``` - -## Ce qui n'est pas signalé - -* Un tirage dans le **corps du test**, qui est bien dans la portée — vérifié par sonde contre xunit.v3 avant l'écriture de cette règle. -* Un tirage depuis un contexte isolé `Any.WithSeed(...)` : ce contexte échappe par conception à la portée ambiante, le signaler serait faux. -* Un générateur atteint par une variable locale, un champ ou un paramètre plutôt qu'écrit en ligne depuis `Any`. La règle ne signale qu'une chaîne dont elle peut prouver qu'elle part de la source ambiante : elle sous-signale plutôt que de se tromper. -* Un constructeur d'`IClassFixture`. Le type de fixture ne porte pas lui-même `[Reproducible]`, donc la règle ne peut pas voir le lien ; le tirage y est tout aussi non épinglé, et cette page est le seul avertissement dont vous disposez. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD008.en.md b/doc/handwritten/for-users/analyzers/JD008.en.md deleted file mode 100644 index 327685b7..00000000 --- a/doc/handwritten/for-users/analyzers/JD008.en.md +++ /dev/null @@ -1,60 +0,0 @@ -# JD008: ArbitraryValueInTheoryData - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD008.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -xUnit evaluates a theory's data provider at **discovery**, before any test case runs. A value drawn there is therefore drawn once for the whole run, outside every seed scope — `Any.Reproducibly` and `[Reproducible]` alike. - -Three defects ride on that one shape, and all three are silent: - -* every case of the theory receives the **same** value, so a theory that reads as if it enumerated arbitrary cases enumerates a constant; -* the value is replayable from no reported seed, because no seed was pinned when it was drawn; -* the draw happens even for cases that are filtered out, and its position in the sequence shifts when cases are added or removed. - -Draw in the test body, or let the provider yield the **generator** and materialize it inside the test. - -## Noncompliant - -```csharp -public static TheoryData Cases => new() { - Any.String().NonEmpty().Generate(), // JD008: drawn at discovery, shared by every case -}; - -[Theory] -[MemberData(nameof(Cases))] -public void It_is_accepted(string reference) { /* ... */ } -``` - -## Compliant - -```csharp -public static TheoryData> Cases => new() { - Any.String().NonEmpty(), // the recipe travels, not the value -}; - -[Theory, Reproducible] -[MemberData(nameof(Cases))] -public void It_is_accepted(IAny reference) { - string value = reference.Generate(); // drawn per case, inside the pinned scope -} -``` - -## What it recognises as a provider - -A member named by a `[MemberData]` in the same type; a member returning `TheoryData` or `TheoryData<...>`; a member returning a sequence of `object[]`; and a type implementing that sequence, which is the `[ClassData]` shape. - -## What it does not flag - -* A provider that yields generators rather than values — the compliant shape above. -* A draw in an ordinary test body, which is the point of the rule. -* A draw from an isolated `Any.WithSeed(...)` context, or a generator reached through a local or field rather than written inline from `Any`. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD008.fr.md b/doc/handwritten/for-users/analyzers/JD008.fr.md deleted file mode 100644 index 61682c48..00000000 --- a/doc/handwritten/for-users/analyzers/JD008.fr.md +++ /dev/null @@ -1,60 +0,0 @@ -# JD008 : ArbitraryValueInTheoryData - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD008.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -xUnit évalue le fournisseur de données d'une théorie à la **découverte**, avant l'exécution du moindre cas de test. Une valeur tirée là l'est donc une seule fois pour toute la campagne, hors de toute portée de graine — aussi bien `Any.Reproducibly` que `[Reproducible]`. - -Trois défauts tiennent dans cette seule forme, et les trois sont silencieux : - -* tous les cas de la théorie reçoivent la **même** valeur : une théorie qui se lit comme énumérant des cas arbitraires énumère une constante ; -* la valeur n'est rejouable depuis aucune graine rapportée, puisque aucune n'était épinglée au moment du tirage ; -* le tirage a lieu même pour les cas filtrés, et sa position dans la séquence se décale dès qu'on ajoute ou retire un cas. - -Tirez dans le corps du test, ou faites livrer le **générateur** par le fournisseur et matérialisez-le dans le test. - -## Non conforme - -```csharp -public static TheoryData Cases => new() { - Any.String().NonEmpty().Generate(), // JD008 : tiré à la découverte, partagé par tous les cas -}; - -[Theory] -[MemberData(nameof(Cases))] -public void It_is_accepted(string reference) { /* ... */ } -``` - -## Conforme - -```csharp -public static TheoryData> Cases => new() { - Any.String().NonEmpty(), // c'est la recette qui voyage, pas la valeur -}; - -[Theory, Reproducible] -[MemberData(nameof(Cases))] -public void It_is_accepted(IAny reference) { - string value = reference.Generate(); // tiré par cas, dans la portée épinglée -} -``` - -## Ce qu'elle reconnaît comme fournisseur - -Un membre nommé par un `[MemberData]` du même type ; un membre retournant `TheoryData` ou `TheoryData<...>` ; un membre retournant une séquence d'`object[]` ; et un type implémentant cette séquence, ce qui est la forme `[ClassData]`. - -## Ce qui n'est pas signalé - -* Un fournisseur qui livre des générateurs plutôt que des valeurs — la forme conforme ci-dessus. -* Un tirage dans un corps de test ordinaire, ce qui est précisément l'objectif de la règle. -* Un tirage depuis un contexte isolé `Any.WithSeed(...)`, ou un générateur atteint par une variable locale ou un champ plutôt qu'écrit en ligne depuis `Any`. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD009.en.md b/doc/handwritten/for-users/analyzers/JD009.en.md deleted file mode 100644 index dc733ebe..00000000 --- a/doc/handwritten/for-users/analyzers/JD009.en.md +++ /dev/null @@ -1,47 +0,0 @@ -# JD009: DrawInStaticInitializer - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD009.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -A type initializer runs **once, lazily**, when the first test touches the type. A value drawn there is therefore drawn under whatever ambient context that particular test happened to have pinned — and then shared, unchanged, by every other test in the class. - -Three consequences follow, none of them visible: - -* the value never varies between runs, so the arbitrary-by-default property that surfaces a test secretly depending on one particular value is switched off; -* the tests become **order-dependent** — a test can pass because the value was drawn under a sibling's seed, and start failing when the suite is reordered or filtered; -* no reported seed replays it, because the draw belongs to whichever test ran first, not to the one that failed. - -Store the **generator** in the static field and call `Generate()` where the value is needed. The random source is resolved at `Generate()` time, never at construction, so a shared generator is safe and idiomatic — only a shared *value* is not. - -## Noncompliant - -```csharp -private static readonly string Tenant = Any.String().NonEmpty().Generate(); // JD009: one draw for the whole suite -``` - -## Compliant - -```csharp -private static readonly IAny Tenant = Any.String().NonEmpty(); - -// ... per test: -string tenant = Tenant.Generate(); -``` - -## What it does not flag - -* A static field holding the **generator** — the compliant shape, and explicitly safe. -* A draw in an instance field initializer or constructor: that is [JD007](JD007.en.md)'s subject when the class is `[Reproducible]`, and outside its scope otherwise. -* A draw from an isolated `Any.WithSeed(...)` context, or a generator reached through a local or field rather than written inline from `Any`. - -A deliberately process-wide constant — a tenant id no test is coupled to — is a legitimate reading of the noncompliant shape. The hazard analysis still holds, so the rule reports it; suppress it at the site if that is genuinely what you want. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD009.fr.md b/doc/handwritten/for-users/analyzers/JD009.fr.md deleted file mode 100644 index 94a86c62..00000000 --- a/doc/handwritten/for-users/analyzers/JD009.fr.md +++ /dev/null @@ -1,47 +0,0 @@ -# JD009 : DrawInStaticInitializer - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD009.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -Un initialiseur de type s'exécute **une fois, paresseusement**, quand le premier test touche le type. Une valeur tirée là l'est donc sous le contexte ambiant que ce test-là se trouvait avoir épinglé — puis elle est partagée, inchangée, par tous les autres tests de la classe. - -Trois conséquences en découlent, aucune visible : - -* la valeur ne varie plus d'une exécution à l'autre : la propriété « arbitraire par défaut », celle qui révèle un test dépendant secrètement d'une valeur particulière, est désactivée ; -* les tests deviennent **dépendants de l'ordre** — un test peut passer parce que la valeur a été tirée sous la graine d'un voisin, et se mettre à échouer dès qu'on réordonne ou filtre la suite ; -* aucune graine rapportée ne la rejoue, le tirage appartenant au test qui s'est exécuté en premier et non à celui qui a échoué. - -Stockez le **générateur** dans le champ statique et appelez `Generate()` là où la valeur est nécessaire. La source aléatoire est résolue au moment de `Generate()`, jamais à la construction : un générateur partagé est donc sûr et idiomatique — seule une *valeur* partagée ne l'est pas. - -## Non conforme - -```csharp -private static readonly string Tenant = Any.String().NonEmpty().Generate(); // JD009 : un seul tirage pour toute la suite -``` - -## Conforme - -```csharp -private static readonly IAny Tenant = Any.String().NonEmpty(); - -// ... dans chaque test : -string tenant = Tenant.Generate(); -``` - -## Ce qui n'est pas signalé - -* Un champ statique portant le **générateur** — la forme conforme, explicitement sûre. -* Un tirage dans un initialiseur de champ d'instance ou un constructeur : c'est le sujet de [JD007](JD007.fr.md) quand la classe est `[Reproducible]`, et hors de sa portée sinon. -* Un tirage depuis un contexte isolé `Any.WithSeed(...)`, ou un générateur atteint par une variable locale ou un champ plutôt qu'écrit en ligne depuis `Any`. - -Une constante délibérément valable pour tout le processus — un identifiant de locataire auquel aucun test n'est couplé — est une lecture légitime de la forme non conforme. L'analyse du risque tient toujours, donc la règle la signale ; supprimez-la localement si c'est réellement ce que vous voulez. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD010.en.md b/doc/handwritten/for-users/analyzers/JD010.en.md deleted file mode 100644 index fa0f70d5..00000000 --- a/doc/handwritten/for-users/analyzers/JD010.en.md +++ /dev/null @@ -1,49 +0,0 @@ -# JD010: ReproducibleOnNonTestMethod - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD010.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -xUnit collects `BeforeAfterTestAttribute`s from the **test method**, its declaring **class** and the **assembly** — nowhere else. `[Reproducible]` on a helper method, on an arrange method, or on a method whose `[Fact]` was removed during a refactor, is never read: it pins no seed and reports none. - -What makes this worth a diagnostic is that a *working* `[Reproducible]` is silent by design — a passing test reports nothing. The inert form and the working form are therefore indistinguishable from the outside, right up until a failure that should have named a seed does not. - -Remove it, or move it to the level xUnit actually reads. - -## Noncompliant - -```csharp -public class OrderTests { - [Reproducible] // JD010: never read — this is not a test - private string ArrangeReference() => Any.String().StartingWith("ORD-").Generate(); - - [Fact] - public void It_is_accepted() { /* uses ArrangeReference() */ } -} -``` - -## Compliant - -```csharp -[Reproducible] // the class level covers every test it declares -public class OrderTests { - private string ArrangeReference() => Any.String().StartingWith("ORD-").Generate(); - - [Fact] - public void It_is_accepted() { /* ... */ } -} -``` - -## What it does not flag - -* The attribute on a `[Fact]`, a `[Theory]`, or any third-party attribute implementing xUnit's `IFactAttribute` — the rule keys on that interface, not on a fixed list. -* The attribute on a class or on the assembly, which are exactly the levels xUnit does collect. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD010.fr.md b/doc/handwritten/for-users/analyzers/JD010.fr.md deleted file mode 100644 index 324087d1..00000000 --- a/doc/handwritten/for-users/analyzers/JD010.fr.md +++ /dev/null @@ -1,49 +0,0 @@ -# JD010 : ReproducibleOnNonTestMethod - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD010.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -xUnit collecte les `BeforeAfterTestAttribute` depuis la **méthode de test**, sa **classe** déclarante et l'**assembly** — nulle part ailleurs. `[Reproducible]` sur une méthode utilitaire, sur une méthode d'arrangement, ou sur une méthode dont le `[Fact]` a disparu lors d'un remaniement, n'est jamais lu : il n'épingle aucune graine et n'en rapporte aucune. - -Ce qui justifie un diagnostic, c'est qu'un `[Reproducible]` *qui fonctionne* est silencieux par conception — un test qui passe ne rapporte rien. La forme inerte et la forme active sont donc indiscernables de l'extérieur, jusqu'au jour où un échec qui aurait dû nommer une graine ne le fait pas. - -Retirez-le, ou déplacez-le au niveau qu'xUnit lit réellement. - -## Non conforme - -```csharp -public class OrderTests { - [Reproducible] // JD010 : jamais lu — ce n'est pas un test - private string ArrangeReference() => Any.String().StartingWith("ORD-").Generate(); - - [Fact] - public void It_is_accepted() { /* utilise ArrangeReference() */ } -} -``` - -## Conforme - -```csharp -[Reproducible] // le niveau classe couvre tous les tests qu'elle déclare -public class OrderTests { - private string ArrangeReference() => Any.String().StartingWith("ORD-").Generate(); - - [Fact] - public void It_is_accepted() { /* ... */ } -} -``` - -## Ce qui n'est pas signalé - -* L'attribut sur un `[Fact]`, une `[Theory]`, ou tout attribut tiers implémentant l'`IFactAttribute` d'xUnit — la règle s'appuie sur cette interface, pas sur une liste figée. -* L'attribut sur une classe ou sur l'assembly, qui sont précisément les niveaux qu'xUnit collecte. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD011.en.md b/doc/handwritten/for-users/analyzers/JD011.en.md deleted file mode 100644 index 30efe993..00000000 --- a/doc/handwritten/for-users/analyzers/JD011.en.md +++ /dev/null @@ -1,56 +0,0 @@ -# JD011: GeneratorWhereValueExpected - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD011.fr.md) - -| | | -|---|---| -| **Category** | Usage (`JustDummies.Usage`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | **No — opt-in** | - -Generators are reference types, so an `object`, `dynamic` or `params object[]` position accepts one with **no conversion at all**. This is the residue [ADR-0020](../../for-maintainers/adr/0020-materialize-dummies-only-through-generate.md) could not close by removing the implicit conversions: there was nothing to remove here. - -The recipe then survives as an opaque object: - -* an assertion helper taking `object` inspects the recipe — `Assert.NotNull(Any.String())` is green for ever and asserts nothing; -* a theory row built as `object[]` feeds the generator itself to the code under test; -* `gen.Equals(value)` resolves to `object.Equals`, which is reference equality against an unrelated object: false for every run and every seed. - -## Enabling it - -```ini -dotnet_diagnostic.JD011.severity = warning -``` - -## Noncompliant - -```csharp -Assert.NotNull(Any.String().NonEmpty()); // JD011: asserts the recipe is non-null. It always is. -object[] row = { Any.Int32().Positive(), 1 }; // JD011: the row carries a recipe -bool same = Any.String().NonEmpty().Equals(expected); // JD011: false, always -``` - -## Compliant - -```csharp -Assert.NotNull(Any.String().NonEmpty().Generate()); -object[] row = { Any.Int32().Positive().Generate(), 1 }; -bool same = Any.String().NonEmpty().Generate().Equals(expected); -``` - -## Why it ships opt-in - -[ADR-0059](../../for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) required this rule's default to be decided on measurement rather than intuition. Dogfooded across this repository's suites it produced **no true positive and two false ones**, both in a convention test that collects generators into a `List` on purpose — a shape indistinguishable from the theory-row mistake the rule exists to catch, and therefore impossible to narrow away. - -That is weak evidence for enabling it everywhere, and good evidence that it belongs in a consumer's suite, where `object`-typed assertion helpers are common and reflection over generator instances is not. Enable it there. - -## What it does not flag - -* A comparison between **two** generators — `ReferenceEquals(original, narrowed)` and `first.Equals(second)`. Comparing recipes by identity is how an immutability test proves a constraint returned a new generator; `Generate()` there would destroy the property under test. -* `Assert.Throws(() => Any.String().WithLength(3))`, which binds to `Func` rather than `Action` and so produces a real generator-to-`object` conversion. That shape exists at 88+ sites in this repository alone. -* A generated value, which is the point. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD011.fr.md b/doc/handwritten/for-users/analyzers/JD011.fr.md deleted file mode 100644 index 43b8fc1c..00000000 --- a/doc/handwritten/for-users/analyzers/JD011.fr.md +++ /dev/null @@ -1,56 +0,0 @@ -# JD011 : GeneratorWhereValueExpected - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD011.en.md) - -| | | -|---|---| -| **Catégorie** | Usage (`JustDummies.Usage`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | **Non — opt-in** | - -Les générateurs sont des types référence : une position `object`, `dynamic` ou `params object[]` en accepte un **sans aucune conversion**. C'est le résidu que l'[ADR-0020](../../for-maintainers/adr/0020-materialize-dummies-only-through-generate.md) ne pouvait pas fermer en supprimant les conversions implicites — il n'y avait rien à supprimer ici. - -La recette survit alors sous forme d'objet opaque : - -* un utilitaire d'assertion prenant `object` inspecte la recette — `Assert.NotNull(Any.String())` est vert pour toujours et n'assère rien ; -* une ligne de théorie construite en `object[]` livre le générateur lui-même au code sous test ; -* `gen.Equals(value)` se résout vers `object.Equals`, soit une égalité de référence contre un objet sans rapport : faux à chaque exécution et pour chaque graine. - -## Comment l'activer - -```ini -dotnet_diagnostic.JD011.severity = warning -``` - -## Non conforme - -```csharp -Assert.NotNull(Any.String().NonEmpty()); // JD011 : assère que la recette est non nulle. Elle l'est toujours. -object[] row = { Any.Int32().Positive(), 1 }; // JD011 : la ligne transporte une recette -bool same = Any.String().NonEmpty().Equals(expected); // JD011 : faux, toujours -``` - -## Conforme - -```csharp -Assert.NotNull(Any.String().NonEmpty().Generate()); -object[] row = { Any.Int32().Positive().Generate(), 1 }; -bool same = Any.String().NonEmpty().Generate().Equals(expected); -``` - -## Pourquoi elle est livrée en opt-in - -L'[ADR-0059](../../for-maintainers/adr/0059-guard-the-recipe-versus-value-boundary-with-analyzers.md) imposait que la valeur par défaut de cette règle soit tranchée sur mesure et non à l'intuition. Mise à l'épreuve sur les suites de ce dépôt, elle a produit **aucun vrai positif et deux faux**, tous deux dans un test de convention qui collecte délibérément des générateurs dans une `List` — une forme indiscernable de l'erreur de ligne de théorie que la règle vise, donc impossible à exclure. - -C'est une preuve faible en faveur d'une activation générale, et une bonne indication qu'elle a sa place dans la suite d'un consommateur, où les utilitaires d'assertion typés `object` sont courants et la réflexion sur des instances de générateurs ne l'est pas. Activez-la là. - -## Ce qui n'est pas signalé - -* Une comparaison entre **deux** générateurs — `ReferenceEquals(original, narrowed)` et `first.Equals(second)`. Comparer des recettes par identité est la façon dont un test d'immuabilité prouve qu'une contrainte a retourné un nouveau générateur ; `Generate()` y détruirait la propriété sous test. -* `Assert.Throws(() => Any.String().WithLength(3))`, qui se lie à `Func` et non à `Action`, produisant donc une véritable conversion générateur → `object`. Cette forme existe à plus de 88 endroits rien que dans ce dépôt. -* Une valeur générée, ce qui est l'objectif. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD012.en.md b/doc/handwritten/for-users/analyzers/JD012.en.md deleted file mode 100644 index 411b480a..00000000 --- a/doc/handwritten/for-users/analyzers/JD012.en.md +++ /dev/null @@ -1,40 +0,0 @@ -# JD012: GeneratorPooledAsValue - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD012.fr.md) - -| | | -|---|---| -| **Category** | Usage (`JustDummies.Usage`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -`Any.OneOf` draws one value from a pool of values. Handed generators, it infers the *builder* type as the pool's element type: the pool holds recipes, and drawing from it yields a recipe rather than a value — which then flows on into the `object` and text positions [JD005](JD005.en.md) and [JD011](JD011.en.md) report. - -What makes this a trap rather than an obvious slip is that the surface is inconsistent about it. `Any.OneOf(Any.Int32(), Any.Int64())` fails type inference and the compiler stops you; `Any.OneOf(Any.Int32(), Any.Int32())` binds cleanly and says nothing. - -## Noncompliant - -```csharp -IAny pool = Any.OneOf(Any.Int32().Positive(), Any.Int32().Negative()); // JD012 -AnyInt32 drawn = pool.Generate(); // a recipe, not a number -``` - -## Compliant - -```csharp -IAny pool = Any.OneOf(Any.Int32().Positive().Generate(), Any.Int32().Negative().Generate()); -int drawn = pool.Generate(); -``` - -Note what the compliant form changes: the pool is built **eagerly**, one draw per element at construction, and `Generate()` then chooses among those fixed values. That is the documented semantics of a choice pool — if you wanted a fresh draw from a chosen generator on every call, that is a different intent and `Any.OneOf` is not the tool. - -## What it does not flag - -* A pool of ordinary values, or of generated values. -* A `OneOf` on any type other than `Any` / `AnyContext`. -* A pool whose generators have different types — the compiler already refuses it. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD012.fr.md b/doc/handwritten/for-users/analyzers/JD012.fr.md deleted file mode 100644 index dcf3c1c5..00000000 --- a/doc/handwritten/for-users/analyzers/JD012.fr.md +++ /dev/null @@ -1,40 +0,0 @@ -# JD012 : GeneratorPooledAsValue - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD012.en.md) - -| | | -|---|---| -| **Catégorie** | Usage (`JustDummies.Usage`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -`Any.OneOf` tire une valeur parmi un ensemble de valeurs. Alimenté avec des générateurs, il infère le type du *constructeur* comme type d'élément : l'ensemble contient des recettes, et y tirer produit une recette plutôt qu'une valeur — laquelle se propage ensuite vers les positions `object` et texte que signalent [JD005](JD005.fr.md) et [JD011](JD011.fr.md). - -Ce qui en fait un piège plutôt qu'une bévue évidente, c'est l'incohérence de la surface. `Any.OneOf(Any.Int32(), Any.Int64())` échoue à l'inférence de types et le compilateur vous arrête ; `Any.OneOf(Any.Int32(), Any.Int32())` se lie proprement et ne dit rien. - -## Non conforme - -```csharp -IAny pool = Any.OneOf(Any.Int32().Positive(), Any.Int32().Negative()); // JD012 -AnyInt32 drawn = pool.Generate(); // une recette, pas un nombre -``` - -## Conforme - -```csharp -IAny pool = Any.OneOf(Any.Int32().Positive().Generate(), Any.Int32().Negative().Generate()); -int drawn = pool.Generate(); -``` - -Notez ce que la forme conforme change : l'ensemble est construit **avec empressement**, un tirage par élément à la construction, et `Generate()` choisit ensuite parmi ces valeurs figées. C'est la sémantique documentée d'un ensemble de choix — si vous vouliez un tirage frais depuis un générateur choisi à chaque appel, c'est une autre intention et `Any.OneOf` n'est pas l'outil. - -## Ce qui n'est pas signalé - -* Un ensemble de valeurs ordinaires, ou de valeurs générées. -* Un `OneOf` porté par un autre type qu'`Any` / `AnyContext`. -* Un ensemble dont les générateurs ont des types différents — le compilateur le refuse déjà. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD013.en.md b/doc/handwritten/for-users/analyzers/JD013.en.md deleted file mode 100644 index f715cf46..00000000 --- a/doc/handwritten/for-users/analyzers/JD013.en.md +++ /dev/null @@ -1,41 +0,0 @@ -# JD013: HeldCollectionPassedToOneOf - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD013.fr.md) - -| | | -|---|---| -| **Category** | Usage (`JustDummies.Usage`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -`Any.OneOf` takes `params T[]`. A single `List` argument therefore binds `T = List`, not `Order`: the pool holds **one** item, every draw returns the same list, and the arbitrary choice the test claims to make never varies. Nothing fails — the call compiles, the draws succeed, and the test is simply less arbitrary than it reads. - -[ADR-0032](../../for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) recorded this as an accepted risk, "mitigated by `ElementOf` being the documented path". This rule turns that mitigation from a documentation hope into a check. - -`Any.ElementOf` is the entry point that takes a collection and draws from its **elements**. - -## Noncompliant - -```csharp -List orders = ...; -IAny> pool = Any.OneOf(orders); // JD013: a pool of one — every draw is the same list -``` - -## Compliant - -```csharp -List orders = ...; -IAny pool = Any.ElementOf(orders); -``` - -## What it does not flag - -* `Any.OneOf>(orders)` — an explicit type argument states the opposite intent, and is the documented way to say "a pool whose single element is that collection". -* A single `string`, which is `IEnumerable`: a one-string pool is ordinary. -* An **array**. An array satisfies `params` directly, so `T` is already inferred as the element type and the call is correct as written. -* Two or more arguments, which cannot produce the confusion. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD013.fr.md b/doc/handwritten/for-users/analyzers/JD013.fr.md deleted file mode 100644 index ba97481d..00000000 --- a/doc/handwritten/for-users/analyzers/JD013.fr.md +++ /dev/null @@ -1,41 +0,0 @@ -# JD013 : HeldCollectionPassedToOneOf - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD013.en.md) - -| | | -|---|---| -| **Catégorie** | Usage (`JustDummies.Usage`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -`Any.OneOf` prend `params T[]`. Un unique argument `List` lie donc `T = List` et non `Order` : l'ensemble contient **un** élément, chaque tirage retourne la même liste, et le choix arbitraire que le test prétend faire ne varie jamais. Rien n'échoue — l'appel compile, les tirages réussissent, et le test est simplement moins arbitraire qu'il ne se lit. - -L'[ADR-0032](../../for-maintainers/adr/0032-draw-arbitrary-values-from-an-explicit-top-level-pool.md) a consigné cela comme un risque accepté, « atténué par le fait qu'`ElementOf` est le chemin documenté ». Cette règle transforme cette atténuation d'un espoir documentaire en vérification. - -`Any.ElementOf` est le point d'entrée qui prend une collection et tire parmi ses **éléments**. - -## Non conforme - -```csharp -List orders = ...; -IAny> pool = Any.OneOf(orders); // JD013 : un ensemble d'un seul élément — chaque tirage rend la même liste -``` - -## Conforme - -```csharp -List orders = ...; -IAny pool = Any.ElementOf(orders); -``` - -## Ce qui n'est pas signalé - -* `Any.OneOf>(orders)` — un argument de type explicite énonce l'intention inverse, et c'est la façon documentée de dire « un ensemble dont l'unique élément est cette collection ». -* Une `string` seule, qui est `IEnumerable` : un ensemble d'une chaîne est ordinaire. -* Un **tableau**. Un tableau satisfait `params` directement : `T` est déjà inféré comme le type d'élément et l'appel est correct tel quel. -* Deux arguments ou plus, qui ne peuvent pas produire la confusion. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD014.en.md b/doc/handwritten/for-users/analyzers/JD014.en.md deleted file mode 100644 index 9b461e35..00000000 --- a/doc/handwritten/for-users/analyzers/JD014.en.md +++ /dev/null @@ -1,53 +0,0 @@ -# JD014: RejectedConstantArgument - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD014.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -The argument is a compile-time constant the generator's own guard refuses, so the call throws **every time it runs**. Nothing is decided at run time that is not already decided at the call site — yet the failure surfaces only when that arrange line executes, often inside a helper shared by many tests, where it reads as a library problem rather than as the transposition typo it usually is. - -`Between(10, 5)` is the archetype: both parameters are `int`, both orders compile, and the mistake survives review because the call reads plausibly. - -## Noncompliant - -```csharp -Any.Int32().Between(10, 5) // the minimum must be ≤ the maximum — transposed -Any.String().WithLengthBetween(10, 5) // same -Any.String().WithLength(-1) // a size must not be negative -Any.String().WithLength(2_000_000) // above the largest producible size -Any.Int32().MultipleOf(0) // the multiple must be strictly positive -Any.Decimal().WithScale(29) // the scale must be in [0, 28] -Any.String().StartingWith("") // the fragment must not be empty -Any.String().WithChars("") // the character pool must not be empty -Any.String().OneOf() // at least one value is required -Any.DateTime().WithGranularity(TimeSpan.Zero) // the granularity must be strictly positive -``` - -## Compliant - -```csharp -Any.Int32().Between(5, 10) -Any.String().WithLength(12) -Any.Int32().MultipleOf(7) -Any.String().StartingWith("ORD-") -``` - -## Scope - -One rule over one table, rather than a rule per method: the library validates these in one place, with one message shape, and a reader who learns "a constant the guard rejects" has learned all of them. The table mirrors `SizeGuard` and the per-generator guards exactly — where it cannot be certain, it stays silent. - -## What it does not flag - -* A **non-constant** argument. That is the run-time guard's business, and it keeps it: this rule front-loads only the subset the compiler can already see. -* A conflict-asserting negative test — `Check.ThatCode(() => Any.Int32().Between(10, 5))` — where the illegal call is the whole body of a lambda argument. This repository writes hundreds of them. -* A same-named method on a type that is not a JustDummies generator. -* `Containing(item)` on a collection, which is not the string overload. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD014.fr.md b/doc/handwritten/for-users/analyzers/JD014.fr.md deleted file mode 100644 index 8ac36d6a..00000000 --- a/doc/handwritten/for-users/analyzers/JD014.fr.md +++ /dev/null @@ -1,53 +0,0 @@ -# JD014 : RejectedConstantArgument - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD014.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -L'argument est une constante de compilation que la garde du générateur refuse : l'appel lève **à chaque exécution**. Rien n'est décidé à l'exécution qui ne le soit déjà au site d'appel — pourtant l'échec ne se manifeste que lorsque cette ligne d'arrangement s'exécute, souvent au fond d'un utilitaire partagé par de nombreux tests, où il se lit comme un problème de bibliothèque plutôt que comme la faute de transposition qu'il est le plus souvent. - -`Between(10, 5)` est l'archétype : les deux paramètres sont des `int`, les deux ordres compilent, et l'erreur survit à la relecture parce que l'appel se lit plausiblement. - -## Non conforme - -```csharp -Any.Int32().Between(10, 5) // le minimum doit être ≤ au maximum — transposés -Any.String().WithLengthBetween(10, 5) // idem -Any.String().WithLength(-1) // une taille ne doit pas être négative -Any.String().WithLength(2_000_000) // au-delà de la plus grande taille productible -Any.Int32().MultipleOf(0) // le multiple doit être strictement positif -Any.Decimal().WithScale(29) // l'échelle doit être dans [0, 28] -Any.String().StartingWith("") // le fragment ne doit pas être vide -Any.String().WithChars("") // le pool de caractères ne doit pas être vide -Any.String().OneOf() // au moins une valeur est requise -Any.DateTime().WithGranularity(TimeSpan.Zero) // la granularité doit être strictement positive -``` - -## Conforme - -```csharp -Any.Int32().Between(5, 10) -Any.String().WithLength(12) -Any.Int32().MultipleOf(7) -Any.String().StartingWith("ORD-") -``` - -## Portée - -Une règle sur une table, plutôt qu'une règle par méthode : la bibliothèque valide tout cela au même endroit, avec une seule forme de message, et un lecteur qui a compris « une constante que la garde refuse » les a toutes comprises. La table reproduit exactement `SizeGuard` et les gardes propres à chaque générateur — là où elle ne peut pas être certaine, elle se tait. - -## Ce qui n'est pas signalé - -* Un argument **non constant**. C'est l'affaire de la garde d'exécution, et elle la conserve : cette règle n'anticipe que le sous-ensemble déjà visible du compilateur. -* Un test négatif vérifiant un conflit — `Check.ThatCode(() => Any.Int32().Between(10, 5))` — où l'appel illégal constitue tout le corps d'une lambda passée en argument. Ce dépôt en écrit des centaines. -* Une méthode homonyme portée par un type qui n'est pas un générateur JustDummies. -* `Containing(item)` sur une collection, qui n'est pas la surcharge chaîne. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD015.en.md b/doc/handwritten/for-users/analyzers/JD015.en.md deleted file mode 100644 index 237057fa..00000000 --- a/doc/handwritten/for-users/analyzers/JD015.en.md +++ /dev/null @@ -1,53 +0,0 @@ -# JD015: StringConstraintsAdmitNoValue - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD015.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -The declared constraints contradict each other for the constants written at the call site, so the chain throws a `ConflictingAnyConstraintException` the moment the arrange line runs. - -This is the case [ADR-0035](../../for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.md) names by hand as the one an analyzer should carry and the type system cannot: - -> `Any.String().Numeric().StartingWith("ORD-")` conflicts because the prefix's letters fall outside the numeric set, while `Any.String().Numeric().StartingWith("123")` is valid; the call site and the static types are identical in both. - -Only the argument's *value* tells them apart — which is what makes it value-dependent, and what puts it on the analyzer's side of the ADR's line. - -## Noncompliant - -```csharp -Any.String().Numeric().StartingWith("ORD-") // 'O' is not a digit -Any.String().LowerCase().StartingWith("ABC") // LowerCase forbids the uppercase 'A' -Any.String().WithLength(3).StartingWith("ORD-") // the prefix needs 4 characters -Any.String().WithMaxLength(6).StartingWith("SKU-").EndingWith("-EUR") // 8 characters into a cap of 6 -``` - -## Compliant - -```csharp -Any.String().Numeric().StartingWith("123") -Any.String().WithChars("ORD-0123456789").StartingWith("ORD-") // widen the pool to include the prefix -Any.String().WithLength(12).StartingWith("ORD-") -``` - -## The two checks, and what the library actually does - -**Character family.** `Alpha()`, `Numeric()`, `AlphaNumeric()` and `WithChars(...)` validate **every character** of every anchored fragment. - -**Casing.** `LowerCase()` and `UpperCase()` are *not* character sets. They constrain the case of a fragment's **letters** and say nothing about its other characters — so `UpperCase().StartingWith("ORD-")` is legal (the `-` is not a letter) while `UpperCase().StartingWith("abc")` is not. This distinction was found by dogfooding: the first version modelled casing as a pool and wrongly flagged a chain the library's own suite asserts is legal. - -**Length budget.** The fragments are laid out side by side and must fit the declared length. The budget is **skipped** once `OneOf(...)` is declared: a terminal value set changes what the fragments are checked against — they are matched against the pooled values rather than laid out — so `OneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba")` is legal even though the fragments sum to 4. - -## What it does not flag - -* A non-constant fragment, pool or length. -* A chain split across statements or variables. The chain must be written as one expression: following a generator through a local would need dataflow, and a rule that claims a chain is unsatisfiable must see every constraint it carries. -* A conflict-asserting negative test, where the illegal chain is the whole body of a lambda argument. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD015.fr.md b/doc/handwritten/for-users/analyzers/JD015.fr.md deleted file mode 100644 index 0da0b339..00000000 --- a/doc/handwritten/for-users/analyzers/JD015.fr.md +++ /dev/null @@ -1,53 +0,0 @@ -# JD015 : StringConstraintsAdmitNoValue - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD015.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -Les contraintes déclarées se contredisent pour les constantes écrites au site d'appel : la chaîne lève une `ConflictingAnyConstraintException` dès l'exécution de la ligne d'arrangement. - -C'est le cas que l'[ADR-0035](../../for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.md) nomme explicitement comme celui qu'un analyseur doit porter et que le système de types ne peut pas porter : - -> `Any.String().Numeric().StartingWith("ORD-")` entre en conflit parce que les lettres du préfixe tombent hors de l'ensemble numérique, tandis qu'`Any.String().Numeric().StartingWith("123")` est valide ; le site d'appel et les types statiques sont identiques dans les deux cas. - -Seule la *valeur* de l'argument les distingue — c'est ce qui rend le cas value-dependent, et ce qui le place du côté analyseur de la frontière tracée par l'ADR. - -## Non conforme - -```csharp -Any.String().Numeric().StartingWith("ORD-") // 'O' n'est pas un chiffre -Any.String().LowerCase().StartingWith("ABC") // LowerCase interdit le 'A' majuscule -Any.String().WithLength(3).StartingWith("ORD-") // le préfixe demande 4 caractères -Any.String().WithMaxLength(6).StartingWith("SKU-").EndingWith("-EUR") // 8 caractères pour un plafond de 6 -``` - -## Conforme - -```csharp -Any.String().Numeric().StartingWith("123") -Any.String().WithChars("ORD-0123456789").StartingWith("ORD-") // élargir le pool pour inclure le préfixe -Any.String().WithLength(12).StartingWith("ORD-") -``` - -## Les deux vérifications, et ce que la bibliothèque fait réellement - -**Famille de caractères.** `Alpha()`, `Numeric()`, `AlphaNumeric()` et `WithChars(...)` valident **chaque caractère** de chaque fragment ancré. - -**Casse.** `LowerCase()` et `UpperCase()` ne sont *pas* des ensembles de caractères. Elles contraignent la casse des **lettres** d'un fragment et ne disent rien de ses autres caractères — ainsi `UpperCase().StartingWith("ORD-")` est légal (le `-` n'est pas une lettre) alors qu'`UpperCase().StartingWith("abc")` ne l'est pas. Cette distinction a été trouvée par la mise à l'épreuve sur le dépôt : la première version modélisait la casse comme un pool et signalait à tort une chaîne que la propre suite de la bibliothèque affirme légale. - -**Budget de longueur.** Les fragments sont juxtaposés et doivent tenir dans la longueur déclarée. Le budget est **ignoré** dès qu'un `OneOf(...)` est déclaré : un ensemble de valeurs terminal change ce à quoi les fragments sont confrontés — ils sont comparés aux valeurs de l'ensemble plutôt que juxtaposés — de sorte qu'`OneOf("aba").WithMaxLength(3).Containing("ab").Containing("ba")` est légal bien que les fragments totalisent 4 caractères. - -## Ce qui n'est pas signalé - -* Un fragment, un pool ou une longueur non constants. -* Une chaîne répartie sur plusieurs instructions ou variables. La chaîne doit être écrite comme une seule expression : suivre un générateur à travers une variable locale exigerait une analyse de flot, et une règle qui affirme qu'une chaîne est insatisfiable doit voir toutes les contraintes qu'elle porte. -* Un test négatif vérifiant un conflit, où la chaîne illégale constitue tout le corps d'une lambda passée en argument. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD016.en.md b/doc/handwritten/for-users/analyzers/JD016.en.md deleted file mode 100644 index 2f272673..00000000 --- a/doc/handwritten/for-users/analyzers/JD016.en.md +++ /dev/null @@ -1,48 +0,0 @@ -# JD016: CollectionConstraintsAdmitNoValue - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD016.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -The declared count constraints cannot all hold, or the chain asks for more **distinct** elements than its element generator can produce — the cardinality gate [ADR-0013](../../for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md) records. - -Both throw at declaration time, so this rule turns an arrange-time red into a build-time red rather than closing a silent green. That is still worth it: the chain usually sits in an arrange helper several call frames away from the test that dies on it, where the message names two constraints the reader must then go hunting for. - -## Noncompliant - -```csharp -Any.ListOf(Any.Int32()).WithCount(0).NonEmpty() // fixed at 0, then required non-empty -Any.ListOf(Any.Int32()).WithMinCount(5).WithMaxCount(2) // no count satisfies both -Any.ListOf(Any.Int32()).WithMaxCount(2).Containing(1).Containing(2).Containing(3) // 3 items, 2 slots -Any.SetOf(Any.Boolean()).WithCount(5) // 5 distinct booleans do not exist -Any.ListOf(Any.Enum()).Distinct().WithCount(10) // more than the enum has members -``` - -## Compliant - -```csharp -Any.ListOf(Any.Int32()).WithCountBetween(2, 10) -Any.SetOf(Any.Boolean()).WithCount(2) -Any.ListOf(Any.Boolean()).WithCount(10) // no Distinct: repeats are fine -``` - -## Which domains it can prove - -Only the ones the compiler settles: `Any.Boolean()` (2), `Any.Enum()` (the declared member count), and a `OneOf`/`ElementOf` pool of constants (its distinct count). Anything else is unprovable and reported as nothing — **an unprovable domain must never be treated as a small one**. - -`AllowingCombinations()` is a case in point. It *widens* an enum's universe to the OR-closure of its declared members — eight values for four flags — so counting declared members there would condemn a legal chain. The rule stands down instead of computing the closure: a deliberate false negative, found by dogfooding against `AnyEnumCombinationTests`, which asserts `WithCount(8)` succeeds. - -## What it does not flag - -* A count or element generator that is not a compile-time constant. -* A chain split across statements — it must be one expression. -* A conflict-asserting negative test, where the illegal chain is the whole body of a lambda argument. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD016.fr.md b/doc/handwritten/for-users/analyzers/JD016.fr.md deleted file mode 100644 index f532df3a..00000000 --- a/doc/handwritten/for-users/analyzers/JD016.fr.md +++ /dev/null @@ -1,48 +0,0 @@ -# JD016 : CollectionConstraintsAdmitNoValue - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD016.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -Les contraintes de cardinal déclarées ne peuvent pas toutes tenir, ou la chaîne réclame plus d'éléments **distincts** que son générateur d'éléments ne peut en produire — la barrière de cardinalité que consigne l'[ADR-0013](../../for-maintainers/adr/0013-gate-distinct-collections-by-cardinality-else-bounded-draw.md). - -Les deux lèvent à la déclaration : cette règle transforme donc un rouge d'arrangement en rouge de compilation, plutôt que de fermer un vert silencieux. Cela vaut tout de même la peine : la chaîne se trouve d'ordinaire dans un utilitaire d'arrangement à plusieurs cadres d'appel du test qui meurt dessus, où le message nomme deux contraintes que le lecteur doit ensuite aller chercher. - -## Non conforme - -```csharp -Any.ListOf(Any.Int32()).WithCount(0).NonEmpty() // fixé à 0, puis exigé non vide -Any.ListOf(Any.Int32()).WithMinCount(5).WithMaxCount(2) // aucun cardinal ne satisfait les deux -Any.ListOf(Any.Int32()).WithMaxCount(2).Containing(1).Containing(2).Containing(3) // 3 éléments, 2 places -Any.SetOf(Any.Boolean()).WithCount(5) // 5 booléens distincts n'existent pas -Any.ListOf(Any.Enum()).Distinct().WithCount(10) // plus que le nombre de membres de l'enum -``` - -## Conforme - -```csharp -Any.ListOf(Any.Int32()).WithCountBetween(2, 10) -Any.SetOf(Any.Boolean()).WithCount(2) -Any.ListOf(Any.Boolean()).WithCount(10) // sans Distinct : les répétitions sont normales -``` - -## Les domaines qu'elle sait prouver - -Uniquement ceux que le compilateur tranche : `Any.Boolean()` (2), `Any.Enum()` (le nombre de membres déclarés), et un ensemble `OneOf`/`ElementOf` de constantes (son nombre de valeurs distinctes). Tout le reste est improuvable et n'est pas signalé — **un domaine improuvable ne doit jamais être traité comme un petit domaine**. - -`AllowingCombinations()` en est l'illustration. Elle *élargit* l'univers d'un enum à la clôture par OU de ses membres déclarés — huit valeurs pour quatre drapeaux — de sorte que compter les membres déclarés condamnerait une chaîne légale. La règle renonce plutôt que de calculer la clôture : un faux négatif délibéré, découvert par mise à l'épreuve contre `AnyEnumCombinationTests`, qui affirme que `WithCount(8)` réussit. - -## Ce qui n'est pas signalé - -* Un cardinal ou un générateur d'éléments qui n'est pas une constante de compilation. -* Une chaîne répartie sur plusieurs instructions — elle doit être une seule expression. -* Un test négatif vérifiant un conflit, où la chaîne illégale constitue tout le corps d'une lambda passée en argument. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD017.en.md b/doc/handwritten/for-users/analyzers/JD017.en.md deleted file mode 100644 index 0f5cbf6d..00000000 --- a/doc/handwritten/for-users/analyzers/JD017.en.md +++ /dev/null @@ -1,45 +0,0 @@ -# JD017: EnumUniverseViolation - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD017.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -`Any.Enum()` draws uniformly across `TEnum`'s **declared members**, and never an undeclared numeric value. That is deliberate — and surprising in one specific place: on a `[Flags]` enum, writing a combination in `OneOf` is the natural thing to do, and the generator refuses it unless `AllowingCombinations()` is declared. - -An exclusion that removes every declared member is the same category error from the other side: the universe is emptied rather than narrowed. - -## Noncompliant - -```csharp -Any.Enum().OneOf(Permissions.Read | Permissions.Write) // a combination is not a declared member -Any.Enum().OneOf((Day)99) // not a declared member at all -Any.Enum().Except(Day.Mon, Day.Tue) // nothing remains to draw -``` - -## Compliant - -```csharp -Any.Enum().AllowingCombinations().OneOf(Permissions.Read | Permissions.Write) -Any.Enum().OneOf(Permissions.Read, Permissions.Write) -Any.Enum().Except(Day.Mon) -``` - -## Why it is separate from the other constraint rules - -Its domain is **metadata** — the declared members — rather than interval arithmetic, and its mistake has its own teachable model: a value outside the declared set is not a narrowing that happens to be empty, it is a category error. The `[Flags]` case even gets its own hint in the message, because the fix (`AllowingCombinations()`) is not something a reader would guess from "not a declared member". - -## What it does not flag - -* Any constraint once `AllowingCombinations()` is declared. That widens the universe to the OR-closure of the declared members, which no longer matches a declared value one for one, so the rule stands down rather than approximate it. -* A generic helper whose type argument is an unsubstituted type parameter — there is no enum to reason about, and the rule bails rather than guess. -* A non-constant value, or a partial exclusion that leaves at least one member. -* A conflict-asserting negative test. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD017.fr.md b/doc/handwritten/for-users/analyzers/JD017.fr.md deleted file mode 100644 index 99164335..00000000 --- a/doc/handwritten/for-users/analyzers/JD017.fr.md +++ /dev/null @@ -1,45 +0,0 @@ -# JD017 : EnumUniverseViolation - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD017.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -`Any.Enum()` tire uniformément parmi les **membres déclarés** de `TEnum`, et jamais une valeur numérique non déclarée. C'est délibéré — et surprenant à un endroit précis : sur un enum `[Flags]`, écrire une combinaison dans `OneOf` est le geste naturel, et le générateur la refuse tant qu'`AllowingCombinations()` n'est pas déclaré. - -Une exclusion qui retire tous les membres déclarés est la même erreur de catégorie vue de l'autre côté : l'univers est vidé plutôt que rétréci. - -## Non conforme - -```csharp -Any.Enum().OneOf(Permissions.Read | Permissions.Write) // une combinaison n'est pas un membre déclaré -Any.Enum().OneOf((Day)99) // pas un membre déclaré du tout -Any.Enum().Except(Day.Mon, Day.Tue) // il ne reste rien à tirer -``` - -## Conforme - -```csharp -Any.Enum().AllowingCombinations().OneOf(Permissions.Read | Permissions.Write) -Any.Enum().OneOf(Permissions.Read, Permissions.Write) -Any.Enum().Except(Day.Mon) -``` - -## Pourquoi elle est séparée des autres règles de contraintes - -Son domaine relève des **métadonnées** — les membres déclarés — et non de l'arithmétique d'intervalles, et sa faute a son propre modèle à enseigner : une valeur hors de l'ensemble déclaré n'est pas un rétrécissement qui se trouve être vide, c'est une erreur de catégorie. Le cas `[Flags]` reçoit même son propre indice dans le message, car la correction (`AllowingCombinations()`) n'est pas quelque chose qu'un lecteur devinerait à partir de « pas un membre déclaré ». - -## Ce qui n'est pas signalé - -* Toute contrainte dès lors qu'`AllowingCombinations()` est déclaré. Cela élargit l'univers à la clôture par OU des membres déclarés, qui ne correspond plus un à un à une valeur déclarée : la règle renonce plutôt que d'approximer. -* Un utilitaire générique dont l'argument de type est un paramètre de type non substitué — il n'y a aucun enum sur lequel raisonner, et la règle s'abstient plutôt que de deviner. -* Une valeur non constante, ou une exclusion partielle qui laisse au moins un membre. -* Un test négatif vérifiant un conflit. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD018.en.md b/doc/handwritten/for-users/analyzers/JD018.en.md deleted file mode 100644 index 74649e8d..00000000 --- a/doc/handwritten/for-users/analyzers/JD018.en.md +++ /dev/null @@ -1,43 +0,0 @@ -# JD018: NestedReproducibilityScope - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD018.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -Both mechanisms report a replay instruction, and nesting makes the outer one **false**. - -`Any.Reproducibly` takes its seed from `Guid.NewGuid().GetHashCode()` — not from the ambient source — so an inner scope ignores whatever the outer one pinned and draws afresh on every run. The outer runner, or `[Reproducible]`, still reports *its* seed. The failure therefore says "reproduce this run with seed N", and replaying with N reproduces nothing. - -That is a wrong instruction rather than a wrong result, which is what makes it worth reporting: the reader trusts it and loses time before doubting it. - -## Noncompliant - -```csharp -[Fact, Reproducible] -public void It_is_accepted() { - Any.Reproducibly(() => { /* ... */ }); // JD018: the attribute's seed governs nothing in here -} -``` - -## Compliant - -```csharp -[Fact, Reproducible] -public void It_is_accepted() { - /* ... */ // the attribute already pins the whole test -} -``` - -## What it does not flag - -* The **seeded** overload — `Any.Reproducibly(1234, ...)`. Pinning a chosen seed inside is deliberate, and the reader who wrote it knows which seed governs what. -* A lone runner in a test that carries no `[Reproducible]`. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD018.fr.md b/doc/handwritten/for-users/analyzers/JD018.fr.md deleted file mode 100644 index 328fb488..00000000 --- a/doc/handwritten/for-users/analyzers/JD018.fr.md +++ /dev/null @@ -1,43 +0,0 @@ -# JD018 : NestedReproducibilityScope - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD018.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -Les deux mécanismes rapportent une instruction de rejeu, et l'imbrication rend celle de l'extérieur **fausse**. - -`Any.Reproducibly` prend sa graine de `Guid.NewGuid().GetHashCode()` — et non de la source ambiante — de sorte qu'une portée interne ignore ce que l'externe a épinglé et tire à neuf à chaque exécution. Le lanceur externe, ou `[Reproducible]`, rapporte pourtant *sa* graine. L'échec dit donc « rejouez avec la graine N », et rejouer avec N ne reproduit rien. - -C'est une instruction fausse plutôt qu'un résultat faux, et c'est ce qui justifie de la signaler : le lecteur lui fait confiance et perd du temps avant d'en douter. - -## Non conforme - -```csharp -[Fact, Reproducible] -public void It_is_accepted() { - Any.Reproducibly(() => { /* ... */ }); // JD018 : la graine de l'attribut ne gouverne rien ici -} -``` - -## Conforme - -```csharp -[Fact, Reproducible] -public void It_is_accepted() { - /* ... */ // l'attribut épingle déjà tout le test -} -``` - -## Ce qui n'est pas signalé - -* La surcharge **avec graine** — `Any.Reproducibly(1234, ...)`. Épingler une graine choisie à l'intérieur est délibéré, et celui qui l'écrit sait quelle graine gouverne quoi. -* Un lanceur isolé dans un test qui ne porte aucun `[Reproducible]`. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD019.en.md b/doc/handwritten/for-users/analyzers/JD019.en.md deleted file mode 100644 index 3c7b6d56..00000000 --- a/doc/handwritten/for-users/analyzers/JD019.en.md +++ /dev/null @@ -1,50 +0,0 @@ -# JD019: CommittedReplaySeed - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD019.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🔵 Info | -| **Enabled by default** | **No — opt-in** | - -The seeded overloads exist to **replay** a run a failure reported. That is correct while you are reproducing and wrong the moment it is committed: the test then draws the same values for ever, and stops surfacing the dependency on one particular value that arbitrary-by-default exists to reveal. - -It is the `fit`/`.only` of this library — right in the moment, wrong in the repository. - -## Enabling it - -```ini -dotnet_diagnostic.JD019.severity = warning -``` - -## Noncompliant - -```csharp -[Fact, Reproducible(Seed = 1234)] // JD019 -Any.Reproducibly(1234, () => { ... }); // JD019 -Any.WithSeed(1234) // JD019 -``` - -## Compliant - -```csharp -[Fact, Reproducible] -Any.Reproducibly(() => { ... }); -``` - -## Why it ships opt-in - -Because this repository's own maintainer guide instructs the opposite for a whole class of tests: *"Pin a seed for anything statistical."* A rule enabled by default would fight documented practice. - -The measurement is blunt. Enabled across `JustDummies.UnitTests`, it reports **238 sites** — nearly all of them legitimate, deliberate pins on distribution and coverage tests. It earns its keep as a **pre-release sweep** you turn on deliberately, not as a standing check. - -## What it does not flag - -* A seed read from configuration, computed, or passed as a parameter — only a compile-time constant is reported. -* The seedless overloads, which are the arbitrary-by-default path. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD019.fr.md b/doc/handwritten/for-users/analyzers/JD019.fr.md deleted file mode 100644 index b068b5ab..00000000 --- a/doc/handwritten/for-users/analyzers/JD019.fr.md +++ /dev/null @@ -1,50 +0,0 @@ -# JD019 : CommittedReplaySeed - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD019.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🔵 Info | -| **Activée par défaut** | **Non — opt-in** | - -Les surcharges avec graine existent pour **rejouer** une exécution qu'un échec a rapportée. C'est correct pendant qu'on reproduit, et faux dès que c'est committé : le test tire alors les mêmes valeurs pour toujours, et cesse de révéler la dépendance à une valeur particulière que l'« arbitraire par défaut » existe pour mettre au jour. - -C'est le `fit`/`.only` de cette bibliothèque — juste sur le moment, faux dans le dépôt. - -## Comment l'activer - -```ini -dotnet_diagnostic.JD019.severity = warning -``` - -## Non conforme - -```csharp -[Fact, Reproducible(Seed = 1234)] // JD019 -Any.Reproducibly(1234, () => { ... }); // JD019 -Any.WithSeed(1234) // JD019 -``` - -## Conforme - -```csharp -[Fact, Reproducible] -Any.Reproducibly(() => { ... }); -``` - -## Pourquoi elle est livrée en opt-in - -Parce que le guide de maintenance de ce dépôt prescrit l'inverse pour toute une classe de tests : « *Pin a seed for anything statistical.* » Une règle activée par défaut combattrait une pratique documentée. - -La mesure est sans appel. Activée sur `JustDummies.UnitTests`, elle signale **238 sites** — presque tous légitimes, des épinglages délibérés sur des tests de distribution et de couverture. Elle gagne sa place comme **balayage d'avant-livraison** qu'on active exprès, pas comme vérification permanente. - -## Ce qui n'est pas signalé - -* Une graine lue depuis une configuration, calculée, ou passée en paramètre — seule une constante de compilation est signalée. -* Les surcharges sans graine, qui sont le chemin arbitraire par défaut. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD020.en.md b/doc/handwritten/for-users/analyzers/JD020.en.md deleted file mode 100644 index 3eabc590..00000000 --- a/doc/handwritten/for-users/analyzers/JD020.en.md +++ /dev/null @@ -1,43 +0,0 @@ -# JD020: SharedStaticAnyContext - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD020.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🔵 Info | -| **Enabled by default** | Yes | - -A static `AnyContext` looks maximally deterministic — a literal seed, right there in the source — and is not. `AnyContext`'s own documentation states the hazard: - -> A context is safe to draw from concurrently, but sharing one across threads costs the **replay** rather than the values: interleaved draws make neither the sequence nor the multiset stable across runs. Keep a context to one thread at a time. - -So in a suite that runs its classes in parallel, each test gets whatever the interleaving happened to hand it. The seed is pinned and the run still does not replay. - -## Noncompliant - -```csharp -private static readonly AnyContext Context = Any.WithSeed(1234); // JD020 -``` - -## Compliant - -```csharp -// One context per unit of work… -AnyContext context = Any.WithSeed(1234); - -// …or the ambient scope, which flows with the execution context: -using IDisposable scope = Any.UseSeed(1234); -``` - -## What it does not flag - -* An **instance** context, which is per-test by construction. -* A static field holding a **generator**: the random source is resolved at `Generate()` time, never at construction, so a shared recipe is safe and idiomatic. - -Info rather than Warning: a single-threaded consumer — a console sample, a benchmark, a `[Collection]`-serialised class — shares one harmlessly, and the rule cannot see the suite's parallelism configuration. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD020.fr.md b/doc/handwritten/for-users/analyzers/JD020.fr.md deleted file mode 100644 index 5406621a..00000000 --- a/doc/handwritten/for-users/analyzers/JD020.fr.md +++ /dev/null @@ -1,43 +0,0 @@ -# JD020 : SharedStaticAnyContext - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD020.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🔵 Info | -| **Activée par défaut** | Oui | - -Un `AnyContext` statique paraît maximalement déterministe — une graine littérale, là, dans le source — et ne l'est pas. La documentation d'`AnyContext` énonce elle-même le risque : - -> Un contexte peut être tiré concurremment sans danger, mais en partager un entre threads coûte le **rejeu** plutôt que les valeurs : des tirages entrelacés ne rendent stables ni la séquence ni le multiensemble d'une exécution à l'autre. Gardez un contexte à un seul thread à la fois. - -Dans une suite qui exécute ses classes en parallèle, chaque test reçoit donc ce que l'entrelacement lui a donné. La graine est épinglée et l'exécution ne rejoue toujours pas. - -## Non conforme - -```csharp -private static readonly AnyContext Context = Any.WithSeed(1234); // JD020 -``` - -## Conforme - -```csharp -// Un contexte par unité de travail… -AnyContext context = Any.WithSeed(1234); - -// …ou la portée ambiante, qui suit le contexte d'exécution : -using IDisposable scope = Any.UseSeed(1234); -``` - -## Ce qui n'est pas signalé - -* Un contexte **d'instance**, qui est par test par construction. -* Un champ statique portant un **générateur** : la source aléatoire est résolue au moment de `Generate()`, jamais à la construction, donc une recette partagée est sûre et idiomatique. - -Info plutôt qu'avertissement : un consommateur monothread — un exemple console, un benchmark, une classe sérialisée par `[Collection]` — en partage un sans dommage, et la règle ne voit pas la configuration de parallélisme de la suite. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD021.en.md b/doc/handwritten/for-users/analyzers/JD021.en.md deleted file mode 100644 index 23daf3ba..00000000 --- a/doc/handwritten/for-users/analyzers/JD021.en.md +++ /dev/null @@ -1,41 +0,0 @@ -# JD021: BlankReplaySnippet - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD021.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -`Any.UseSeed(int, string)` supplies the **replay snippet** — the code a reader copies to replay the run — that generation-failure guidance quotes verbatim. It rejects a blank one at run time. - -What makes the compile-time check worth having is *where* that throw lands. This overload exists for a test-framework adapter, which opens the scope from a hook that runs before every test. A blank snippet therefore fails the whole suite as an infrastructure error, not one assertion — a disproportionately expensive way to learn about a typo the compiler can already see. - -## Noncompliant - -```csharp -using IDisposable scope = Any.UseSeed(1234, ""); // JD021 -using IDisposable scope = Any.UseSeed(1234, " "); // JD021 -``` - -## Compliant - -```csharp -using IDisposable scope = Any.UseSeed(1234, "[Reproducible(Seed = 1234)]"); - -// Or drop the argument: the default snippet names Any.Reproducibly(seed, ...). -using IDisposable scope = Any.UseSeed(1234); -``` - -Pass the **code** itself — an attribute with its seed argument, a runner setting — not a sentence about it. It is quoted verbatim into the failure message. - -## What it does not flag - -* A non-constant snippet. -* A test asserting the rejection, where the call is the whole body of a lambda argument. `JustDummies.PropertyTests` asserts exactly this guard. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD021.fr.md b/doc/handwritten/for-users/analyzers/JD021.fr.md deleted file mode 100644 index 7e0de51f..00000000 --- a/doc/handwritten/for-users/analyzers/JD021.fr.md +++ /dev/null @@ -1,41 +0,0 @@ -# JD021 : BlankReplaySnippet - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD021.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -`Any.UseSeed(int, string)` fournit le **snippet de rejeu** — le code qu'un lecteur copie pour rejouer l'exécution — que les conseils d'échec de génération citent tels quels. Un snippet vide est rejeté à l'exécution. - -Ce qui justifie la vérification à la compilation, c'est *où* cette levée atterrit. Cette surcharge existe pour un adaptateur de framework de test, qui ouvre la portée depuis un hook s'exécutant avant chaque test. Un snippet vide fait donc échouer toute la suite comme erreur d'infrastructure, et non une seule assertion — une façon disproportionnellement coûteuse d'apprendre une faute de frappe que le compilateur voit déjà. - -## Non conforme - -```csharp -using IDisposable scope = Any.UseSeed(1234, ""); // JD021 -using IDisposable scope = Any.UseSeed(1234, " "); // JD021 -``` - -## Conforme - -```csharp -using IDisposable scope = Any.UseSeed(1234, "[Reproducible(Seed = 1234)]"); - -// Ou supprimez l'argument : le snippet par défaut nomme Any.Reproducibly(seed, ...). -using IDisposable scope = Any.UseSeed(1234); -``` - -Passez le **code** lui-même — un attribut avec son argument de graine, un réglage de lanceur — et non une phrase à son sujet. Il est cité tel quel dans le message d'échec. - -## Ce qui n'est pas signalé - -* Un snippet non constant. -* Un test qui vérifie le rejet, où l'appel constitue tout le corps d'une lambda passée en argument. `JustDummies.PropertyTests` vérifie exactement cette garde. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD022.en.md b/doc/handwritten/for-users/analyzers/JD022.en.md deleted file mode 100644 index 12ef652f..00000000 --- a/doc/handwritten/for-users/analyzers/JD022.en.md +++ /dev/null @@ -1,49 +0,0 @@ -# JD022: ParallelDrawWithoutPerItemSeed - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD022.fr.md) - -| | | -|---|---| -| **Category** | Reproducibility (`JustDummies.Reproducibility`) | -| **Severity** | 🔵 Info | -| **Enabled by default** | Yes | - -The ambient seed scope flows with the execution context, so a scope opened *around* a parallel loop reaches every worker — and their draws interleave. Neither the sequence nor the multiset is stable across runs, so the run replays nothing even though a seed was pinned. - -This is the shape the library's own documentation names, and the fix it prescribes: a scope opened **inside** the loop body gives each unit of work its own sequence, and the whole run replays. - -## Noncompliant - -```csharp -Parallel.For(0, 64, index => { - sut.Handle(Any.String().NonEmpty().Generate()); // JD022: one shared sequence, interleaved -}); -``` - -## Compliant - -```csharp -const int runSeed = 20240501; // recorded by hand: keep it to replay, change it to explore - -Parallel.For(0, 64, index => { - // a distinct, deterministic sub-seed per work item, floor-safe on netstandard2.0 - using (Any.UseSeed(unchecked(runSeed * 397 ^ index))) { - sut.Handle(Any.String().NonEmpty().Generate()); - } -}); -``` - -## No code fix - -The repair needs a run seed the developer must **choose and record**, plus a per-item derivation from the loop variable. An analyzer cannot invent a seed someone intends to keep — generating one would produce exactly the committed-replay-seed problem [JD019](JD019.en.md) describes. - -## What it does not flag - -* A body that already opens an `Any.UseSeed` scope. -* A draw from an isolated `Any.WithSeed(...)` context, or a generator reached through a local rather than written inline from `Any`. -* A per-item scope opened in a helper the body calls — a one-hop miss the rule deliberately accepts rather than attempt interprocedural analysis. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD022.fr.md b/doc/handwritten/for-users/analyzers/JD022.fr.md deleted file mode 100644 index c8a24ade..00000000 --- a/doc/handwritten/for-users/analyzers/JD022.fr.md +++ /dev/null @@ -1,49 +0,0 @@ -# JD022 : ParallelDrawWithoutPerItemSeed - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD022.en.md) - -| | | -|---|---| -| **Catégorie** | Reproductibilité (`JustDummies.Reproducibility`) | -| **Sévérité** | 🔵 Info | -| **Activée par défaut** | Oui | - -La portée de graine ambiante suit le contexte d'exécution : une portée ouverte *autour* d'une boucle parallèle atteint donc chaque worker — et leurs tirages s'entrelacent. Ni la séquence ni le multiensemble ne sont stables d'une exécution à l'autre : l'exécution ne rejoue rien alors qu'une graine a été épinglée. - -C'est la forme que la documentation de la bibliothèque nomme elle-même, avec le remède qu'elle prescrit : une portée ouverte **dans** le corps de boucle donne à chaque unité de travail sa propre séquence, et toute l'exécution rejoue. - -## Non conforme - -```csharp -Parallel.For(0, 64, index => { - sut.Handle(Any.String().NonEmpty().Generate()); // JD022 : une séquence partagée, entrelacée -}); -``` - -## Conforme - -```csharp -const int runSeed = 20240501; // noté à la main : gardez-le pour rejouer, changez-le pour explorer - -Parallel.For(0, 64, index => { - // une sous-graine distincte et déterministe par unité de travail, compatible netstandard2.0 - using (Any.UseSeed(unchecked(runSeed * 397 ^ index))) { - sut.Handle(Any.String().NonEmpty().Generate()); - } -}); -``` - -## Pas de correction automatique - -La réparation exige une graine d'exécution que le développeur doit **choisir et consigner**, plus une dérivation par item à partir de la variable de boucle. Un analyseur ne peut pas inventer une graine que quelqu'un compte conserver — en générer une produirait exactement le problème de graine committée que décrit [JD019](JD019.fr.md). - -## Ce qui n'est pas signalé - -* Un corps qui ouvre déjà une portée `Any.UseSeed`. -* Un tirage depuis un contexte isolé `Any.WithSeed(...)`, ou un générateur atteint par une variable locale plutôt qu'écrit en ligne depuis `Any`. -* Une portée par item ouverte dans un utilitaire appelé par le corps — un manque à un saut que la règle accepte délibérément plutôt que de tenter une analyse interprocédurale. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD023.en.md b/doc/handwritten/for-users/analyzers/JD023.en.md deleted file mode 100644 index ea3fa185..00000000 --- a/doc/handwritten/for-users/analyzers/JD023.en.md +++ /dev/null @@ -1,49 +0,0 @@ -# JD023: ScalarChainAdmitsNoValue - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD023.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🟠 Warning | -| **Enabled by default** | Yes | - -The constant constraints narrow the integer domain to nothing, so the chain throws a `ConflictingAnyConstraintException` the moment the arrange line runs. - -The library computes this with one emptiness test over bounds, lattice and allow-list. This rule runs the same test over the constants written at the call site — and stays silent for every argument it cannot fold. - -## Noncompliant - -```csharp -Any.Int32().Between(1, 10).MultipleOf(20) // no multiple of 20 in [1, 10] -Any.Int32().GreaterThan(10).LessThan(3) // empty interval -Any.Int32().Positive().Negative() // empty by construction -Any.Int32().Zero().NonZero() // the only value left is then forbidden -Any.Int32().OneOf(5).Except(5) // the allow-list is emptied -``` - -## Compliant - -```csharp -Any.Int32().Between(1, 10).MultipleOf(5) -Any.Int32().GreaterThan(-100).LessThan(100) -Any.Int32().OneOf(1, 2, 3).Except(2) -``` - -## Scope, and one boundary worth knowing - -**Integer generators only** — `Int32`, `Int16`, `Int64`, `Byte`, `SByte`, `UInt16`, `UInt32`, `UInt64`. The model is integer arithmetic, and a floating-point or decimal domain does not behave like one. - -Bounds run to the **representable extremes**. `Any.Int64().LessThanOrEqualTo(long.MinValue)` is a legal chain that yields exactly one value, and is not reported; only a bound asking for something genuinely beyond the range — `GreaterThan(long.MaxValue)` — empties the domain. The first version of this rule got that wrong, using `-long.MaxValue` as its "unbounded" sentinel, which made `long.MinValue` unrepresentable and condemned a chain the library's own suite asserts is legal. - -## What it does not flag - -* A chain with any argument that does not fold to a constant. -* A chain split across statements — it must be one expression. -* A constraint the model does not track: the walk ends rather than guessing. -* A conflict-asserting negative test. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD023.fr.md b/doc/handwritten/for-users/analyzers/JD023.fr.md deleted file mode 100644 index d1946a8e..00000000 --- a/doc/handwritten/for-users/analyzers/JD023.fr.md +++ /dev/null @@ -1,49 +0,0 @@ -# JD023 : ScalarChainAdmitsNoValue - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD023.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🟠 Avertissement | -| **Activée par défaut** | Oui | - -Les contraintes constantes réduisent le domaine entier à rien : la chaîne lève une `ConflictingAnyConstraintException` dès l'exécution de la ligne d'arrangement. - -La bibliothèque calcule cela par un unique test de vacuité sur les bornes, le treillis et la liste d'autorisation. Cette règle exécute le même test sur les constantes écrites au site d'appel — et se tait pour tout argument qu'elle ne peut pas replier. - -## Non conforme - -```csharp -Any.Int32().Between(1, 10).MultipleOf(20) // aucun multiple de 20 dans [1, 10] -Any.Int32().GreaterThan(10).LessThan(3) // intervalle vide -Any.Int32().Positive().Negative() // vide par construction -Any.Int32().Zero().NonZero() // la seule valeur restante est ensuite interdite -Any.Int32().OneOf(5).Except(5) // la liste d'autorisation est vidée -``` - -## Conforme - -```csharp -Any.Int32().Between(1, 10).MultipleOf(5) -Any.Int32().GreaterThan(-100).LessThan(100) -Any.Int32().OneOf(1, 2, 3).Except(2) -``` - -## Portée, et une limite qui mérite d'être connue - -**Générateurs entiers uniquement** — `Int32`, `Int16`, `Int64`, `Byte`, `SByte`, `UInt16`, `UInt32`, `UInt64`. Le modèle est de l'arithmétique entière, et un domaine flottant ou décimal ne se comporte pas ainsi. - -Les bornes vont jusqu'aux **extrêmes représentables**. `Any.Int64().LessThanOrEqualTo(long.MinValue)` est une chaîne légale qui produit exactement une valeur, et n'est pas signalée ; seule une borne réclamant quelque chose de réellement hors plage — `GreaterThan(long.MaxValue)` — vide le domaine. La première version de cette règle s'est trompée là-dessus, en prenant `-long.MaxValue` comme sentinelle « non borné », ce qui rendait `long.MinValue` inexprimable et condamnait une chaîne que la propre suite de la bibliothèque affirme légale. - -## Ce qui n'est pas signalé - -* Une chaîne dont un argument ne se replie pas en constante. -* Une chaîne répartie sur plusieurs instructions — elle doit être une seule expression. -* Une contrainte que le modèle ne suit pas : la marche s'arrête plutôt que de deviner. -* Un test négatif vérifiant un conflit. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD024.en.md b/doc/handwritten/for-users/analyzers/JD024.en.md deleted file mode 100644 index 0fe2a143..00000000 --- a/doc/handwritten/for-users/analyzers/JD024.en.md +++ /dev/null @@ -1,44 +0,0 @@ -# JD024: ConstraintWithNoEffect - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD024.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🔵 Info | -| **Enabled by default** | Yes | - -The constraint is legal and **inert**: the domain it produces is the one that already existed. - -This is the only member of the constraint family the run time **never** reports. Every other contradiction throws eventually and loudly; an inert constraint leaves the test green while it exercises a domain the author did not write. - -The dangerous case is an exclusion of a sentinel the generator could never draw. It silently misses — and starts mattering the day someone widens the range, at which point the sentinel begins appearing and a distant test starts flaking. - -## Noncompliant - -```csharp -Any.Int32().Between(1, 10).Except(20) // JD024: 20 was never in [1, 10] -Any.Int32().Positive().GreaterThan(-5) // JD024: Positive() already requires ≥ 1 -``` - -## Compliant - -```csharp -Any.Int32().Between(1, 30).Except(20) // the exclusion now removes something -Any.Int32().Positive().GreaterThan(100) // the bound now narrows -``` - -## Info rather than Warning - -A defensive or documentary constraint is a real and reasonable style: a team writes `.Except(0)` on a range that already excludes 0 so the intent survives a future widening. The rule states the fact without insisting it is a defect. - -## What it does not flag - -* An exclusion that removes at least one reachable value. -* A bound that genuinely narrows the domain. -* Anything on a non-integer generator, or a chain with a non-constant argument. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD024.fr.md b/doc/handwritten/for-users/analyzers/JD024.fr.md deleted file mode 100644 index 5a1e7c7c..00000000 --- a/doc/handwritten/for-users/analyzers/JD024.fr.md +++ /dev/null @@ -1,44 +0,0 @@ -# JD024 : ConstraintWithNoEffect - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD024.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🔵 Info | -| **Activée par défaut** | Oui | - -La contrainte est légale et **inerte** : le domaine qu'elle produit est celui qui existait déjà. - -C'est le seul membre de la famille des contraintes que l'exécution ne signale **jamais**. Toutes les autres contradictions finissent par lever, bruyamment ; une contrainte inerte laisse le test au vert alors qu'il exerce un domaine que l'auteur n'a pas écrit. - -Le cas dangereux est l'exclusion d'une valeur sentinelle que le générateur n'aurait jamais pu tirer. Elle manque sa cible en silence — et se met à compter le jour où quelqu'un élargit la plage, moment où la sentinelle commence à apparaître et où un test lointain devient instable. - -## Non conforme - -```csharp -Any.Int32().Between(1, 10).Except(20) // JD024 : 20 n'a jamais été dans [1, 10] -Any.Int32().Positive().GreaterThan(-5) // JD024 : Positive() exige déjà ≥ 1 -``` - -## Conforme - -```csharp -Any.Int32().Between(1, 30).Except(20) // l'exclusion retire maintenant quelque chose -Any.Int32().Positive().GreaterThan(100) // la borne rétrécit maintenant -``` - -## Info plutôt qu'avertissement - -Une contrainte défensive ou documentaire est un style réel et raisonnable : une équipe écrit `.Except(0)` sur une plage qui exclut déjà 0 pour que l'intention survive à un élargissement futur. La règle énonce le fait sans prétendre qu'il s'agit d'un défaut. - -## Ce qui n'est pas signalé - -* Une exclusion qui retire au moins une valeur atteignable. -* Une borne qui rétrécit réellement le domaine. -* Quoi que ce soit sur un générateur non entier, ou une chaîne avec un argument non constant. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD025.en.md b/doc/handwritten/for-users/analyzers/JD025.en.md deleted file mode 100644 index 91cbf2ad..00000000 --- a/doc/handwritten/for-users/analyzers/JD025.en.md +++ /dev/null @@ -1,51 +0,0 @@ -# JD025: DuplicatePoolValue - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD025.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🟡 Warning | -| **Enabled by default** | Yes | - -The same value is listed twice in a pool. A pool is deduplicated under the default equality when the generator is built, so a value written twice contributes exactly once. - -The reading this rule exists to refuse is **weighting**. Listing a value twice looks like "draw this one more often", and the library declines to weight a pool on purpose — so the duplicate does nothing at all, and the pool is one value smaller than it reads. - -## Noncompliant - -```csharp -Any.OneOf(1, 2, 1) // JD025: the pool holds two values, not three -Any.OneOf("EUR", "USD", "EUR") // JD025 -Any.Int32().OneOf(3, 3) // JD025 -``` - -## Compliant - -```csharp -Any.OneOf(1, 2) // say what the pool is -Any.OneOf("EUR", "USD") -``` - -## Where the gap surfaces - -Nothing fails here, which is the problem: the consequence lands somewhere else entirely. A distinct collection over the pool gates against the **real** distinct count, and reports a number the author cannot find in their source: - -```csharp -Any.SetOf(Any.OneOf(1, 2, 1)).WithCount(3) -// ConflictingAnyConstraintException: 3 elements required to be distinct -// exceed the 2 distinct value(s) the element generator can produce. -``` - -Three values are written on the line; the message says two. This rule points at the line that is actually wrong. - -## What it does not flag - -* A pool whose elements are not all compile-time constants — one unfoldable element and the pool stops being knowable, so the rule stands down rather than report a partial walk. -* A pool held in a variable or built by a query (`Any.ElementOf(orders)`); only the values written at the call site are visible. -* Values that merely look alike: `Any.OneOf("a", "A")` is a pool of two. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD025.fr.md b/doc/handwritten/for-users/analyzers/JD025.fr.md deleted file mode 100644 index f25f088c..00000000 --- a/doc/handwritten/for-users/analyzers/JD025.fr.md +++ /dev/null @@ -1,51 +0,0 @@ -# JD025 : DuplicatePoolValue - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD025.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🟡 Avertissement | -| **Activée par défaut** | Oui | - -La même valeur figure deux fois dans un réservoir. Un réservoir est dédoublonné sous l'égalité par défaut à la construction du générateur : une valeur écrite deux fois n'y contribue qu'une seule fois. - -La lecture que cette règle existe pour refuser est la **pondération**. Lister une valeur deux fois ressemble à « tirer celle-ci plus souvent », et la bibliothèque refuse délibérément de pondérer un réservoir — le doublon ne fait donc rien du tout, et le réservoir est plus petit d'une valeur que ce qu'il paraît. - -## Non conforme - -```csharp -Any.OneOf(1, 2, 1) // JD025 : le réservoir contient deux valeurs, pas trois -Any.OneOf("EUR", "USD", "EUR") // JD025 -Any.Int32().OneOf(3, 3) // JD025 -``` - -## Conforme - -```csharp -Any.OneOf(1, 2) // dire ce qu'est le réservoir -Any.OneOf("EUR", "USD") -``` - -## Où l'écart se manifeste - -Rien n'échoue ici, et c'est bien le problème : la conséquence tombe complètement ailleurs. Une collection distincte au-dessus du réservoir vérifie le **vrai** nombre de valeurs distinctes, et annonce un nombre que l'auteur ne retrouve pas dans son source : - -```csharp -Any.SetOf(Any.OneOf(1, 2, 1)).WithCount(3) -// ConflictingAnyConstraintException : 3 elements required to be distinct -// exceed the 2 distinct value(s) the element generator can produce. -``` - -Trois valeurs sont écrites sur la ligne ; le message en annonce deux. Cette règle désigne la ligne réellement fautive. - -## Ce qui n'est pas signalé - -* Un réservoir dont les éléments ne sont pas tous des constantes de compilation — un seul élément non repliable et le réservoir cesse d'être connaissable, donc la règle se retire plutôt que de signaler un parcours partiel. -* Un réservoir tenu dans une variable ou construit par une requête (`Any.ElementOf(orders)`) ; seules les valeurs écrites sur le site d'appel sont visibles. -* Des valeurs qui se ressemblent seulement : `Any.OneOf("a", "A")` est un réservoir de deux. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD026.en.md b/doc/handwritten/for-users/analyzers/JD026.en.md deleted file mode 100644 index a20eb4f5..00000000 --- a/doc/handwritten/for-users/analyzers/JD026.en.md +++ /dev/null @@ -1,50 +0,0 @@ -# JD026: EmptyRelativeUri - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD026.fr.md) - -| | | -|---|---| -| **Category** | Constraints (`JustDummies.Constraints`) | -| **Severity** | 🟡 Warning | -| **Enabled by default** | Yes | - -The chain describes the **empty reference**, which no URI can be: a relative URI with exactly zero path segments, no query, no fragment and no root renders as the empty string. - -## Noncompliant - -```csharp -Any.Uri().Relative().WithPathSegments(0) // JD026: nothing left to render -``` - -## Compliant - -```csharp -Any.Uri().Relative().WithPathSegments(0).WithQuery() // "?page=2" -Any.Uri().Relative().WithPathSegments(0).WithFragment() // "#top" -Any.Uri().Relative().Rooted().WithPathSegments(0) // "/" -Any.Uri().Relative().WithPathSegments(1) // "orders" -``` - -Any one of the four is enough: the reference only needs *something* to render. - -## Why this one is worth a rule - -The library already reports it — but only at `Generate()`. This is the one member of the constraint family whose failure lands at **act** time rather than at the arrange line, because emptiness is only settled once the components have been drawn: - -``` -AnyGenerationException: A relative URI with exactly 0 path segments and no query, -fragment or root is empty, which is not a valid URI reference. Add a query, a -fragment, Rooted(), or a positive segment count. -``` - -The message is clear; the stack is not. It points at the code under test, several frames from the declaration that is wrong — and, when the chain lives in a shared fixture, in a test that never mentions URIs. Moving the report to build time is the whole value of the rule. - -## What it does not flag - -* A non-relative family. `Web()`, `WebSocket()` and `Ftp()` carry an authority, so a path of zero segments still renders as `/` and the reference stays valid. -* A segment count that is not a compile-time constant. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD026.fr.md b/doc/handwritten/for-users/analyzers/JD026.fr.md deleted file mode 100644 index b8d627fa..00000000 --- a/doc/handwritten/for-users/analyzers/JD026.fr.md +++ /dev/null @@ -1,50 +0,0 @@ -# JD026 : EmptyRelativeUri - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD026.en.md) - -| | | -|---|---| -| **Catégorie** | Contraintes (`JustDummies.Constraints`) | -| **Sévérité** | 🟡 Avertissement | -| **Activée par défaut** | Oui | - -La chaîne décrit la **référence vide**, ce qu'aucune URI ne peut être : une URI relative à exactement zéro segment de chemin, sans requête, sans fragment et sans racine se rend comme la chaîne vide. - -## Non conforme - -```csharp -Any.Uri().Relative().WithPathSegments(0) // JD026 : plus rien à rendre -``` - -## Conforme - -```csharp -Any.Uri().Relative().WithPathSegments(0).WithQuery() // "?page=2" -Any.Uri().Relative().WithPathSegments(0).WithFragment() // "#top" -Any.Uri().Relative().Rooted().WithPathSegments(0) // "/" -Any.Uri().Relative().WithPathSegments(1) // "orders" -``` - -N'importe laquelle des quatre suffit : la référence a seulement besoin de *quelque chose* à rendre. - -## Pourquoi celle-ci mérite une règle - -La bibliothèque le signale déjà — mais seulement à `Generate()`. C'est le seul membre de la famille des contraintes dont l'échec atterrit au moment de l'**act** plutôt que sur la ligne d'arrange, parce que la vacuité ne se décide qu'une fois les composants tirés : - -``` -AnyGenerationException: A relative URI with exactly 0 path segments and no query, -fragment or root is empty, which is not a valid URI reference. Add a query, a -fragment, Rooted(), or a positive segment count. -``` - -Le message est clair ; la pile ne l'est pas. Elle désigne le code sous test, à plusieurs cadres de la déclaration fautive — et, quand la chaîne vit dans une fixture partagée, dans un test qui ne parle jamais d'URI. Ramener le signalement au moment de la compilation est tout l'intérêt de la règle. - -## Ce qui n'est pas signalé - -* Une famille non relative. `Web()`, `WebSocket()` et `Ftp()` portent une autorité : un chemin de zéro segment se rend quand même en `/` et la référence reste valide. -* Un nombre de segments qui n'est pas une constante de compilation. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD027.en.md b/doc/handwritten/for-users/analyzers/JD027.en.md deleted file mode 100644 index 82e5d1b8..00000000 --- a/doc/handwritten/for-users/analyzers/JD027.en.md +++ /dev/null @@ -1,61 +0,0 @@ -# JD027: UnusedCombineOperand - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD027.fr.md) - -| | | -|---|---| -| **Category** | Composition (`JustDummies.Composition`) | -| **Severity** | 🟡 Warning | -| **Enabled by default** | Yes | - -The composer never reads the parameter one of the operands is bound to, so that operand is **drawn and thrown away**. - -`Combine` generates every part before calling the composer — constraints built, conflict checks run, value produced — and then drops it. Nothing fails: the composed value is well-formed, and simply does not carry the part the call site says it carries. - -## Noncompliant - -```csharp -IAny customer = Any.Combine( - Any.String().NonEmpty().WithMaxLength(50), - Any.String().StartingWith("ORD-").WithLength(12), - (name, reference) => new Customer(name)); // JD027: 'reference' never reaches the Customer -``` - -## Compliant - -```csharp -IAny customer = Any.Combine( - Any.String().NonEmpty().WithMaxLength(50), - Any.String().StartingWith("ORD-").WithLength(12), - (name, reference) => new Customer(name, OrderReference.Create(reference))); -``` - -Or drop the operand entirely, if it is genuinely not part of the value: - -```csharp -IAny customer = Any.String().NonEmpty().WithMaxLength(50).As(name => new Customer(name)); -``` - -## Saying the draw is deliberate - -Name the parameter `_`, the same way C# spells "I know, and I mean it" anywhere else: - -```csharp -Any.Combine(first, second, (value, _) => new Wrapper(value)) // no diagnostic -``` - -## Why it happens - -The two shapes seen most often are a constructor argument forgotten during a refactor, and a composer whose parameters no longer line up with its operands after one was inserted in the middle. Both leave a generator whose carefully written constraints have no effect on anything the test observes. - -## What it does not flag - -* A parameter named `_`. -* A composer passed as a method group — its body is not necessarily this compilation's to read, so which operands it uses is not knowable. -* A composer whose whole body is a `throw`. It reads no parameter by construction, and is exercising the failure path `Combine` wraps rather than ignoring an operand. -* A parameter read only inside a nested lambda: that still counts as read. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD027.fr.md b/doc/handwritten/for-users/analyzers/JD027.fr.md deleted file mode 100644 index 3b188e73..00000000 --- a/doc/handwritten/for-users/analyzers/JD027.fr.md +++ /dev/null @@ -1,61 +0,0 @@ -# JD027 : UnusedCombineOperand - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD027.en.md) - -| | | -|---|---| -| **Catégorie** | Composition (`JustDummies.Composition`) | -| **Sévérité** | 🟡 Avertissement | -| **Activée par défaut** | Oui | - -Le composeur ne lit jamais le paramètre auquel l'un des opérandes est lié : cet opérande est donc **tiré puis jeté**. - -`Combine` génère chaque partie avant d'appeler le composeur — contraintes construites, vérifications de conflit exécutées, valeur produite — puis la laisse tomber. Rien n'échoue : la valeur composée est bien formée, et ne porte simplement pas la partie que le site d'appel dit qu'elle porte. - -## Non conforme - -```csharp -IAny customer = Any.Combine( - Any.String().NonEmpty().WithMaxLength(50), - Any.String().StartingWith("ORD-").WithLength(12), - (name, reference) => new Customer(name)); // JD027 : 'reference' n'atteint jamais le Customer -``` - -## Conforme - -```csharp -IAny customer = Any.Combine( - Any.String().NonEmpty().WithMaxLength(50), - Any.String().StartingWith("ORD-").WithLength(12), - (name, reference) => new Customer(name, OrderReference.Create(reference))); -``` - -Ou supprimer l'opérande, s'il ne fait réellement pas partie de la valeur : - -```csharp -IAny customer = Any.String().NonEmpty().WithMaxLength(50).As(name => new Customer(name)); -``` - -## Dire que le tirage est délibéré - -Nommer le paramètre `_`, comme C# l'écrit partout ailleurs pour dire « je sais, et c'est voulu » : - -```csharp -Any.Combine(first, second, (value, _) => new Wrapper(value)) // aucun diagnostic -``` - -## Pourquoi cela arrive - -Les deux formes les plus fréquentes sont un argument de constructeur oublié pendant un remaniement, et un composeur dont les paramètres ne correspondent plus à ses opérandes après l'insertion d'un opérande au milieu. Les deux laissent un générateur dont les contraintes soigneusement écrites n'ont d'effet sur rien de ce que le test observe. - -## Ce qui n'est pas signalé - -* Un paramètre nommé `_`. -* Un composeur passé comme groupe de méthodes — son corps n'appartient pas nécessairement à cette compilation, donc les opérandes qu'il utilise ne sont pas connaissables. -* Un composeur dont le corps entier est un `throw`. Il ne lit aucun paramètre par construction, et exerce le chemin d'échec que `Combine` enveloppe plutôt qu'il n'ignore un opérande. -* Un paramètre lu seulement dans un lambda imbriqué : cela compte comme lu. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/JD028.en.md b/doc/handwritten/for-users/analyzers/JD028.en.md deleted file mode 100644 index 2be3597e..00000000 --- a/doc/handwritten/for-users/analyzers/JD028.en.md +++ /dev/null @@ -1,63 +0,0 @@ -# JD028: InertDistinctness - -🌍 **Languages:** -🇬🇧 English (this file) | 🇫🇷 [Français](./JD028.fr.md) - -| | | -|---|---| -| **Category** | Composition (`JustDummies.Composition`) | -| **Severity** | 🟡 Warning | -| **Enabled by default** | Yes | - -Distinctness is declared over an element type that has no value equality. The default comparer falls back to reference equality, and every element the generator builds is a new instance — so the requirement is satisfied by construction and constrains nothing. - -The collection can hold the same value several times, which is precisely what the declaration asks it not to. - -## Noncompliant - -```csharp -public sealed class Box { // neither Equals nor IEquatable - public Box(int value) { Value = value; } - public int Value { get; } -} - -Any.ListOf(Any.Int32().Between(1, 2).As(v => new Box(v))).Distinct().WithCount(6) // JD028 -``` - -Measured on the library, that declaration returns six "distinct" boxes holding `[1, 1, 1, 2, 1, 2]` — green, every time. - -## Compliant - -Give the element type value equality: - -```csharp -public sealed record Box(int Value); - -Any.ListOf(Any.Int32().Between(1, 2).As(v => new Box(v))).Distinct().WithCount(6) -// AnyGenerationException: the element generator produced only 2 distinct value(s) -``` - -Now the declaration means something, and the impossible request is reported. - -Or answer the equality question explicitly: - -```csharp -Any.ListOf(generator).Distinct(new BoxByValue()) -Any.SetOf(generator, new BoxByValue()) -``` - -## Why the library cannot report this - -From its side the requirement is met: the draws really are pairwise unequal under the comparer it was given, and there is nothing to complain about. Only the element type's equality tells the inert case from the real one, and that is visible at the call site. - -## What it does not flag - -* A generator that hands back **existing** instances. `Any.SetOf(Any.OneOf(first, second))` returns the very references it was given, so drawing the same member twice yields the same reference and distinctness binds exactly as asked. -* An element type that is a value type, a record, an `IEquatable` implementer, or that overrides `Equals` — anywhere in its base chain. -* A non-sealed element type. A derived instance is free to add the equality the base lacks, and the rule only claims what it can prove. -* A projection that may return a shared instance (`As(v => Lookup(v))`); only a chain that provably builds a new value here qualifies. -* Any collection given an explicit comparer. - ---- - -[← All analyzer rules](README.md) diff --git a/doc/handwritten/for-users/analyzers/JD028.fr.md b/doc/handwritten/for-users/analyzers/JD028.fr.md deleted file mode 100644 index a7bd0278..00000000 --- a/doc/handwritten/for-users/analyzers/JD028.fr.md +++ /dev/null @@ -1,63 +0,0 @@ -# JD028 : InertDistinctness - -🌍 **Langues :** -🇫🇷 Français (ce fichier) | 🇬🇧 [English](./JD028.en.md) - -| | | -|---|---| -| **Catégorie** | Composition (`JustDummies.Composition`) | -| **Sévérité** | 🟡 Avertissement | -| **Activée par défaut** | Oui | - -La distinction est déclarée sur un type d'élément dépourvu d'égalité de valeur. Le comparateur par défaut retombe sur l'égalité de référence, et chaque élément que le générateur construit est une nouvelle instance — l'exigence est donc satisfaite par construction et ne contraint rien. - -La collection peut contenir plusieurs fois la même valeur, ce que la déclaration lui demande précisément de ne pas faire. - -## Non conforme - -```csharp -public sealed class Box { // ni Equals ni IEquatable - public Box(int value) { Value = value; } - public int Value { get; } -} - -Any.ListOf(Any.Int32().Between(1, 2).As(v => new Box(v))).Distinct().WithCount(6) // JD028 -``` - -Mesuré sur la bibliothèque, cette déclaration rend six boîtes « distinctes » portant `[1, 1, 1, 2, 1, 2]` — au vert, à chaque fois. - -## Conforme - -Donner une égalité de valeur au type d'élément : - -```csharp -public sealed record Box(int Value); - -Any.ListOf(Any.Int32().Between(1, 2).As(v => new Box(v))).Distinct().WithCount(6) -// AnyGenerationException : the element generator produced only 2 distinct value(s) -``` - -La déclaration signifie maintenant quelque chose, et la demande impossible est signalée. - -Ou répondre explicitement à la question de l'égalité : - -```csharp -Any.ListOf(generator).Distinct(new BoxByValue()) -Any.SetOf(generator, new BoxByValue()) -``` - -## Pourquoi la bibliothèque ne peut pas le signaler - -De son côté, l'exigence est tenue : les tirages sont réellement deux à deux différents sous le comparateur qu'on lui a donné, et il n'y a rien à redire. Seule l'égalité du type d'élément distingue le cas inerte du cas réel, et elle est visible sur le site d'appel. - -## Ce qui n'est pas signalé - -* Un générateur qui restitue des instances **existantes**. `Any.SetOf(Any.OneOf(first, second))` rend les références mêmes qu'on lui a confiées : tirer deux fois le même membre donne la même référence, et la distinction s'applique exactement comme demandé. -* Un type d'élément qui est un type valeur, un record, une implémentation d'`IEquatable`, ou qui redéfinit `Equals` — n'importe où dans sa chaîne de bases. -* Un type d'élément non scellé. Une instance dérivée est libre d'ajouter l'égalité qui manque à la base, et la règle ne prétend que ce qu'elle peut prouver. -* Une projection susceptible de rendre une instance partagée (`As(v => Lookup(v))`) ; seule une chaîne qui construit prouvablement une valeur neuve ici est concernée. -* Toute collection à laquelle un comparateur explicite est fourni. - ---- - -[← Toutes les règles d'analyse](README.fr.md) diff --git a/doc/handwritten/for-users/analyzers/README.fr.md b/doc/handwritten/for-users/analyzers/README.fr.md index fa284488..5009a19b 100644 --- a/doc/handwritten/for-users/analyzers/README.fr.md +++ b/doc/handwritten/for-users/analyzers/README.fr.md @@ -3,9 +3,11 @@ 🌍 **Langues:** 🇬🇧 [English](./README.md) | 🇫🇷 Français (ce fichier) -Ce dépôt fournit des règles Roslyn avec deux packages. Elles s'exécutent pendant la compilation et transforment en diagnostics de compilation des erreurs que le runtime et le pipeline de documentation ne signaleraient sinon que tardivement — voire jamais. Les règles **FirstClassErrors** (`FCExxx`) sont incluses dans le package `FirstClassErrors` ; les règles **JustDummies** (`JDxxx`) sont incluses dans le package `JustDummies`. Tout projet qui référence un package en bénéficie automatiquement, sans installation supplémentaire. +Ce dépôt fournit des règles Roslyn avec le package `FirstClassErrors`. Elles s'exécutent pendant la compilation et transforment en diagnostics de compilation des erreurs que le runtime et le pipeline de documentation ne signaleraient sinon que tardivement — voire jamais. Tout projet qui référence le package bénéficie automatiquement des règles `FCExxx`, sans installation supplémentaire. -Chaque règle a un identifiant stable (`FCExxx` ou `JDxxx`). Les erreurs sont des défauts durs ; les avertissements signalent des fautes probables ; les règles d'info sont des conventions, et plusieurs sont opt-in (voir chaque page pour les activer). +Les règles `JDxxx` sont incluses dans le package `JustDummies` et documentées dans [son propre dépôt](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-users/analyzers/README.fr.md). + +Chaque règle a un identifiant stable (`FCExxx`). Les erreurs sont des défauts durs ; les avertissements signalent des fautes probables ; les règles d'info sont des conventions, et plusieurs sont opt-in (voir chaque page pour les activer). ## Codes d'erreur @@ -49,62 +51,6 @@ Chaque règle a un identifiant stable (`FCExxx` ou `JDxxx`). Les erreurs sont de | [FCE021 PreferNonThrowingAlternativeToTry](FCE021.fr.md) | 🟠 Warning | activée | Outcome.Try enveloppe un appel qui a déjà une contrepartie non-levante TryXxx / TryCreate disponible pour le framework cible ; envisagez de mapper son résultat (conseil — à supprimer là où la contrepartie n'est pas un vrai inverse). | | [FCE022 TryCatchesCancellation](FCE022.fr.md) | 🟠 Warning | activée | Outcome.Try lie TException à OperationCanceledException (ou un sous-type) ; Try laisse toujours l'annulation se propager, donc le catch est inatteignable et le mapper ne s'exécute jamais. | -## JustDummies — Reproductibilité - -Ces règles sont incluses dans le package **`JustDummies`** (pas FirstClassErrors) et empêchent un corps de test asynchrone d'avaler silencieusement ses propres échecs. - -| Règle | Sévérité | Défaut | Description | -|-------|----------|--------|-------------| -| [JD001 AsyncBodyPassedToReproducibly](JD001.fr.md) | 🔴 Erreur | on | Une lambda async est passée à `Any.Reproducibly(Action)` synchrone ; liée à une Action elle devient async void et ses échecs ne font jamais échouer le test. Utilisez `Any.ReproduciblyAsync` et faites `await`. | -| [JD002 DiscardedReproduciblyAsyncResult](JD002.fr.md) | 🔴 Erreur | on | Le `Task` retourné par `Any.ReproduciblyAsync` est jeté (instruction isolée ou `_ =`) ; les échecs du corps sont perdus. Faites `await`. | -| [JD003 AwaitableBodyPassedToReproducibly](JD003.fr.md) | 🔴 Erreur | on | Une lambda synchrone dont le corps abandonne une tâche, ou un groupe de méthodes `async void`, atteint `Any.Reproducibly` ; la portée retourne avant l'exécution des assertions, et `CS4014` ne se déclenche pas. | -| [JD004 DiscardedSeedingResult](JD004.fr.md) | 🔴 Erreur | on | La poignée retournée par `Any.UseSeed` est jetée, laissant la graine épinglée pour la suite — ou `Any.WithSeed` est appelé pour son effet, alors qu'il n'épingle rien. | -| [JD007 DrawOutsideThePinnedScope](JD007.fr.md) | 🟠 Avertissement | on | Une valeur est tirée pendant la construction d'une classe de test `[Reproducible]`, qu'xUnit exécute avant l'ouverture de la portée de graine ; la graine rapportée ne la rejoue pas. | -| [JD008 ArbitraryValueInTheoryData](JD008.fr.md) | 🟠 Avertissement | on | Le fournisseur de données d'une théorie tire une valeur à la découverte, avant tout épinglage ; tous les cas partagent cette unique valeur. | -| [JD009 DrawInStaticInitializer](JD009.fr.md) | 🟠 Avertissement | on | Un initialiseur statique tire une seule fois pour toute la suite, sous le premier test exécuté, rendant les tests dépendants de l'ordre et rejouables depuis aucune graine. | -| [JD010 ReproducibleOnNonTestMethod](JD010.fr.md) | 🟠 Avertissement | on | `[Reproducible]` sur une méthode qu'xUnit ne traite jamais comme un test ; il n'épingle rien, et ressemble exactement à la forme active. | -| [JD018 NestedReproducibilityScope](JD018.fr.md) | 🟠 Avertissement | on | Une portée de reproductibilité imbriquée dans une autre ; l'interne tire une graine neuve, donc la graine rapportée par l'externe ne rejoue rien. | -| [JD021 BlankReplaySnippet](JD021.fr.md) | 🟠 Avertissement | on | `Any.UseSeed` reçoit un snippet de rejeu vide, que la garde rejette — depuis un hook d'adaptateur, faisant échouer toute la suite. | -| [JD019 CommittedReplaySeed](JD019.fr.md) | 🔵 Info | opt-in | Une graine de rejeu constante est épinglée dans du code committé : le test cesse de varier d'une exécution à l'autre. | -| [JD020 SharedStaticAnyContext](JD020.fr.md) | 🔵 Info | on | Un `AnyContext` tenu dans un champ statique ; les tirages entrelacés ne rendent stables ni la séquence ni le multiensemble. | -| [JD022 ParallelDrawWithoutPerItemSeed](JD022.fr.md) | 🔵 Info | on | Une unité de travail parallèle tire sans sa propre portée de graine : les tirages s'entrelacent et l'exécution ne rejoue rien. | - -## JustDummies — Usage - -Un générateur est une *recette* immuable, et `Generate()` est la seule chose qui en matérialise une valeur. Ces règles ferment les deux façons dont cette distinction se perd silencieusement. - -| Règle | Sévérité | Défaut | Description | -|-------|----------|--------|-------------| -| [JD005 GeneratorRenderedAsText](JD005.fr.md) | 🔴 Erreur | on | Un générateur est interpolé, concaténé ou passé à `ToString()` au lieu d'être généré ; aucun générateur ne surcharge `ToString()`, donc le texte obtenu est le nom de type du constructeur. | -| [JD006 DiscardedGeneratorResult](JD006.fr.md) | 🟠 Avertissement | on | Le générateur retourné par une contrainte est jeté en instruction isolée ; les générateurs étant immuables, l'invariant déclaré est silencieusement perdu. | -| [JD011 GeneratorWhereValueExpected](JD011.fr.md) | 🟠 Avertissement | opt-in | Un générateur atteint une position `object`, `dynamic` ou `params object[]` : c'est la recette qui est stockée, comparée ou assérée, pas la valeur. | -| [JD012 GeneratorPooledAsValue](JD012.fr.md) | 🟠 Avertissement | on | `Any.OneOf` reçoit des générateurs et infère un ensemble de recettes ; y tirer produit une recette plutôt qu'une valeur. | -| [JD013 HeldCollectionPassedToOneOf](JD013.fr.md) | 🟠 Avertissement | on | Une collection tenue passée à `Any.OneOf` lie `T` au type de la collection, formant un ensemble d'un seul élément ; `Any.ElementOf` tire parmi ses éléments. | - -## JustDummies — Contraintes - -Ces règles anticipent, à la compilation, le sous-ensemble des vérifications de contraintes de la bibliothèque qui est décidable depuis des constantes. Les vérifications d'exécution demeurent : elles couvrent tous les arguments que celles-ci ne peuvent pas voir. - -| Règle | Sévérité | Défaut | Description | -|-------|----------|--------|-------------| -| [JD014 RejectedConstantArgument](JD014.fr.md) | 🟠 Avertissement | on | Un argument de contrainte est une constante que la garde du générateur refuse : l'appel lève à chaque exécution. | -| [JD015 StringConstraintsAdmitNoValue](JD015.fr.md) | 🟠 Avertissement | on | Les contraintes constantes d'une chaîne `AnyString` n'admettent aucune valeur — un fragment hors de la famille de caractères ou de la casse déclarée, ou des fragments qui ne peuvent pas tenir dans la longueur déclarée. | -| [JD016 CollectionConstraintsAdmitNoValue](JD016.fr.md) | 🟠 Avertissement | on | Les contraintes de cardinal d'une chaîne de collection ne peuvent pas toutes tenir, ou elle réclame plus d'éléments distincts que son générateur d'éléments ne peut en produire. | -| [JD017 EnumUniverseViolation](JD017.fr.md) | 🟠 Avertissement | on | Une contrainte d'enum sort des membres déclarés — une combinaison de drapeaux sans `AllowingCombinations()`, ou une exclusion qui vide l'univers. | -| [JD023 ScalarChainAdmitsNoValue](JD023.fr.md) | 🟠 Avertissement | on | Les contraintes constantes d'une chaîne entière réduisent le domaine à rien — bornes, treillis ou liste d'autorisation. | -| [JD024 ConstraintWithNoEffect](JD024.fr.md) | 🔵 Info | on | Une contrainte ne rétrécit rien : exclusion d'une valeur que le domaine ne pouvait pas produire, ou borne déjà impliquée. La seule famille de contraintes que l'exécution ne signale jamais. | -| [JD025 DuplicatePoolValue](JD025.fr.md) | 🟠 Avertissement | on | La même constante figure deux fois dans un réservoir ; les doublons sont écrasés, donc le réservoir est plus petit d'une valeur qu'il n'y paraît et le doublon ne pondère rien. | -| [JD026 EmptyRelativeUri](JD026.fr.md) | 🟠 Avertissement | on | Une URI relative à zéro segment, sans requête, fragment ni racine est la référence vide — la seule chaîne dont l'échec atterrit au moment de l'act plutôt que sur la ligne d'arrange. | - -## JustDummies — Composition - -Ces règles concernent l'assemblage de générateurs en générateurs plus gros — les opérandes de `Combine`, et le contrat d'élément sur lequel s'appuie un générateur de collection. Leur point commun : rien ne va de travers. Le générateur composé se construit, tire et rend une valeur. Ce n'est simplement pas la valeur que le site d'appel décrit. - -| Règle | Sévérité | Défaut | Description | -|-------|----------|--------|-------------| -| [JD027 UnusedCombineOperand](JD027.fr.md) | 🟠 Avertissement | on | Un opérande de `Combine` est tiré puis jeté parce que le composeur ne lit jamais son paramètre. Nommer le paramètre `_` pour dire que le tirage est délibéré. | -| [JD028 InertDistinctness](JD028.fr.md) | 🟠 Avertissement | on | La distinction est déclarée sur un type d'élément sans égalité de valeur : elle est satisfaite par construction et la collection peut quand même contenir deux fois la même valeur. | - ## Configuration La sévérité de chaque règle se règle dans `.editorconfig`, par exemple : diff --git a/doc/handwritten/for-users/analyzers/README.md b/doc/handwritten/for-users/analyzers/README.md index e713fe33..0b45ede5 100644 --- a/doc/handwritten/for-users/analyzers/README.md +++ b/doc/handwritten/for-users/analyzers/README.md @@ -3,9 +3,11 @@ 🌍 **Languages:** 🇬🇧 English (this file) | 🇫🇷 [Français](./README.fr.md) -This repository ships Roslyn rules with two packages. They run while your project compiles, turning mistakes that the runtime and documentation pipeline would otherwise report late — or never at all — into build-time diagnostics. The **FirstClassErrors** rules (`FCExxx`) ship inside the `FirstClassErrors` package; the **JustDummies** rules (`JDxxx`) ship inside the `JustDummies` package. Any project that references a package picks up its rules automatically, with no extra install. +This repository ships Roslyn rules with the `FirstClassErrors` package. They run while your project compiles, turning mistakes that the runtime and documentation pipeline would otherwise report late — or never at all — into build-time diagnostics. Any project that references the package picks up the `FCExxx` rules automatically, with no extra install. -Each rule has a stable id (`FCExxx` or `JDxxx`). Errors are hard defects; warnings flag likely mistakes; the info rules are conventions, and several are opt-in (see each page for how to enable them). +The `JDxxx` rules ship inside the `JustDummies` package and are documented in [its own repository](https://github.com/Reefact/just-dummies/blob/main/doc/handwritten/for-users/analyzers/README.md). + +Each rule has a stable id (`FCExxx`). Errors are hard defects; warnings flag likely mistakes; the info rules are conventions, and several are opt-in (see each page for how to enable them). ## Error codes @@ -49,62 +51,6 @@ Each rule has a stable id (`FCExxx` or `JDxxx`). Errors are hard defects; warnin | [FCE021 PreferNonThrowingAlternativeToTry](FCE021.en.md) | 🟠 Warning | on | Outcome.Try wraps a call that already has a non-throwing TryXxx / TryCreate counterpart available for the target framework; consider mapping its result (advisory — suppress where the counterpart is not a true inverse). | | [FCE022 TryCatchesCancellation](FCE022.en.md) | 🟠 Warning | on | Outcome.Try binds TException to OperationCanceledException (or a subtype); Try always lets cancellation propagate, so the catch is unreachable and the mapper never runs. | -## JustDummies — Reproducibility - -These rules ship in the **`JustDummies`** package (not FirstClassErrors) and keep an asynchronous test body from silently swallowing its own failures. - -| Rule | Severity | Default | Description | -|------|----------|---------|-------------| -| [JD001 AsyncBodyPassedToReproducibly](JD001.en.md) | 🔴 Error | on | An async lambda is passed to the synchronous Any.Reproducibly(Action); bound to an Action it becomes async void and its failures never fail the test. Use Any.ReproduciblyAsync and await it. | -| [JD002 DiscardedReproduciblyAsyncResult](JD002.en.md) | 🔴 Error | on | The task returned by Any.ReproduciblyAsync is discarded (a bare statement, or `_ =`); the body's failures are lost. Await it. | -| [JD003 AwaitableBodyPassedToReproducibly](JD003.en.md) | 🔴 Error | on | A synchronous lambda whose body drops a task, or an async void method group, reaches Any.Reproducibly; the scope returns before the assertions run, and CS4014 does not fire. | -| [JD004 DiscardedSeedingResult](JD004.en.md) | 🔴 Error | on | The handle returned by Any.UseSeed is discarded, leaving the seed pinned for whatever runs next — or Any.WithSeed is called for effect, which pins nothing at all. | -| [JD007 DrawOutsideThePinnedScope](JD007.en.md) | 🟠 Warning | on | A value is drawn during a [Reproducible] test class's construction, which xUnit runs before the seed scope opens; the reported seed does not replay it. | -| [JD008 ArbitraryValueInTheoryData](JD008.en.md) | 🟠 Warning | on | A theory's data provider draws a value at discovery, before any seed is pinned; every case shares the one value. | -| [JD009 DrawInStaticInitializer](JD009.en.md) | 🟠 Warning | on | A static initializer draws once for the whole suite, under whichever test ran first, making the tests order-dependent and replayable from no seed. | -| [JD010 ReproducibleOnNonTestMethod](JD010.en.md) | 🟠 Warning | on | [Reproducible] on a method xUnit never treats as a test; it pins nothing, and looks exactly like the working form. | -| [JD018 NestedReproducibilityScope](JD018.en.md) | 🟠 Warning | on | A reproducibility scope nested inside another; the inner one draws a fresh seed, so the outer's reported seed replays nothing. | -| [JD021 BlankReplaySnippet](JD021.en.md) | 🟠 Warning | on | Any.UseSeed is given a blank replay snippet, which the guard rejects — from an adapter hook, failing the whole suite. | -| [JD019 CommittedReplaySeed](JD019.en.md) | 🔵 Info | opt-in | A constant replay seed is pinned in committed code, so the test stops varying between runs. | -| [JD020 SharedStaticAnyContext](JD020.en.md) | 🔵 Info | on | An AnyContext held in a static field; interleaved draws make neither the sequence nor the multiset stable. | -| [JD022 ParallelDrawWithoutPerItemSeed](JD022.en.md) | 🔵 Info | on | A parallel work item draws without its own seed scope, so the draws interleave and the run replays nothing. | - -## JustDummies — Usage - -A generator is an immutable *recipe*, and `Generate()` is the only thing that materializes a value from it. These rules close the two ways that distinction is lost silently. - -| Rule | Severity | Default | Description | -|------|----------|---------|-------------| -| [JD005 GeneratorRenderedAsText](JD005.en.md) | 🔴 Error | on | A generator is interpolated, concatenated or ToString()'d instead of generated from; no generator overrides ToString(), so the text is the builder's type name. | -| [JD006 DiscardedGeneratorResult](JD006.en.md) | 🟠 Warning | on | The generator returned by a constraint is discarded as a bare statement; generators are immutable, so the declared invariant is silently lost. | -| [JD011 GeneratorWhereValueExpected](JD011.en.md) | 🟠 Warning | opt-in | A generator reaches an object, dynamic or params object[] position, so the recipe is stored, compared or asserted on instead of the value. | -| [JD012 GeneratorPooledAsValue](JD012.en.md) | 🟠 Warning | on | Any.OneOf is given generators, inferring a pool of recipes; drawing from it yields a recipe rather than a value. | -| [JD013 HeldCollectionPassedToOneOf](JD013.en.md) | 🟠 Warning | on | A held collection passed to Any.OneOf binds T to the collection type, making a pool of one; Any.ElementOf draws from its elements. | - -## JustDummies — Constraints - -These rules front-load, to build time, the subset of the library's run-time constraint checks that is decidable from compile-time constants. The run-time checks stay: they cover every argument these cannot see. - -| Rule | Severity | Default | Description | -|------|----------|---------|-------------| -| [JD014 RejectedConstantArgument](JD014.en.md) | 🟠 Warning | on | A constraint argument is a compile-time constant the generator's own guard refuses, so the call throws every time it runs. | -| [JD015 StringConstraintsAdmitNoValue](JD015.en.md) | 🟠 Warning | on | An AnyString chain's constant constraints admit no value — a fragment outside the declared character family or casing, or fragments that cannot fit the declared length. | -| [JD016 CollectionConstraintsAdmitNoValue](JD016.en.md) | 🟠 Warning | on | A collection chain's count constraints cannot all hold, or it asks for more distinct elements than its element generator can produce. | -| [JD017 EnumUniverseViolation](JD017.en.md) | 🟠 Warning | on | An enum constraint steps outside the declared members — a flag combination without AllowingCombinations(), or an exclusion that empties the universe. | -| [JD023 ScalarChainAdmitsNoValue](JD023.en.md) | 🟠 Warning | on | An integer chain's constant constraints narrow the domain to nothing — bounds, lattice or allow-list. | -| [JD024 ConstraintWithNoEffect](JD024.en.md) | 🔵 Info | on | A constraint narrows nothing: an exclusion of a value the domain could never produce, or a bound already implied. The only constraint family the run time never reports. | -| [JD025 DuplicatePoolValue](JD025.en.md) | 🟠 Warning | on | The same constant is listed twice in a pool; duplicates collapse, so the pool is one value smaller than it reads and the duplicate weights nothing. | -| [JD026 EmptyRelativeUri](JD026.en.md) | 🟠 Warning | on | A relative URI with zero path segments and no query, fragment or root is the empty reference — the one chain whose failure lands at act time rather than at the arrange line. | - -## JustDummies — Composition - -These rules are about assembling generators into bigger ones — `Combine`'s operands, and the element contract a collection generator relies on. What they share is that nothing goes wrong: the composed generator builds, draws and returns a value. It is simply not the value the call site describes. - -| Rule | Severity | Default | Description | -|------|----------|---------|-------------| -| [JD027 UnusedCombineOperand](JD027.en.md) | 🟠 Warning | on | A Combine operand is drawn and thrown away because the composer never reads its parameter. Name the parameter `_` to say the draw is deliberate. | -| [JD028 InertDistinctness](JD028.en.md) | 🟠 Warning | on | Distinctness is declared over an element type with no value equality, so it is satisfied by construction and the collection can still hold the same value twice. | - ## Configuring Every rule's severity can be tuned in `.editorconfig`, for example: