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 new file mode 100644 index 0000000..6c333af --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.fr.md @@ -0,0 +1,156 @@ +# 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é +**Date :** 2026-07-26 +**Décideurs :** Reefact + +## Contexte + +* Chaque générateur du point d'entrée `Any` de `Dummies` — 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. +* `Dummies` 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 Dummies 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 new file mode 100644 index 0000000..be9922b --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0035-enforce-structural-any-conflicts-at-compile-time.md @@ -0,0 +1,146 @@ +# 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 +**Date:** 2026-07-26 +**Decision Makers:** Reefact + +## Context + +* Every generator on `Dummies`' `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. +* `Dummies` 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 Dummies Nice-to-Have backlog that prompted `Any.Uri()`. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 73c7bbd..f8b2da8 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -217,3 +217,4 @@ Optional supporting material: | [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 |