Skip to content

fix: refuse a seam a base record declares in place of the value object - #203

Merged
CaffeinatedCoder merged 2 commits into
release/2.3.1from
fix/inherited-seam-refused-2-3-1
Oct 30, 2026
Merged

CaffeinatedCoder merged 2 commits into
release/2.3.1from
fix/inherited-seam-refused-2-3-1

Conversation

@CaffeinatedCoder

@CaffeinatedCoder CaffeinatedCoder commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #194

Problem

A base record that declares a seam stood in for the generated member without a word:

public abstract record ReversedBase<TSelf> where TSelf : ReversedBase<TSelf>
{
    public int CompareTo(TSelf? other) => /* the reverse order */;
}

public sealed partial record Priority : ReversedBase<Priority>, IValue<int>;

new[] { Priority.From(1), Priority.From(2) }.Order();   // [2, 1]
db.Jobs.OrderBy(j => j.Priority);                      // SQLite: [1, 2], not refused
Priority.From(1) < Priority.From(2);                   // stack overflow, the process ends
  • ValueObjectComparableAspect.DeclaresCompareTo read the value object's own methods, so the inherited seam got no [HandWrittenOrdering], and EF Core ordered by the column.
  • The generated CompareTo(TSelf) was not introduced, because OverrideStrategy.Ignore saw the inherited one.
  • The generated operators and CompareTo(object) call left.CompareTo(right). C# binds that to the generated CompareTo(object), declared on the more derived type, so it called itself.
  • A base's TryFrom(T) and Revalidate() were the value object's the same way, and the generated ones, which run Create, were not introduced.
  • A base's CompareTo(object) failed the aspect (LAMA0500).

Measured on release/2.4.0 and again on #201's branch, the code unchanged since 2.3.0.

Change

  • HandWrittenComparisonMembers reports a base record's CompareTo(TSelf) and CompareTo(object) as CMTK1008. A member that the interface mapping also finds is named once.
  • ValueObjectDeclaration.InheritedSeams reports a base's TryFrom(T) and Revalidate() on a validated value object as CMTK1011. The remedy says to declare it on the value object, calling the base's if that is what's meant.
  • A base's FromKnownGood(T) stays allowed. Its signature differs from the generated one, which takes the caller's expression too and hides it.
  • Decision 121 is widened. It records why refusing wins over taking a base's as the seam: that would need the generated operators to call it past C#'s overload resolution, and the ordering marker to follow it, for a seam the value object can declare in one line. A base record's sealed ToString() stays the seam.
  • The Generators README ("Write your own members"), design/aspects.md, the 2.3.1 changelog and claim C22-a-seam-in-a-base-record follow.

Tests

BuildOutcomeTests (285):

  • What_a_base_record_declares_is_refused_by_name gains the four seams in a base record.
  • A_seam_or_a_foreign_bound_is_not_refused gains a base's FromKnownGood, which builds.
  • A_base_member_found_twice_is_named_once holds the Distinct.

Reverted (verify-the-guard):

Seed Fails
The source back to #201's tip (03e199b) 6: the 4 seam rows, plus Every_error_is_one_of_the_intended_ones and Nested_value_objects_... (LAMA0500)
Skip the base records in HandWrittenComparisonMembers 4: the two CompareTo rows, plus the LAMA guards
InheritedSeams returns none 2: the TryFrom and Revalidate rows
Count a base's FromKnownGood as a seam 3: the FromKnownGood control, plus the compile-error guards
Drop the Distinct 1: A_base_member_found_twice_is_named_once

Variants. Input class GTS: a seam by where it's declared: each seam in a base record, beside the value object's own seams the suite already holds.

Built on #201 (merged into release/2.3.1 as 8fac39b), whose helpers and build-outcome library fixture it uses.

🤖 Generated with Claude Code

A base record's CompareTo(TSelf) was not seen as the comparison seam:
DeclaresCompareTo read the value object's own methods, so no
[HandWrittenOrdering] was put on it and an EF Core query ordered by the
column ([1, 2]) where memory ordered [2, 1]. The generated
CompareTo(TSelf) was not introduced, since OverrideStrategy.Ignore saw
the inherited one, and the generated operators and CompareTo(object)
called left.CompareTo(right), which C# binds to the generated
CompareTo(object), declared on the more derived type: < overflowed the
stack and ended the process. A base's TryFrom(T) and Revalidate() were
the value object's in the same way, and the generated ones, which run
Create, were not introduced. A base's CompareTo(object) failed the
aspect (LAMA0500).

HandWrittenComparisonMembers reports a base record's CompareTo(TSelf)
and CompareTo(object) (CMTK1008), named once where the interface
mapping finds the second too. ValueObjectDeclaration.InheritedSeams
reports a base's TryFrom(T) and Revalidate() on a validated value
object (CMTK1011), whose remedy says to declare it on the value object.
A base's FromKnownGood(T) is another signature than the generated one,
which takes the caller's expression too and hides it, so it stays
allowed. Decision 121 records the choice over taking a base's as the
seam.

Input class (GTS): a seam, by where it is declared. Catalogue: each seam
in a base record (CompareTo(TSelf), CompareTo(object), TryFrom,
Revalidate, FromKnownGood), beside the value object's own seams the
suite already holds. A base's sealed ToString() stays the seam.

Reverting the source to 03e199b fails 6 of 285 BuildOutcomeTests; each
guard's own seed fails only its rows, the named-once check included.

Refs #194

Defect-Class: GTS
Found-By: OR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 2.3.1 milestone Oct 30, 2026
@CaffeinatedCoder CaffeinatedCoder added bug Something isn't working silent Compiles or answers and does less than it appears to stacked Base is another open PR; merge in order, CI does not run until it targets a release severity/high Wrong results, a crash or a failed build in a realistic scenario package/generators Affects CodoMetis.TypeKit.Generators area/value-objects Value-object contracts and what the aspects generate for them labels Oct 30, 2026
Base automatically changed from fix/interface-explicit-implementation-refused-2-3-1 to release/2.3.1 October 30, 2026 17:31
@CaffeinatedCoder CaffeinatedCoder removed the stacked Base is another open PR; merge in order, CI does not run until it targets a release label Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/value-objects Value-object contracts and what the aspects generate for them bug Something isn't working package/generators Affects CodoMetis.TypeKit.Generators severity/high Wrong results, a crash or a failed build in a realistic scenario silent Compiles or answers and does less than it appears to

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant