Skip to content

fix(justdummies): give the values their identity, and declare what a value is - #393

Merged
Reefact merged 4 commits into
mainfrom
claude/oneof-factory-refactor-v1ci6r
Jul 31, 2026
Merged

fix(justdummies): give the values their identity, and declare what a value is#393
Reefact merged 4 commits into
mainfrom
claude/oneof-factory-refactor-v1ci6r

Conversation

@Reefact

@Reefact Reefact commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Two of the library's three values — ConstraintClaim and Replay — compared by reference despite both saying in their own remarks that they are values like every other here. ConstraintCall had its equality only because code happened to compare constraints with ==, which forced the question; nothing forced it for the other two. They get their identity, and [ValueObject] plus a reflection convention keeps the gap from reopening.

Type of change

  • Bug fix
  • Refactoring
  • Tests
  • Documentation

Changes

  • Gives ConstraintClaim and Replay a full value identity: IEquatable<T>, both Equals overloads, GetHashCode, and ==/!=.
  • Neither identity is the text alone. A claim compares its constraint alongside its wording, because a phrase reading like WithLength(3) is not the constraint WithLength(3) — only the latter can be recognised as the one being applied, which is what the blame choice in Contradicts turns on. A replay compares its guidance alongside its seed, because the same seed replays a run in full or only in part.
  • Adds [ValueObject] and ValueObjectConventionTests, which holds every marked type to sealed, immutable, IEquatable<T>, both Equals overloads, GetHashCode, and the operator pair.
  • Marks the three values. The generators and the specifications are deliberately not marked: they are immutable too, but they are recipes — two identically constrained generators are two recipes, not one value, and comparing them by content would answer a question that has no meaning for them.
  • Records ADR-0066 (English and French, indexed) for the declare-and-enforce decision.

Testing

  • dotnet build FirstClassErrors.sln — 0 warnings, 0 errors
  • dotnet test FirstClassErrors.sln — 13 test projects green
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests) — run as part of the solution above

Also run, beyond the boxes:

  • JustDummies.UnitTests — 663, up from 651: 12 new cases covering every new member. Each factory, both Equals overloads, GetHashCode, ==, !=, and each property getter is asserted directly — a getter carries no logic, so a mutation score says nothing about it.
  • dotnet build JustDummies.UnitTests -c Release -f net472 -p:EnableNet472Floor=true — the .NET Framework support floor: 0 warnings, 0 errors.
  • Mutation on ConstraintClaim.cs and Replay.cs: 100%, 30/30 killed.
  • Both new equalities were broken on purpose to confirm the tests are load-bearing: removing the constraint comparison from a claim, and the guidance comparison from a replay, reddens exactly the two cases written for them.
  • The convention was checked in both directions: a specification marked on purpose reports five violations, one line each; a field made writable on a marked type reports field '_rendered' is not readonly.

Documentation

  • README / doc/ updated — ADR-0066 in English and French, indexed
  • No documentation change required for user-facing behaviour: everything here is internal, the public API and every message are unchanged

Architecture decisions

  • New decision recorded — ADR drafted as Proposed: ADR-0066

Two things worth your eye rather than a reviewer's. Replay has no ToString() where its two peers do, and it stays that way on purpose: AnyGenerationException reads Guidance and Seed separately, so nothing renders a replay whole, and adding one would be a member nothing exercises. And the convention checks structure only — whether two equal instances hash alike, and whether the fields chosen for equality are the right ones, stay with each type's own tests. ADR-0066 states both rather than leaving them to be assumed. @Reefact

Related issues


Generated by Claude Code

claude added 4 commits July 30, 2026 23:33
Both are immutable, both are built through factories, and both say in
their own remarks that they are values like every other here — and both
answered "is this the same one?" by reference, which is the answer a
reference type gives silently when nobody writes another.

ConstraintCall carried its equality because code compared constraints
with ==, so the gap was visible there. Nothing compares two claims or two
replays today, so nothing forced the question, and it went unasked. A
value that would answer wrongly the first time it is asked is worse than
one that answers now.

The two identities are not the text alone. A claim compares its
constraint alongside its wording, because a phrase reading like
WithLength(3) is not the constraint WithLength(3) — only the latter can
be recognised as the one being applied, which is what the blame choice
turns on. A replay compares its guidance alongside its seed, because the
same seed replays a run in full or only in part.

Neither comparison nor hashing composes anything, so both stay within
ADR-0064: they cannot fail while a failure is being reported.

The generators and the specifications are deliberately left alone. They
are immutable too, but they are recipes rather than values — two
identically constrained generators are two recipes, and comparing them by
value would claim a meaning they do not have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Di41ybdgmZ8tTkfvYTjGNr
A reference type compares by identity when nobody writes another answer,
and it does so silently — no warning, no failing test, nothing for a
reviewer to catch. That is how two of this library's three values shipped
without one: only ConstraintCall had its equality, because code happened
to compare constraints with == and forced the question.

[ValueObject] makes the claim explicit and ValueObjectConventionTests
holds it to the contract by reflection: sealed, immutable, IEquatable<T>,
both Equals overloads, GetHashCode, and the ==/!= pair — the pair being
the silent half, since its absence compiles and compares references where
a missing Equals would at least be visible in the type.

It is a declaration rather than a detection, and deliberately so.
Immutability alone would sweep in the generators and the specifications,
which are immutable recipes: two identically constrained generators are
two recipes, not one value, and comparing them by value would claim a
meaning they do not have. Only a type that says it is a value is held to
the contract.

Structure is what reflection can settle, and it is the half that goes
missing; whether two equal instances really hash alike stays with each
type's own tests. The convention was checked against a specification
marked on purpose — five violations, one line each — and against a field
made writable, so neither branch passes by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Di41ybdgmZ8tTkfvYTjGNr
Drafted as Proposed, per the agent procedure: the decision is the
maintainer's to accept. It records why value-object-hood is declared
rather than detected — immutability would sweep in the generators and the
specifications, which are recipes — and why the operator pair is the
member most worth enforcing, being the only one whose absence changes
behaviour without changing whether the code compiles.

It also states what the convention deliberately does not check: whether
two equal instances hash alike, and whether the fields chosen for
equality are the right ones, are questions about a type's meaning that no
reflection over its shape can answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Di41ybdgmZ8tTkfvYTjGNr
A value that does not override ToString shows a debugger the one thing
its reader already knows — its type name — and nothing about that looks
wrong either, which is the same shape as the equality gap this convention
was written for.

The form was already settled here: [DebuggerDisplay] forwarding to
ToString, on the values in FirstClassErrors. It was followed there by
attention alone, and the three values added since did not follow it. The
convention carries it now instead of a reader.

Replay had no rendering at all. Its two peers did, and that is why the
omission read as deliberate rather than missed: their ToString is quoted
into conflict messages, so it was load-bearing and got written. Nothing
appends a replay whole — AnyGenerationException takes its guidance and
its seed separately — so nothing forced the question, and the debugger
was left with a type name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Di41ybdgmZ8tTkfvYTjGNr
@Reefact
Reefact merged commit 574a982 into main Jul 31, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants