Skip to content

JustDummies: AnyEnum and AnyGuid conflict messages misname the cause when an exclusion empties the domain #314

Description

@Reefact

Problem

The same defect fixed for the four interval engines in #312 / #313 also lives in two bespoke generators that do not route through those engines: AnyEnum and AnyGuid. When an exclusion (Except/DifferentFrom/NonEmpty) empties their domain, the ConflictingAnyConstraintException names the allow-list or the pin — never the exclusion that was the actual cause — so the message is self-referential or misattributes the conflict, exactly the failure #312 describes.

AnyEnum (AnyEnum.cs:264-274)

private AnyEnum<TEnum> Validated(AnyEnum<TEnum> candidate, string applying) {
    if (candidate._pool.Count > 0) { return candidate; }

    string pool = candidate._allowedConstraint is not null
                      ? $"no value {candidate._allowedConstraint} allows remains available"   // <- self-referential
                      : candidate._combinable
                          ? $"no {typeof(TEnum).Name} combination remains available"
                          : $"no declared {typeof(TEnum).Name} member remains available";

    throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {pool}.");
}

An Except(...) that removes every value an OneOf(...) allows yields "no value OneOf(...) allows remains available" — it names OneOf, the constraint being emptied, and never Except, the constraint doing the emptying. This is the exact allow-list case #312 fixed in the interval engines.

AnyGuid (AnyGuid.cs:165-182)

if (candidate._excluded.Contains(pinned)) {
    throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate._pinnedConstraint} already pins the value to {V(pinned)}, which the exclusions forbid.");
}
...
if (candidate._effectiveAllowed is not null && candidate._effectiveAllowed.Count == 0) {
    throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no value {candidate._allowedConstraint} allows remains available.");
}

The pin branch names _pinnedConstraint and adds a generic "which the exclusions forbid" without naming which exclusion; the allow-list branch is the same self-referential "no value allows remains available" as AnyEnum.

Root cause

The same asymmetry #312 identified: both generators keep exclusions as a bare _excluded list with no provenance, while the allow-list and pin carry a constraint label. The diagnostic layer therefore has no exclusion label to name and falls back to a constraint that did not cause the emptiness.

Why it was left out of #313

#313 is scoped to the four shared interval engines (OrdinalIntervalSpec, WideIntervalSpec, DecimalIntervalSpec, ContinuousIntervalSpec), which share one DescribeExhaustion path. AnyEnum and AnyGuid have their own bespoke Validated/message code with a different shape (finite pool / single pin), so folding them in would have widened that PR past its shared-engine scope. #312 explicitly flagged them as "out of this PR … recommended as a tight follow-up".

Direction

Apply the same treatment #313 established: give each generator's exclusions a (constraint label, values) provenance, and rewrite the Validated message branches to name the excluding constraint(s) that actually emptied the pool/pin — never self-referential, never misattributed. The generation and feasibility logic is unchanged; only the diagnostic layer gains the information. Reuse the wording shape landed in #313 (<exclusion> forbids <value>, the only value <bounds> leaves for a pin; <exclusion> forbids every value <allow-list> allows for an emptied allow-list) so the two surfaces read consistently.

Acceptance criteria

  • Every exclusion-caused conflict on AnyEnum and AnyGuid names the excluding constraint; no message is self-referential or misattributed.
  • Example tests in JustDummies.UnitTests pin the message content for each shape (per ADR-0040 / WritingJustDummiesTests), confirmed red before the fix.
  • No behavioural change to generation, the pool/pin resolution, or the public API surface; existing suites stay green; build stays at 0 warnings.

Context

Surfaced by the 2026-07 JustDummies v1.0.0 readiness audit (problem #2) and split out from #312 / #313 so the interval-engine fix stayed focused. Same class of defect, different (bespoke) code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions