Skip to content
Open
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
77 changes: 77 additions & 0 deletions design/adr/0129-metalama-2027-0-moves-into-2-4-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 0129. Metalama 2027.0 moves into 2.4.0, so a consumer with only the .NET 11 SDK builds

Date: 2026-10-31
Status: accepted

Context: 2026-10-31, issue #280; replaces the timing in
[decision 120](0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md)'s Metalama paragraph ("2.3 and 2.4
stay on 2026.1, and 2.5.0 moves both targets to 2027.0"). Decision 120 named the cost of holding 2026.1 on SDK 11 as
LAMA0617, "a warning, falling back to the SDK 10 Razor compiler". The RC3 spike (`spikes/Sdk11Rc3Metalama`,
[KB-060](../knowledge/KB-060-metalama-2026-1-on-the-net-11-sdk.md)) found that the fallback exists only when an SDK 10
is installed beside SDK 11: without one, a web project with the Generators package fails with LAMA0625 (#275). The
maintainer decided, in these words: "I want a 2.4.0 that compiles at every .NET11-consumer, even when he has only
.NET 11 SDK installed. If this doesn't work with Metalama 2026, then we need to take a straight decision", and
accepted the proposal below the same day.

## Decision

**The Generators package moves to Metalama 2027.0 in 2.4.0, for the `net10.0` and `net11.0` assets alike.** Every
other rule of decision 120 stands: one Metalama version for every target, the floor raise documented under its
first rule, and the five conditions of its Metalama paragraph, which are now 2.4.0's. Where decision 120 says 2.5.0
of the move, read 2.4.0, and where it says the 2.4.x line, read 2.3.x.

**2.4.0 is tagged only on a stable Metalama 2027.0.** A stable package does not depend on a prerelease (NU5104,
an error under this repository's warnings as errors), so 2.4.0's date is Metalama's: 2027.0 is planned for
2027-01-01 in Metalama's internal documents (#173), with no public date on 2026-10-31. A fix a consumer needs before
then ships as a 2.3.x patch.

**One condition is added: a build with only SDK 11 installed.** CI and the consumer smoke test get a lane whose
.NET installation holds SDK 11 and nothing else, with a web consumer (`Microsoft.NET.Sdk.Web`) on the packages,
because that is the case Metalama 2026.1 fails and the one a machine with SDK 10 beside it never shows. It runs on
the general availability SDK before the tag (#281).

**2.3.x is the line below the ceiling.** A project that needs Metalama 2026 stays on 2.3.x. On a machine with only
SDK 11, its web projects need SDK 10 installed beside SDK 11, or a direct `Metalama.Framework` 2027 reference beside
the 2.3.x packages, which weaves (KB-060, decision 120); #275 says so, and 2.4.0's changelog names both.

Refused:

- **Staying on 2026.1 in 2.4.0 and documenting SDK 10 beside SDK 11.** It fails the requirement, on `net10.0` as on
`net11.0`: the Razor compiler is the SDK's, whatever the target (KB-060).
- **A 2.4.0 without `net11.0`, on 2026.1.** The same: a `net10.0` web project fails on SDK 11 alone.
- **A 2.4.0 on 2027.0-preview.** A stable package on a prerelease dependency (above).
- **Waiting for a 2026.1 patch that supports SDK 11's Roslyn.** Not in this repository's hands, and 2026.1.31
(compiler 2026.1.21) still fails.

## Evidence

[KB-060](../knowledge/KB-060-metalama-2026-1-on-the-net-11-sdk.md), measured 2026-10-31 on macOS arm64 with the
published 2.3.1 packages:

| Web project | Metalama 2026.1 | Metalama 2027.0.5-preview |
|---|---|---|
| SDK 11 RC3 alone, `net11.0` | LAMA0625 (2026.1.31 too) | builds |
| SDK 11 RC3 alone, `net10.0` | LAMA0625 | builds |
| SDK 11 RC1 alone | LAMA0625 | builds |
| SDK 11 RC3 with SDK 10.0.403 beside it | builds, LAMA0617 twice | builds |
| SDK 10.0.403, `net10.0` | builds | builds |

A console project builds on 2026.1 in every case, so the failure is the web SDK's Razor compiler, which needs Roslyn
5.11 (RC1) or 5.12 (RC3) where Metalama 2026.1 bundles 5.9. With 2027, one project on `net10.0;net11.0` and no
`LangVersion` compiles `net10.0` at C# 14.0 and `net11.0` at 15.0, the value object is generated on both, and a union
under `#if NET11_0_OR_GREATER` or in a file removed from `net10.0` builds on both targets. This machine has a Metalama
license entry for 2027.0, so these runs say nothing about a build without one; decision 120's clean-container run of
2027.0.4-preview did, and the CI condition stays.

## Consequences

- The roadmap's Metalama item moves to 2.4.0. #173 (the move), #272 (a union over value objects in a woven project)
and #223 (what the generators do with a union or a `closed` class) move from the 2.5.0 epic to the 2.4.0 epic:
the release that makes a woven project compile those shapes defines what they do, since a value object over a union
that compiles with no defined answer is the silent failure this repository refuses. 2.5.0 keeps what 2027.0 offers
beyond that: AspectTesting on xunit.v3 (#220), the aspect-author analyzers (#221), type facets (#222) and the
platform requirement (#224).
- Decision 120's rule 2 sentence "until 2.5.0 a union member reaches the consumers without the Generators package"
ends at 2.4.0: from 2.4.0 a woven `net11.0` project compiles at C# 15, so #209 and #210 reach every consumer.
- #275 closes when 2.4.0 ships on 2027.0; #281 is the lane.
- Decision 120's status and index row name this record.
3 changes: 2 additions & 1 deletion design/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ per area ([design](../plan.md)) link to the records they rest on.
| [0117](0117-a-seed-is-caught-against-a-passing-baseline.md) | `oracles seeds` credits a seed only when its oracle passes without it, in a baseline run with the same build and filter, and fails with it in a test of its own; a run that never reached the tests (a fixture xunit could not set up, an error counted outside any test, a crashed host, no TRX, every test skipped) or a failing baseline is `ERROR`, exit 4, never `CAUGHT` | 2026-10-29 | accepted |
| [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 |
| [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; its Metalama timing superseded by [0129](0129-metalama-2027-0-moves-into-2-4-0.md) |
| [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; refined by [0128](0128-one-signature-identity-and-a-mark-on-every-generated-member.md) |
| [0122](0122-arithmetic-operators-are-opt-in-and-a-hand-written-one-is-a-seam.md) | A plain value object gets the arithmetic operators (`+`, `-`, `*`, `/`, `%`, unary `-` and `+`, and their `checked` forms) of the operator interfaces it declares, where the wrapped type implements the same interface with the value object replaced by itself; each calls the wrapped type's through `GeneratedArithmetic`, a constrained generic call with its context stated; never by default and never on a validated value object, which keeps CS0535; a hand-written operator for the same operands, or an explicit implementation of the interface, its own or a base record's, is a seam, kept with its `checked` form, so 2.3 code builds unchanged; refines decision 28 | 2026-10-30 | accepted; refined by [0123](0123-a-hand-written-operator-is-refused-in-a-query.md) and [0124](0124-an-operator-in-a-query-is-the-wrapped-type-s-arithmetic.md) |
| [0123](0123-a-hand-written-operator-is-refused-in-a-query.md) | An arithmetic, bitwise or shift operator a value object declares by hand is refused in an EF Core query, naming the value object and `.Value`, by an interceptor and, for query filters, when the model is built; the generated ones carry `[TranslatedAsWrappedArithmetic]`, which is what tells them apart; a conversion operator is left to EF; refines decision 122 | 2026-10-30 | accepted |
Expand All @@ -135,3 +135,4 @@ per area ([design](../plan.md)) link to the records they rest on.
| [0126](0126-parallel-work-runs-in-worktrunk-worktrees-beside-the-repository.md) | Parallel work runs in worktrunk worktrees at `../CodoMetis.TypeKit-worktrees/<branch>`, set per machine in worktrunk's user config; worktrunk's default branch is the open `release/<version>`, set per clone, so `wt switch -c`, the plugin's agent worktrees, `wt remove` and `wt step prune` base on it; the Bash guard refuses `wt merge` and `wt step squash`, `rebase` and `push`, since work lands by a PR and a merge commit (decision 94); nothing is copied into a new worktree, which builds the solution cold in 27 s; `.claude/worktrees/` joins `.gitignore`; [design/worktrees.md](../worktrees.md) is the runbook | 2026-10-31 | accepted |
| [0127](0127-a-change-to-tools-or-tests-runs-the-test-projects-it-reaches.md) | A change whose paths are prose, the ignore list or inside solution projects under `tools/` and `test/` runs the projects it reaches (`suite=reach`, written as `test/Reach.slnf`): a project reaches its directory, what its items and imports name and what its references reach, and a run-time read by path is declared with `<ReadsByPath>` (the conventions the root, the oracle tests `test/`), which `TestReachTests` holds against MSBuild's evaluation and the sources' literals; anything else, or anything unreadable, runs the whole solution; `.gitignore` is Linux alone; the gate refuses a reached suite with no projects, the pack or macOS; amends decision 113 | 2026-10-31 | accepted |
| [0128](0128-one-signature-identity-and-a-mark-on-every-generated-member.md) | Every check of a hand-written member asks one identity, C#'s: a method of the generated one's name, arity, parameter types without nullable annotations (`Nullable<T>` its own type) and by-reference-ness, or another kind of member of its name (in a base too, but a base's method beside a generated property), visible from the value object; the named generated members are entries the aspects introduce through, which name, check and mark them, and the check, the CMTK1005/CMTK1021 names and the refused interfaces read the same entries; every generated member carries `[GeneratedMember(typeof(Contract), "Member")]` from the base package, but the overrides Metalama writes over a record's implicit members, which take no attribute; refines decisions 28 and 121 | 2026-10-31 | accepted |
| [0129](0129-metalama-2027-0-moves-into-2-4-0.md) | The Generators package moves to Metalama 2027.0 in 2.4.0, for both targets, so a consumer with only the .NET 11 SDK builds (on 2026.1 a web project fails there with LAMA0625, on `net10.0` as on `net11.0`, KB-060); 2.4.0 is tagged only on a stable 2027.0; a CI and smoke-test lane with only SDK 11 installed and a web consumer joins decision 120's conditions (#281); 2.3.x is the line below the ceiling; #173, #272 and #223 move to 2.4.0; staying on 2026.1, a 2.4.0 without `net11.0` and a 2.4.0 on the preview were refused; replaces decision 120's Metalama timing | 2026-10-31 | accepted |
20 changes: 11 additions & 9 deletions design/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,17 @@ Small, each closable in a minor version. Found by the reviews named.
converters' trimming warnings decide whether it is a package ([decision 71](adr/0071-converters-say-what-they-write.md)).
- A stable external name for a closed-set member, apart from its C# identifier: parsing, JSON, EF,
OpenAPI and the fault identifiers at once (proposed in the review of 2026-10-13).
- The Metalama 2027.0 upgrade, in 2.5.0 (#173), once 2027.0 is stable (`2027.0.4-preview` on 2026-10-14;
[decision 4](adr/0004-metalama-2026-1-x-latest-stable.md)), for both targets at once and with 2.3 and 2.4 on
2026.1 ([decision 120](adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md)). It raises a floor in
a minor, which decision 120 allows when documented: a project that references `Metalama.Framework` 2026.1
directly gets NU1605 and stays on TypeKit 2.4.x or raises its reference, and a woven `net11.0` project moves
from C# 14 to 15. Its conditions: a build with no Metalama license passes in CI and a clean container on 2027
(#170), the weaving review and the smoke test run on it, the changelog and the Generators README name the
ceiling, the language-version change, the supported IDEs and what Metalama 2027 drops, and the release notes
say whether 2.4.x is patched. `[Durable]` returns to the `_value` placeholder with it.
- The Metalama 2027.0 upgrade, in 2.4.0 (#173), which is tagged only once 2027.0 is stable (`2027.0.5-preview` on
2026-10-31; [decision 4](adr/0004-metalama-2026-1-x-latest-stable.md)), for both targets at once and with 2.3 on
2026.1 ([decision 129](adr/0129-metalama-2027-0-moves-into-2-4-0.md), which moved it from 2.5.0 because on 2026.1 a
web project fails with LAMA0625 where SDK 11 is the only SDK). It raises a floor in a minor, which
[decision 120](adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md) allows when documented: a
project that references `Metalama.Framework` 2026.1 directly gets NU1605 and stays on TypeKit 2.3.x or raises its
reference, and a woven `net11.0` project moves from C# 14 to 15. Its conditions: a build with no Metalama license
passes in CI and a clean container on 2027 (#170), a CI and smoke-test lane with only SDK 11 installed builds a web
consumer (#281), the weaving review and the smoke test run on it, the changelog and the Generators README name the
ceiling, the language-version change, the supported IDEs and what Metalama 2027 drops, and the release notes say
whether 2.3.x is patched. `[Durable]` returns to the `_value` placeholder with it.
- A `net11.0` target beside `net10.0`, in a 2.x minor when a feature needs it ([decision 44](adr/0044-2-0-0-net10-0-metalama.md)),
on the terms of [decision 120](adr/0120-a-net11-0-target-and-documented-floor-raises-join-a-minor.md): a higher
floor in the `net11.0` group (EF Core 11 and Microsoft.AspNetCore.OpenApi 11 in the satellites) allowed when the
Expand Down
Loading