From 9f4fcc3e962ecac50cd82356a77274b53b5831e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 16:02:41 +0000 Subject: [PATCH] fix(justdummies): qualify the Combine replay hint across mixed sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a composer failed inside a Combine, the replay hint promised a full deterministic replay whenever every operand was individually reproducible — even when the operands drew from different seeded sources. Combine reports the first operand's source, so a mix of a WithSeed context and the ambient source (or two different WithSeed contexts) named one seed and claimed it replays the whole run, when it reproduces only the operands that draw from it. The flagship promise — a run is reproducible from a reported seed — was overstated exactly where the seed cannot deliver it. Gate the full-replay promise on every operand drawing from the one reported source, not merely on each being reproducible: DrawsOnlyFrom(g, source) = IsReproducible(g) && ReferenceEquals(SourceOf(g), source). A combine whose operands do not all share the reported source is now qualified ("not reproducible from this seed alone"), the same wording a foreign operand already produces. Same-ambient and single-WithSeed-context combines stay full; the only new qualification is a genuine source mix (and, conservatively, two separate WithSeed contexts of the same seed — safe under-promising). Internal to the derivation plumbing; no public API change. Refs: #319 --- JustDummies.UnitTests/CompositionTests.cs | 19 +++++++++++++++++++ JustDummies/Any.cs | 14 +++++++------- JustDummies/AnyDerivation.cs | 12 ++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/JustDummies.UnitTests/CompositionTests.cs b/JustDummies.UnitTests/CompositionTests.cs index b6e00108..e7b54c48 100644 --- a/JustDummies.UnitTests/CompositionTests.cs +++ b/JustDummies.UnitTests/CompositionTests.cs @@ -155,6 +155,25 @@ public void CombineOverMixedForeignAndLibraryQualifiesTheHint() { Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); } + [Fact(DisplayName = "A composer failure over a Combine mixing two different seeded sources does not promise a full replay from one seed.")] + public void CombineOverMixedSeededSourcesQualifiesTheHint() { + // The first operand draws from Any.WithSeed(4242); the second from the ambient source. The composed value + // depends on BOTH, so replaying WithSeed(4242) alone reproduces only the first — the hint must not promise a + // deterministic full replay from that one seed (issue #319). + IAny generator = Any.Combine( + Any.WithSeed(4242).Int32().Between(1, 3), + Any.Int32().Between(4, 6), + (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); + + AnyGenerationException caught = Assert.Throws(() => generator.Generate()); + + Check.That(caught.Message).Contains("Combine(...)"); + Check.WithCustomMessage($"The hint over-promised a full replay. Message: {caught.Message}") + .That(caught.Message).Not.Contains("already replays deterministically"); + Check.WithCustomMessage($"The hint did not qualify the replay promise. Message: {caught.Message}") + .That(caught.Message).Contains("not reproducible from this seed alone"); + } + [Fact(DisplayName = "Combine composes four through eight parts, passing every constrained part to the lambda.")] public void CombineSupportsHigherArities() { IAny part = Any.Int32().Between(1, 9); diff --git a/JustDummies/Any.cs b/JustDummies/Any.cs index c509326f..28e2972e 100644 --- a/JustDummies/Any.cs +++ b/JustDummies/Any.cs @@ -605,7 +605,7 @@ public static IAny Combine(IAny first, IAny se if (compose is null) { throw new ArgumentNullException(nameof(compose)); } RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -636,7 +636,7 @@ public static IAny Combine(IAny first, IAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -670,7 +670,7 @@ public static IAny Combine(IAny first, IAn if (compose is null) { throw new ArgumentNullException(nameof(compose)); } RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -708,7 +708,7 @@ public static IAny Combine(IAny first, if (compose is null) { throw new ArgumentNullException(nameof(compose)); } RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -750,7 +750,7 @@ public static IAny Combine(IAny fi if (compose is null) { throw new ArgumentNullException(nameof(compose)); } RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth) && AnyDerivation.IsReproducible(sixth); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source) && AnyDerivation.DrawsOnlyFrom(sixth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -798,7 +798,7 @@ public static IAny Combine(IAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -851,7 +851,7 @@ public static IAny Combine(IAn if (compose is null) { throw new ArgumentNullException(nameof(compose)); } RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth) ?? AnyDerivation.SourceOf(seventh) ?? AnyDerivation.SourceOf(eighth); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth) && AnyDerivation.IsReproducible(sixth) && AnyDerivation.IsReproducible(seventh) && AnyDerivation.IsReproducible(eighth); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source) && AnyDerivation.DrawsOnlyFrom(sixth, source) && AnyDerivation.DrawsOnlyFrom(seventh, source) && AnyDerivation.DrawsOnlyFrom(eighth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); diff --git a/JustDummies/AnyDerivation.cs b/JustDummies/AnyDerivation.cs index 9f0ced3e..c91711dd 100644 --- a/JustDummies/AnyDerivation.cs +++ b/JustDummies/AnyDerivation.cs @@ -63,6 +63,18 @@ internal static bool IsReproducible(IAny generator) { return SourceOf(generator) is not null; } + /// + /// Whether is reproducible and draws from + /// specifically — the per-operand condition for a Combine's full-replay promise. An operand that is + /// individually reproducible but draws from a different seeded source (a second + /// context, or the ambient source alongside a fixed one) leaves the reported seed covering only part of the run, + /// so naming it as a deterministic full replay would over-promise. When the operands do not all draw from the one + /// reported source, the hint is qualified instead — exactly as it is for a foreign operand. + /// + internal static bool DrawsOnlyFrom(IAny generator, RandomSource? source) { + return IsReproducible(generator) && ReferenceEquals(SourceOf(generator), source); + } + /// /// A conservative upper bound on the number of distinct values yields, when it /// advertises one through ; null when the domain is unbounded or unknown.