Skip to content

JustDummies: record and document the arbitrary-yet-valid boundary, and where it stops #308

Description

@Reefact

Problem

"Arbitrary yet valid values" is JustDummies' defining constraint, and it is not recorded in any ADR. It is asserted in JustDummies/README.nuget.md (l.3), repeated across XML docs, and used as the audit's closing formula — but none of the 16 JustDummies ADRs (0011, 0013, 0015, 0020, 0025, 0026, 0030–0033, 0035–0040) states it.

That matters because it is already a load-bearing rule, applied consistently and enforced as an input guard, not merely as an output property:

  • Floating point. ContinuousIntervalSpec.EnsureFinite (ContinuousIntervalSpec.cs:35) is called from 27 argument sites across AnyDouble, AnySingle and AnyHalf — every bound, every allow-list entry, every exclusion. Any.Double().Except(double.NaN) throws; the library refuses to discuss NaN at all. Locked in by ContinuousIntervalProperties.cs:236/:253 ("rejected as argument errors by every entry point taking a bound") and AnyModernTypeTests.cs:101.
  • Enums. AnyEnum.OneOf rejects an undeclared numeric value (AnyEnum.cs:77), locked in by AnySetTypeTests.cs:138.
  • As an admission criterion. The audit's Out of Scope list already refuses Index/Range on exactly this ground — "validity is contextual, so 'arbitrary yet valid' cannot hold standalone" (audit §10, l.877).

So the same decision has been taken at least twice in code and once as a design filter, with no record — and it is exactly the class of decision a future maintainer would question.

And the principle is not a global invariant of the library. The generic entry points do not carry it, by construction:

  • AnyOneOf<T>.FromPool (AnyOneOf.cs:39-40) validates only non-empty pool and no null element. Any.OneOf(double.NaN, 1.0) therefore compiles and yields NaN today.
  • AnyExtensions.As (AnyExtensions.cs:36) projects to anything.

That is correct — T is opaque, the library cannot judge its semantics — but an ADR that claimed a library-wide invariant would be false the day it is written. The boundary has to be stated at the level where it actually holds: the typed builders.

Impact

Three concrete costs, all already observed:

  1. The rule cannot be invoked. A proposal to add Any.Enum<T>().Undeclared(), Any.Double().WithNaN(), Any.String().NotMatching(regex) or Any.DateTime().OutOfRange() contradicts no accepted ADR. The refusal is currently an intuition, re-derived from scratch each time.
  2. A legitimate near-neighbour is indistinguishable from the refused ones. A [Flags] combination (Read|Write) is undeclared and perfectly valid — it passes the criterion, whereas an "undeclared enum member" generator does not. Without the criterion written down, the audit's AllowingCombinations() proposal and an Undeclared() proposal look like the same request.
  3. The escape hatch is undiscoverable. The one legitimate shape of the need — a domain where a non-finite double genuinely means something (NaN = "missing measurement"), or where a specific out-of-domain value is the subject of a test — is already served by Any.OneOf(...) / .As(...). Nothing says so, so users hit the ArgumentException wall and conclude the library is missing a feature.

Direction

Draft one Proposed ADR stating the boundary where it is true:

A typed builder draws, and accepts as an argument, only valid values of the domain it represents. The generic entry points (OneOf<T>, ElementOf<T>, As) carry no such guarantee: the caller's pool is the whole specification, and the library does not judge T.

with, as rationale, the two independent lines the code already supports:

  • The dummy criterion. A dummy is "a value a test needs but never asserts on" (README l.3). A value chosen for being out of domain is the subject of the assertion, however arbitrary it is within that class — arbitrariness is not insignificance. Such a value is a hostile fixture, and its correct form is a literal at the call site.
  • The engine constraint (floats, independent of doctrine). ContinuousIntervalSpec is an ordered interval model: bounds comparison, midpoint sampling (:184), and an exclusion nudge walking the representable ladder. Every comparison with NaN is false, so NaN is not one more value in the interval, it is a value outside the model. Compounding it, EqualityComparer<double>.Default.Equals(NaN, NaN) is true while NaN == NaN is false, so a NaN in a pool would dedupe under Distinct() while user code comparing with == sees two different values — the literal "sabotage arithmetic" of ContinuousIntervalSpec.cs:8.

And record the two corollaries that make the criterion usable:

  • [Flags] combinations remain admissible (a combined value is undeclared and valid) — pre-answering the audit's AllowingCombinations() proposal without pre-empting its own decision.
  • decimal has neither NaN nor infinity. System.Decimal has no such representation; AnyDecimal runs on DecimalIntervalSpec, which contains no finiteness guard because there is nothing to guard. Worth one documented sentence so nobody looks for the symmetry with AnyDouble, or files it as a gap.

User-facing documentation, in the same unit of work:

  • One paragraph in JustDummies/README.nuget.md stating the boundary and naming the escape hatch (Any.OneOf / As) for the legitimate case.
  • The same in the JustDummies user guide when it lands (audit recommendation §11.6).
  • A sentence on EnsureFinite's user-visible message path and on AnyEnum's XML docs pointing at the escape hatch, so the wall explains its own exit.

Per repo convention, an agent drafts as Proposed; @reefact accepts.

Acceptance criteria

Context

Surfaced while assessing whether Any.Enum<T>() should be able to produce an undeclared numeric value (e.g. (OrderStatus)6) for tests of defensive boundary code — the conclusion being no, but that the repository had no recorded reason to say so, and no recorded reason to keep saying so for the next such request.

Sibling structural-gap ADR issues: #216 (determinism contract), #217 (ordinal engine). Related: #228 (foundational ADR backfill, core library).

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions