From 9b27ee40aaab152415e5fdbc8fb1907934406e31 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 19:52:54 +0000 Subject: [PATCH 1/4] docs(dummies): record ADR-0031 for the factory-naming rule Record the decision the 2026-07-20 Dummies architecture audit surfaced (section 8.2): every parameterless scalar factory on Any and AnyContext, and the builder it returns, is named after the CLR name of the type it produces, with no exception. Accepted by the maintainer. Numbered 0031 because ADR-0030 was taken by the terminal String().OneOf decision. Refs: #222 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019XoisyHa8b5UpezV7QV4Jd --- ...e-any-factories-after-their-clr-type.fr.md | 140 ++++++++++++++++++ ...name-any-factories-after-their-clr-type.md | 130 ++++++++++++++++ doc/handwritten/for-maintainers/adr/README.md | 1 + 3 files changed, 271 insertions(+) create mode 100644 doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.fr.md create mode 100644 doc/handwritten/for-maintainers/adr/0031-name-any-factories-after-their-clr-type.md 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 | From 7f05c21094469ad8ce067d5636b6087749b0b5e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 19:53:07 +0000 Subject: [PATCH 2/4] refactor(dummies): name the boolean factory Boolean, not Bool The scalar factories are named after the CLR type they produce (Int32, Single, ...), which is also their builder's name; Bool()/AnyBool was the sole deviation from that rule, since the CLR name of bool is Boolean. Rename it to Boolean()/AnyBoolean so the convention holds without exception, while the library is still pre-release and the rename costs nothing. Implements ADR-0031. Refs: #222 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019XoisyHa8b5UpezV7QV4Jd --- Dummies.UnitTests/AnyCollectionTests.cs | 8 ++++---- Dummies.UnitTests/AnySetTypeTests.cs | 16 ++++++++-------- Dummies.UnitTests/SeedReproducibilityTests.cs | 2 +- Dummies/Any.cs | 4 ++-- Dummies/{AnyBool.cs => AnyBoolean.cs} | 18 +++++++++--------- Dummies/AnyContext.cs | 4 ++-- Dummies/README.nuget.md | 2 +- tools/dummies-check/Program.cs | 2 +- 8 files changed, 28 insertions(+), 28 deletions(-) rename Dummies/{AnyBool.cs => AnyBoolean.cs} (84%) 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/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/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/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), From 0cdfe3c7cbbd0ba7c243c3630c0d686f7d9f59b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 19:53:17 +0000 Subject: [PATCH 3/4] test(dummies): guard the factory-naming convention Assert by reflection that every parameterless scalar factory on Any is named after the CLR type it produces, that its builder is Any{ClrType}, and that AnyContext mirrors it. The rule is derived from the surface, so no allow-list can drift; it fails on any future keyword-short name (a second Bool) that ADR-0031 forbids. Refs: #222 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019XoisyHa8b5UpezV7QV4Jd --- .../FactoryNamingConventionTests.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 Dummies.UnitTests/FactoryNamingConventionTests.cs diff --git a/Dummies.UnitTests/FactoryNamingConventionTests.cs b/Dummies.UnitTests/FactoryNamingConventionTests.cs new file mode 100644 index 00000000..7ed3435d --- /dev/null +++ b/Dummies.UnitTests/FactoryNamingConventionTests.cs @@ -0,0 +1,67 @@ +#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 — and is mirrored on . This is the guard that +/// would have caught the Bool/AnyBool deviation before release. +/// +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); + } + } + + [Fact(DisplayName = "Every type-named scalar factory on Any is mirrored on AnyContext with the same name and return type.")] + public void ScalarFactoriesAreMirroredOnAnyContext() { + foreach (MethodInfo factory in ScalarFactories()) { + MethodInfo? mirror = typeof(AnyContext).GetMethod(factory.Name, BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); + + Check.WithCustomMessage($"Any.{factory.Name}() has no parameterless AnyContext.{factory.Name}() counterpart; the seeded-context surface must mirror the ambient one.") + .That(mirror).IsNotNull(); + Check.WithCustomMessage($"AnyContext.{factory.Name}() returns {mirror!.ReturnType.Name}, not {factory.ReturnType.Name}.") + .That(mirror.ReturnType).IsEqualTo(factory.ReturnType); + } + } + +} From f2c39430063d79895047e754446f210ca0ef54d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 20:09:34 +0000 Subject: [PATCH 4/4] test(dummies): reconcile factory-naming guards with the AnyBool rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merging main brought in SurfaceParityTests (the audit's mirror + algebra parity guard, #214), which hard-codes typeof(AnyBool) — stale after this branch renamed the builder to AnyBoolean, so the test project stopped compiling. Point it at AnyBoolean, and drop the now-redundant mirror method from FactoryNamingConventionTests: SurfaceParityTests guards the Any<->AnyContext mirror more thoroughly, leaving this file to own only the CLR-naming rule ADR-0031 adds. Refs: #222 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019XoisyHa8b5UpezV7QV4Jd --- .../FactoryNamingConventionTests.cs | 17 +++-------------- Dummies.UnitTests/SurfaceParityTests.cs | 2 +- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/Dummies.UnitTests/FactoryNamingConventionTests.cs b/Dummies.UnitTests/FactoryNamingConventionTests.cs index 7ed3435d..e8bd3ace 100644 --- a/Dummies.UnitTests/FactoryNamingConventionTests.cs +++ b/Dummies.UnitTests/FactoryNamingConventionTests.cs @@ -11,8 +11,9 @@ 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 — and is mirrored on . This is the guard that -/// would have caught the Bool/AnyBool deviation before release. +/// 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 { @@ -52,16 +53,4 @@ public void FactoriesAreNamedAfterTheirClrType() { } } - [Fact(DisplayName = "Every type-named scalar factory on Any is mirrored on AnyContext with the same name and return type.")] - public void ScalarFactoriesAreMirroredOnAnyContext() { - foreach (MethodInfo factory in ScalarFactories()) { - MethodInfo? mirror = typeof(AnyContext).GetMethod(factory.Name, BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); - - Check.WithCustomMessage($"Any.{factory.Name}() has no parameterless AnyContext.{factory.Name}() counterpart; the seeded-context surface must mirror the ambient one.") - .That(mirror).IsNotNull(); - Check.WithCustomMessage($"AnyContext.{factory.Name}() returns {mirror!.ReturnType.Name}, not {factory.ReturnType.Name}.") - .That(mirror.ReturnType).IsEqualTo(factory.ReturnType); - } - } - } 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" }];