Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ section is the GitHub release's notes.
- Generators: a generated member you wrote where a value object reaches it now fails the build (CMTK1011, CMTK1008 for comparison), so your build can fail when you upgrade: an explicit implementation such as `IFormattable.ToString` in an interface a value object or closed set implements, one in a base record from another project, a base record's public `MinValue` or `MaxValue`, which stood in for the generated one, and a base record's `Create` beside your own. Remove it, don't implement that interface, or declare the marker on the value object itself. A base record's public `Value`, `Parse`, `ToString(string?, IFormatProvider?)` and the like, and `IConvertible` through an interface, now fail with a CMTK error rather than a Metalama crash ([Write your own members](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members)).
- Generators: an `Equals` a value object declares for one of its base records' types, such as `Equals(DeepBase?)`, now fails the build (CMTK1011), so your build can fail when you upgrade. A `HashSet<DeepBase>` compared by it while `==` compared the underlying value; normalise values in `Create` instead ([Equality](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#equality)).
- Generators: a `CompareTo(TSelf)`, `CompareTo(object)`, `TryFrom` or `Revalidate` a value object inherits from a base record now fails the build (CMTK1008, CMTK1011), so your build can fail when you upgrade. It stood in for the generated member: an inherited `CompareTo` made `<` overflow the stack and an EF Core query order by the column, and an inherited `TryFrom` need not run `Create`. Declare it on the value object ([Write your own members](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members)).
- Generators: a closed set over a base class that declares a member the generators add now fails the build with CMTK1011 naming it, where it failed with a Metalama error that named nothing you wrote: its `Parse`, `TryParse`, `Match` or `All`, and an `IParsable<TSelf>` it implements. A base class's `ToString()` that isn't sealed now fails the build too (CMTK1011), so your build can fail when you upgrade: the generated `ToString()` replaced it without a word. Declare `ToString()` in the set, or seal the base's, which is then kept ([Closed sets](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#closed-sets)).
- Generators: `IValueObject<TSelf, T>`, `IValueObjectMaterializer<TSelf, T>` or `IPlainValueObject<TSelf, T>` declared through an interface of yours, such as an `IEntityId<TSelf> : IValueObject<TSelf, Guid>`, or by a base record now fails with CMTK1011 naming that interface, where it failed with a Metalama error (LAMA0512) that named nothing you wrote. To constrain generic code, use `IValueObject<TSelf, T>` itself ([Write your own members](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Generators/README.md#write-your-own-members)).

## 2.3.0 — 2026-10-29
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ Context: [#191](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/191
operators, where an interface's explicit implementation is kept, from an outside review of that pull request;
widened the same day by [#195](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/195),
[#197](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/197) and
[#194](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/194), from a second outside review, and by
[#192](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/192) before 2.3.1 was tagged; refines
[#194](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/194), from a second outside review, by
[#192](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/192), and by
[#205](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/issues/205) from a sweep, before 2.3.1 was tagged; refines
[decision 28](0028-seams-declared-everything-else-refused.md), and is an exception to
[decision 34](0034-errors-fixed-within-major-version-warnings.md), taken in a patch release. Decision 28 refuses a member the
generators introduce written by hand, and an explicit implementation of an interface they implement, on the value
Expand Down Expand Up @@ -65,6 +66,13 @@ generated by unchanged since 2.3.0 (`v2.3.0`, edeb90c), which 2.3.1 patches:
Guid>` written to constrain generic code (#192). So did a closed set's `IParsable<TSelf>` declared through an interface (LAMA0512). A
closed set's interface that implements `IClosedSet<TSelf>.Name` or `TryParse` explicitly was replaced by the
generated members without a word.
- **A closed set's base class counted for nothing** (#205, measured on `release/2.3.1` at `dbdd44e`).
`ClosedSetTypes` read the set's own members. A base class's `Parse(string)`, `Parse(string, bool)`,
`TryParse(string?, bool, out TSelf)`, `Match`, a static `All`, a protected `_name`, an `IParsable<TSelf>` it
implements, and its `sealed` `ToString()` failed the aspect (LAMA0500, LAMA0502, LAMA0503, LAMA0512), in this
project and from another. Its `ToString()` that is not sealed was replaced by the generated one without a word,
where a value object's base record's is CMTK1011. A base's private `_name` and `_all`, and a `[JsonConverter]` on
it, built and still do.
- **An interface's `==`, `!=` and `IEquatable<T>.Equals` are replaced by the record's own**, in a plain record
as in a value object, so the generators change nothing there. For the same reason
`ValueObjectContractAspect.ExplicitlyImplementsEquatableBetween` has no gap: a base record's synthesized
Expand Down Expand Up @@ -97,6 +105,12 @@ never as an explicit implementation. A closed set's `IClosedSet<TSelf>` is held
- **A base record's `Create` counts.** One the value object reaches through the marker, explicit or beside its own,
is CMTK1011, which says to declare the marker on the value object itself. A base's public `Create` alone is one
rule set, which the generated code calls too, and builds.
- **A closed set's base class counts as a value object's base record does** (#205). Its members of a generated
name or signature, other than private ones, and an `IParsable<TSelf>` it implements are CMTK1011, naming the
base. Its `sealed` `ToString()` is the seam; one that is not sealed is CMTK1011, which says to declare
`ToString()` in the set or seal the base's. Taking an unsealed one as the seam was the alternative: C# would keep
it in a derived class, but the set has printed its member's name over it since closed sets shipped, and taking it
would change what such a set prints in a patch, without a word.
- **`IConvertible`, the three contracts and a set's `IParsable<TSelf>` declared through an interface or by a base
record** are CMTK1011, naming the interface or the base, where the aspect failed. Accepting the contracts through
an interface, which the generated `Value` would implement, was the alternative; it would add behaviour to a patch
Expand All @@ -112,8 +126,9 @@ never as an explicit implementation. A closed set's `IClosedSet<TSelf>` is held
**The exception to decision 34.** A build that worked on 2.3.0 can fail on 2.3.1: a value object or a closed set
that reaches an interface's explicit implementation of a generated member, or a base record's in another
assembly; a value object over a base record whose public `MinValue` or `MaxValue` stood in for the generated one;
one whose generic `T.Create` reached a base's `Create` beside its own; and one over a base record that declares
`CompareTo(TSelf)`, `TryFrom` or `Revalidate`. Each is code that never runs (an
one whose generic `T.Create` reached a base's `Create` beside its own; one over a base record that declares
`CompareTo(TSelf)`, `TryFrom` or `Revalidate`; and a closed set over a base class whose `ToString()` is not sealed,
whose generated `ToString()` replaced the base's. Each is code that never runs (an
interface's default the generated member replaced) or a second answer beside the generated one (a base's kept
member), in every build that contains it, so the error names a defect, never correct code. The defect shipped in
2.3.0, so its fix ships in the patch 2.3.1, not in a minor: waiting would keep a wrong answer in every 2.3 build,
Expand All @@ -127,5 +142,7 @@ interface, a record class over a reference type, and from another assembly (`IPr
`IProbeOrders`, `ProbeFormattableRecord` and `ProbeMinMaxRecord` in `CodoMetis.TypeKit.Generators.Probes`); a
base record's public member of each kind, in this project and in a library the generators do not run in, which
the fixture builds beside its consumers; a base record's `Create`; a base record's seams, and its `FromKnownGood`, which builds; a closed set through an interface of either
assembly and over the library's base class; and the cases that build: a `ToString()` seam, a `Uri`'s missing
ordering, an interface's `==`, an `INumberBase<TSelf>` value object, and one `Create` over a base record.
assembly and over the library's base class; a closed set over a base class of either assembly that declares each
generated member (#205); and the cases that build: a `ToString()` seam, a `Uri`'s missing
ordering, an interface's `==`, an `INumberBase<TSelf>` value object, one `Create` over a base record, and a closed set over a base class's
`sealed` `ToString()`, private fields or `[JsonConverter]`.
2 changes: 1 addition & 1 deletion design/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,4 @@ per area ([design](../plan.md)) link to the records they rest on.
| [0118](0118-an-error-may-learn-a-form-only-new-api-can-write.md) | An Error may report, in a minor version, a form that only an API new in that version can write, since no build that worked before contains it: CMTK0003's dropped assertion of `CodoMetis.TypeKit.Testing` on a `Task` or `ValueTask` in 2.3.0, its `ConfigureAwait`, `WaitAsync` and branch forms (#152) included, known by the package's assembly; the forms freeze at the tag; refines decision 34; one exception the maintainer took into 2.3.0, a dropped `Task.WhenAny` of tasks of results (#159) | 2026-10-29 | accepted; widened by [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) |
| [0119](0119-a-gap-found-before-the-tag-ships-in-that-release.md) | A form an Error misses, found before its version is tagged, is reported in that version when it loses what the rule protects in every build, though code built on the previous minor can contain it: 2.3.0's CMTK0003 on a `Task.WhenAny` of results (#159), `Wait()` on a branch (#160), an array in `Task.WaitAll`/`WaitAny` (#161), a dropped `Task.WhenEach` (#162), and a `WhenAny` of held tasks silent; the rule stops at the tag, after which decision 34 holds; refines 34 and 118 | 2026-10-29 | accepted |
| [0120](0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md) | A `net11.0` target joins a 2.x minor when a feature needs it, and a dependency floor may rise in a minor, on every target or on `net11.0` alone, if it is documented (the Semantic Versioning FAQ calls a dependency update with an unchanged public API compatible; a project that references the dependency lower gets NU1605, measured on SDK 11 RC1, so the changelog names the floor, who it reaches and both ways out, and the smoke test shows it); a union new on `net11.0` is an addition and may join a 2.x minor (a `LangVersion` 14 consumer who matches on it gets CS9327, and on Metalama 2026.1 CS8652, measured); Metalama is one version for every target and TypeKit follows the maintained Metalama year in a minor: 2.3 and 2.4 stay on 2026.1 and 2.5.0 moves both targets to 2027.0 once stable (a direct 2026.1 reference gets NU1605 and stays on 2.4.x; a woven `net11.0` project goes from C# 14 to 15; conditions: a license-free CI and clean-container build, the ceiling and the 2.4.x line documented); the SDK 11 spike comes first (#170); 3.0.0 stays the `net10.0` drop and a change of an existing contract, timed to .NET 12 LTS, and .NET 11 is no reason for a major; refines decision 44 | 2026-10-29 | accepted |
| [0121](0121-a-generated-member-is-refused-wherever-the-value-object-reaches-it.md) | CMTK1011, and CMTK1008 for comparison, refuse a generated member written by hand wherever a value object or closed set reaches it: on it, in a base record, public or explicit, or as an interface's explicit implementation, in any assembly, found by the interface mapping since Metalama lists no private member of another assembly's type; a base record's `Create` beside the value object's own (#195) and a seam a base record declares (#194) too; `IConvertible`, `IValueObject<,>`, `IValueObjectMaterializer<,>`, `IPlainValueObject<,>` and a set's `IParsable` declared through an interface or by a base record are CMTK1011 where the aspect crashed (#192); an interface's `==` and .NET's own `INumberBase` bridges are not refused; an exception to 34 taken in the patch 2.3.1 (#191, #192, #194, #195, #197) | 2026-10-30 | accepted |
| [0121](0121-a-generated-member-is-refused-wherever-the-value-object-reaches-it.md) | CMTK1011, and CMTK1008 for comparison, refuse a generated member written by hand wherever a value object or closed set reaches it: on it, in a base record, public or explicit, or as an interface's explicit implementation, in any assembly, found by the interface mapping since Metalama lists no private member of another assembly's type; a base record's `Create` beside the value object's own (#195) and a seam a base record declares (#194) too; `IConvertible`, `IValueObject<,>`, `IValueObjectMaterializer<,>`, `IPlainValueObject<,>` and a set's `IParsable` declared through an interface or by a base record are CMTK1011 where the aspect crashed (#192); a closed set's base class counts as a base record does, its `sealed` `ToString()` the seam and one not sealed refused (#205); an interface's `==` and .NET's own `INumberBase` bridges are not refused; an exception to 34 taken in the patch 2.3.1 (#191, #192, #194, #195, #197, #205) | 2026-10-30 | accepted |
4 changes: 3 additions & 1 deletion design/aspects.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@
member of a type in another assembly, so a base record's from a referenced project was missed as well.
`IConvertible`, `IValueObject<,>`, `IValueObjectMaterializer<,>` and `IPlainValueObject<,>` declared through an
interface or by a base record, and a closed set's `IParsable<TSelf>` through an interface, are CMTK1011 where
they failed the aspect (LAMA0041, LAMA0512, #192). Not refused: an interface's `==`, which the record's own
they failed the aspect (LAMA0041, LAMA0512, #192). A closed set's base class counts as a base record does: its
members of a generated name or signature and its `IParsable<TSelf>` are CMTK1011, its `sealed` `ToString()` is
the seam and one not sealed is CMTK1011 (#205). Not refused: an interface's `==`, which the record's own
replaces anyway, and `INumberBase<TSelf>`'s UTF-8 bridges.
- **Equality over a base record is generated (decided 2026-10-14, [decision 75](adr/0075-equality-over-base-record-generated.md)).**
A derived record's synthesized equality calls the base's, which compares the base's fields and
Expand Down
2 changes: 1 addition & 1 deletion design/closed-sets.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The consumer's guide is [docs/closed-sets.md](../docs/closed-sets.md).
- **Generated:** `Name`, `QualifiedName` (the nesting chain with dots, `Order.Status.Draft`), `All`
in declaration order, `Match` with one required delegate per member, `TryParse`/`Parse` (ordinal,
each with an `ignoreCase` overload), `IParsable<TSelf>` explicitly, `ToString()` unless written by
hand (the one seam), a nested JSON converter and type converter, `GeneratedClosedSetAttribute<TSet>`
hand (the one seam; a base class's `sealed` one counts, one not sealed is refused), a nested JSON converter and type converter, `GeneratedClosedSetAttribute<TSet>`
for run-time discovery, and a private constructor where none is declared.
- **Members are singletons.** Equality is reference equality, `Parse` answers the member itself, and
nothing outside the type can make an instance (CMTK1016, CMTK1017). A record is refused (CMTK1020):
Expand Down
8 changes: 8 additions & 0 deletions src/CodoMetis.TypeKit.Generators/ClosedSetAspect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ public override void BuildAspect(IAspectBuilder<INamedType> builder)
foreach (var (member, earlier, reason) in ClosedSetTypes.CaseCollisions(members))
problems.Add(ClosedSetMemberNameTaken.WithArguments((target, member.Name, reason.Replace("{0}", $"'{target.Name}.{earlier.Name}'"))));

// A base class's ToString() that is not sealed was replaced by the generated one without a word (#205),
// as a value object's base record's was. A sealed one is the seam (DeclaresToString).
if (ValueObjectDeclaration.UnsealedInheritedToString(target) is { } inheritedToString)
problems.Add(HandWrittenGeneratedMember.WithArguments((target,
$"{inheritedToString.ToDisplayString()} in its base type, which is not sealed",
$"its generated ToString() replaces it and prints the member's name; declare ToString() in {target.Name} (it may return base.ToString()), "
+ "or seal the base's, which C# keeps in every derived class and the generators then keep too")));

// A hand-written member the generators introduce failed the aspect with a Metalama error that
// names no fix; a hand-written ToString() is the seam and is kept instead.
var declaredByHand = ClosedSetTypes.GeneratedMembersDeclaredByHand(target, members);
Expand Down
Loading
Loading