diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index 10d24726..4c55db61 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -80,14 +80,14 @@ public void DistinctOverAWideDomain() { [Fact(DisplayName = "Distinct: a count beyond the element cardinality conflicts eagerly, naming the shortfall.")] public void DistinctCardinalityConflictsEagerly() { ConflictingAnyConstraintException fromBool = Assert.Throws( - () => Any.SetOf(Any.Bool()).WithCount(3)); + () => Any.SetOf(Any.Boolean()).WithCount(3)); Check.That(fromBool.Message).Contains("2 distinct value"); Check.ThatCode(() => Any.SetOf(Any.Enum()).WithMinCount(5)).Throws(); Check.ThatCode(() => Any.SetOf(Any.Int32().Between(1, 3)).WithCount(5)).Throws(); Check.ThatCode(() => Any.ListOf(Any.Int32().Between(1, 3)).WithCount(5).Distinct()).Throws(); // Order-independent: turning distinct on after the count is set conflicts just the same. - Check.ThatCode(() => Any.ListOf(Any.Bool()).WithCount(3).Distinct()).Throws(); + Check.ThatCode(() => Any.ListOf(Any.Boolean()).WithCount(3).Distinct()).Throws(); } [Fact(DisplayName = "Distinct: an unknowable small domain cannot be detected early, so a shortfall surfaces at generation.")] @@ -228,7 +228,7 @@ public void ContainingAnyDefersToGeneration() { // When every source draws from the same two-value domain, three distinct values are impossible — but the // overlap is opaque, so it is caught while drawing (a replayable AnyGenerationException) rather than as a // false eager conflict. - Check.ThatCode(() => Any.SetOf(Any.Bool()).ContainingAny(Any.Bool()).ContainingAny(Any.Bool()).ContainingAny(Any.Bool()).Generate()) + Check.ThatCode(() => Any.SetOf(Any.Boolean()).ContainingAny(Any.Boolean()).ContainingAny(Any.Boolean()).ContainingAny(Any.Boolean()).Generate()) .Throws(); } @@ -315,7 +315,7 @@ public void DictionaryOfBehaves() { Check.That(dictionary.Values).ContainsOnlyElementsThatMatch(value => value.Length > 0); } - Check.ThatCode(() => Any.DictionaryOf(Any.Bool(), Any.Int32()).WithCount(3)).Throws(); + Check.ThatCode(() => Any.DictionaryOf(Any.Boolean(), Any.Int32()).WithCount(3)).Throws(); Check.ThatCode(() => Any.DictionaryOf(null!, Any.Int32())).Throws(); } diff --git a/Dummies.UnitTests/AnySetTypeTests.cs b/Dummies.UnitTests/AnySetTypeTests.cs index 44370a7c..5d067352 100644 --- a/Dummies.UnitTests/AnySetTypeTests.cs +++ b/Dummies.UnitTests/AnySetTypeTests.cs @@ -18,24 +18,24 @@ private enum OrderStatus { } - [Fact(DisplayName = "Bool: unconstrained draws hit both values; pins pin; contradictory pins conflict.")] - public void BoolBehaves() { + [Fact(DisplayName = "Boolean: unconstrained draws hit both values; pins pin; contradictory pins conflict.")] + public void BooleanBehaves() { HashSet seen = new(); - for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Bool().Generate()); } + for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Boolean().Generate()); } Check.That(seen.Count).IsEqualTo(2); for (int i = 0; i < SampleCount; i++) { - Check.That(Any.Bool().True().Generate()).IsTrue(); - Check.That(Any.Bool().False().Generate()).IsFalse(); - Check.That(Any.Bool().DifferentFrom(true).Generate()).IsFalse(); + Check.That(Any.Boolean().True().Generate()).IsTrue(); + Check.That(Any.Boolean().False().Generate()).IsFalse(); + Check.That(Any.Boolean().DifferentFrom(true).Generate()).IsFalse(); } ConflictingAnyConstraintException conflict = Assert.Throws( - () => Any.Bool().True().False()); + () => Any.Boolean().True().False()); Check.That(conflict.Message).Contains("False()"); Check.That(conflict.Message).Contains("True()"); - bool value = Any.Bool().True().Generate(); + bool value = Any.Boolean().True().Generate(); Check.That(value).IsTrue(); } diff --git a/Dummies.UnitTests/FactoryNamingConventionTests.cs b/Dummies.UnitTests/FactoryNamingConventionTests.cs new file mode 100644 index 00000000..e8bd3ace --- /dev/null +++ b/Dummies.UnitTests/FactoryNamingConventionTests.cs @@ -0,0 +1,56 @@ +#region Usings declarations + +using System.Reflection; + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +/// +/// Locks the factory-naming rule recorded in ADR-0031: every parameterless, type-named scalar factory +/// on is named after the CLR type it produces — which is also the name of its +/// Any{ClrType} builder. This is the guard that would have caught the Bool/AnyBool +/// deviation before release. The mirror itself is guarded +/// separately by SurfaceParityTests. +/// +public sealed class FactoryNamingConventionTests { + + // The type-named scalar factories are exactly Any's public, static, non-generic, parameterless methods + // whose return type is a builder (implements IAny). StringMatching (parameters), Enum (generic), + // the collection/composition factories (generic, parameterized) and WithSeed/Reproducibly (not builders) + // fall out by construction, so no hand-maintained allow-list can drift out of sync with the surface. + private static IEnumerable ScalarFactories() { + return typeof(Any).GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(method => !method.IsGenericMethod + && method.GetParameters().Length == 0 + && ElementTypeOf(method.ReturnType) is not null); + } + + // The T of the single IAny a builder implements, or null when the type is not a builder. + private static Type? ElementTypeOf(Type builder) { + return builder.GetInterfaces() + .FirstOrDefault(candidate => candidate.IsGenericType && candidate.GetGenericTypeDefinition() == typeof(IAny<>)) + ?.GetGenericArguments()[0]; + } + + [Fact(DisplayName = "Every type-named scalar factory, and its builder, is named after the CLR type it produces.")] + public void FactoriesAreNamedAfterTheirClrType() { + List factories = ScalarFactories().ToList(); + + // Guards the reflection itself: were the query ever to match nothing, every assertion below would pass vacuously. + Check.That(factories.Count).IsStrictlyGreaterThan(15); + + foreach (MethodInfo factory in factories) { + Type builder = factory.ReturnType; + string clrName = ElementTypeOf(builder)!.Name; + + Check.WithCustomMessage($"Any.{factory.Name}() returns {builder.Name} (IAny<{clrName}>); the factory must be named '{clrName}', after the CLR type it produces.") + .That(factory.Name).IsEqualTo(clrName); + Check.WithCustomMessage($"The builder for {clrName} is named '{builder.Name}'; it must be 'Any{clrName}' to match the CLR type.") + .That(builder.Name).IsEqualTo("Any" + clrName); + } + } + +} diff --git a/Dummies.UnitTests/SeedReproducibilityTests.cs b/Dummies.UnitTests/SeedReproducibilityTests.cs index d4fc559a..3eae09a3 100644 --- a/Dummies.UnitTests/SeedReproducibilityTests.cs +++ b/Dummies.UnitTests/SeedReproducibilityTests.cs @@ -25,7 +25,7 @@ private static string Batch() { ulong unsigned = Any.UInt64().Generate(); double real = Any.Double().Between(0d, 1000d).Generate(); decimal exact = Any.Decimal().Between(0m, 1000m).Generate(); - bool flag = Any.Bool().Generate(); + bool flag = Any.Boolean().Generate(); Guid id = Any.Guid().Generate(); char letter = Any.Char().Generate(); TimeSpan span = Any.TimeSpan().Generate(); diff --git a/Dummies.UnitTests/SurfaceParityTests.cs b/Dummies.UnitTests/SurfaceParityTests.cs index e90ba889..ca2b580b 100644 --- a/Dummies.UnitTests/SurfaceParityTests.cs +++ b/Dummies.UnitTests/SurfaceParityTests.cs @@ -127,7 +127,7 @@ public static IEnumerable Builders() { yield return [typeof(AnyDateTimeOffset), InstantAlgebra]; // The remaining scalar builders each carry their own deliberate set. - yield return [typeof(AnyBool), new[] { "True", "False", "DifferentFrom" }]; + yield return [typeof(AnyBoolean), new[] { "True", "False", "DifferentFrom" }]; yield return [typeof(AnyGuid), new[] { "NonEmpty", "Empty", "OneOf", "Except", "DifferentFrom" }]; yield return [typeof(AnyEnum), new[] { "OneOf", "Except", "DifferentFrom" }]; yield return [typeof(AnyChar), new[] { "Alpha", "AlphaNumeric", "Numeric", "UpperCase", "LowerCase", "OneOf", "Except", "DifferentFrom" }]; diff --git a/Dummies/Any.cs b/Dummies/Any.cs index 5c570531..d46b876e 100644 --- a/Dummies/Any.cs +++ b/Dummies/Any.cs @@ -235,8 +235,8 @@ public static AnyDecimal Decimal() { /// flip unless pinned with True() or False(). /// /// A generator to constrain fluently. - public static AnyBool Bool() { - return AnyBool.Create(AmbientRandomSource.Instance); + public static AnyBoolean Boolean() { + return AnyBoolean.Create(AmbientRandomSource.Instance); } /// diff --git a/Dummies/AnyBool.cs b/Dummies/AnyBoolean.cs similarity index 84% rename from Dummies/AnyBool.cs rename to Dummies/AnyBoolean.cs index c1b44b85..4b229325 100644 --- a/Dummies/AnyBool.cs +++ b/Dummies/AnyBoolean.cs @@ -5,12 +5,12 @@ namespace Dummies; /// mostly useful for symmetry when a test sweeps cases — and contradictory pins fail eagerly with a /// naming both sides, like every other generator. /// -public sealed class AnyBool : IAny, IHasRandomSource, ICardinalityHint { +public sealed class AnyBoolean : IAny, IHasRandomSource, ICardinalityHint { #region Statics members declarations - internal static AnyBool Create(RandomSource source) { - return new AnyBool(source, null, null); + internal static AnyBoolean Create(RandomSource source) { + return new AnyBoolean(source, null, null); } private static string V(bool value) { @@ -27,7 +27,7 @@ private static string V(bool value) { #endregion - private AnyBool(RandomSource source, bool? pinned, string? pinnedConstraint) { + private AnyBoolean(RandomSource source, bool? pinned, string? pinnedConstraint) { _source = source; _pinned = pinned; _pinnedConstraint = pinnedConstraint; @@ -44,14 +44,14 @@ private AnyBool(RandomSource source, bool? pinned, string? pinnedConstraint) { /// Pins the value to true. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. - public AnyBool True() { + public AnyBoolean True() { return Pin(true, "True()"); } /// Pins the value to false. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. - public AnyBool False() { + public AnyBoolean False() { return Pin(false, "False()"); } @@ -62,7 +62,7 @@ public AnyBool False() { /// The value the generated value must differ from. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. - public AnyBool DifferentFrom(bool value) { + public AnyBoolean DifferentFrom(bool value) { return Pin(!value, $"DifferentFrom({V(value)})"); } @@ -71,12 +71,12 @@ public bool Generate() { return _pinned ?? _source.Current.Random.Next(2) == 0; } - private AnyBool Pin(bool value, string applying) { + private AnyBoolean Pin(bool value, string applying) { if (_pinnedConstraint is not null && _pinned != value) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_pinnedConstraint} already pins the value to {V(_pinned!.Value)}."); } - return new AnyBool(_source, value, applying); + return new AnyBoolean(_source, value, applying); } } diff --git a/Dummies/AnyContext.cs b/Dummies/AnyContext.cs index dec01cd2..ea3b9641 100644 --- a/Dummies/AnyContext.cs +++ b/Dummies/AnyContext.cs @@ -213,8 +213,8 @@ public AnyDecimal Decimal() { /// flip unless pinned with True() or False(). /// /// A generator to constrain fluently. - public AnyBool Bool() { - return AnyBool.Create(_source); + public AnyBoolean Boolean() { + return AnyBoolean.Create(_source); } /// diff --git a/Dummies/README.nuget.md b/Dummies/README.nuget.md index de2048ca..a26b2907 100644 --- a/Dummies/README.nuget.md +++ b/Dummies/README.nuget.md @@ -26,7 +26,7 @@ matter — and that is the point. `.Generate()`, across the .NET simple types: `String`, `Char`, every integer width (`SByte`/`Byte`/`Int16`/`UInt16`/`Int32`/`UInt32`/`Int64`/`UInt64`), `Double`/`Single`/`Decimal` (finite values only — never NaN or infinities), - `Bool`, `Guid`, `Enum` (declared members only), `TimeSpan`, `DateTime` (UTC) + `Boolean`, `Guid`, `Enum` (declared members only), `TimeSpan`, `DateTime` (UTC) and `DateTimeOffset`. On modern targets (`net8.0`) the surface extends to `DateOnly`, `TimeOnly`, `Int128`, `UInt128` and `Half`; the package also targets `netstandard2.0` for the widest reach. 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 new file mode 100644 index 00000000..f7a28968 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.fr.md @@ -0,0 +1,140 @@ +# 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é +**Date :** 2026-07-21 +**Décideurs :** Reefact + +## Contexte + +* `Dummies` 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 Dummies 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. +* `Dummies` 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é `dummies-check` et le README du package *(fait + dans ce changement)*. +* Ajouter le garde de parité de nommage à `Dummies.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 Dummies 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 new file mode 100644 index 00000000..2ddb7239 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.md @@ -0,0 +1,130 @@ +# 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 +**Date:** 2026-07-21 +**Decision Makers:** Reefact + +## Context + +* `Dummies` 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 Dummies architecture & design audit surfaced it (§8.2, §8.4) and + recommended the choice be settled deliberately and recorded before release. +* `Dummies` 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 `dummies-check` packaged-asset probe, and the package README *(done in this + change)*. +* Add the factory-naming parity guard to `Dummies.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 Dummies architecture & design audit, §8.2 and §8.4 — surfaced the deviation. +* Issue #222. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index e003c6cf..9bfe7217 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -213,3 +213,4 @@ Optional supporting material: | [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 | Accepted | +| [ADR-0031](0031-name-any-factories-after-their-clr-type.md) | Name Any's scalar factories after their CLR type | Accepted | diff --git a/tools/dummies-check/Program.cs b/tools/dummies-check/Program.cs index aa4fd628..7dc4cbe7 100644 --- a/tools/dummies-check/Program.cs +++ b/tools/dummies-check/Program.cs @@ -132,7 +132,7 @@ private static string SeedBatch(AnyContext any) { any.UInt64().Generate().ToString(CultureInfo.InvariantCulture), any.Double().Between(0d, 1000d).Generate().ToString("R", CultureInfo.InvariantCulture), any.Decimal().Between(0m, 1000m).Generate().ToString(CultureInfo.InvariantCulture), - any.Bool().Generate().ToString(), + any.Boolean().Generate().ToString(), any.Guid().Generate().ToString(), any.Char().Generate().ToString(), any.TimeSpan().Generate().Ticks.ToString(CultureInfo.InvariantCulture),