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.
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:
AnyEnumandAnyGuid. When an exclusion (Except/DifferentFrom/NonEmpty) empties their domain, theConflictingAnyConstraintExceptionnames 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)An
Except(...)that removes every value anOneOf(...)allows yields "no value OneOf(...) allows remains available" — it namesOneOf, the constraint being emptied, and neverExcept, the constraint doing the emptying. This is the exact allow-list case #312 fixed in the interval engines.AnyGuid(AnyGuid.cs:165-182)The pin branch names
_pinnedConstraintand 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" asAnyEnum.Root cause
The same asymmetry #312 identified: both generators keep exclusions as a bare
_excludedlist 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 oneDescribeExhaustionpath.AnyEnumandAnyGuidhave their own bespokeValidated/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 theValidatedmessage 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> leavesfor a pin;<exclusion> forbids every value <allow-list> allowsfor an emptied allow-list) so the two surfaces read consistently.Acceptance criteria
AnyEnumandAnyGuidnames the excluding constraint; no message is self-referential or misattributed.JustDummies.UnitTestspin the message content for each shape (per ADR-0040 /WritingJustDummiesTests), confirmed red before the fix.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.