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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 0118. An Error may learn, in a minor version, a form only that version's new API can write

Date: 2026-10-29
Status: accepted

Context: 2026-10-29, issue #153, found by the pre-release sweep of `release/2.3.0` at `99c6032`; refines
[decision 34](0034-errors-fixed-within-major-version-warnings.md). 2.3.0 gives every assertion of
`CodoMetis.TypeKit.Testing` a form on a `Task` or `ValueTask` of what it checks (#87), and CMTK0003, an
Error since 2.0.0 ([decision 50](0050-warnings-cmtk0003-alone-becomes-error-2.md)), reports such an
assertion left without `await` (`IgnoredOutcomeAnalyzer.IsPendingAssertion`): it fails the test only through
the task it returns, so the dropped task passes whatever the value is. Decision 34 says an Error that
reports more waits for a major version, and the Analyzers README promised the same, so the 2.3.0 analyzer
contradicted the 2.3.0 README with no record of why.

## Decision

**An Error may report, in a minor version, a form that only an API new in that same version can write.**
Decision 34 keeps Errors fixed within a major so that an update never fails a build that worked before.
A form that needs the new API to be written cannot occur in code that built on the previous version, so
reporting it fails no such build. The rule holds when every reported form calls the new API itself; a
form that merely sits beside it, or that a name alone recognises, does not qualify.

**What qualifies in 2.3.0.** CMTK0003's dropped pending assertion: an expression statement that drops,
without `await`, the task an assertion of `CodoMetis.TypeKit.Testing` returns on a `Task` or `ValueTask`.
The rule knows the assertion by the package's own assembly (`TypeKitSymbols.IsTestingAssembly`), so a
consumer's extension of the same name on a pending result, which may have built on 2.2.0, stays silent
(`IgnoredOutcomeAnalyzerTests.A_task_of_another_assembly_s_extension_on_a_pending_value_is_silent`). No
preview of 2.3.0 was published (nuget.org lists 2.2.0 as the latest of both packages on 2026-10-29), so
no build on these assertions exists before the tag.

**It covers #152's forms.** The same assertions passed through `ConfigureAwait` or `WaitAsync` of a plain
`Task`, and reached through a branch of a `?:`, a `??` or a `switch` expression, are written only with
the same new API, so their reach widens before the tag under this decision.

**The forms freeze at the tag.** Once 2.3.0 ships, its API is no longer new: a further form of these
assertions, or of any other 2.3.0 API, that CMTK0003 learns afterwards waits for a major version, as
decision 34 says. A Warning or an Info may still learn more forms in any minor version.

**One exception in 2.3.0, taken by the maintainer.** #159: CMTK0003 also reports a `Task.WhenAny` of tasks
of results dropped without `await`, converted to a plain `Task` or waited for with `Wait`, which code built
on 2.2.0 can contain. The maintainer took it into 2.3.0 on 2026-10-29, before the tag: such a statement
loses every result in any build, so the report finds a defect, never correct code. A `WhenAny` of tasks the
caller holds completes with one of them and stays silent. The Analyzers README names the exception beside
the rule.

**Decision 34 stands otherwise.** A new rule ships at Warning or Info in a minor version, and an Error
that reports more of the code a consumer could already write waits for a major. The Analyzers README's
"What a new version may change" states both, and claim `T01-an-error-learns-only-new-api-forms` in
`test/oracles/claims.json` quotes the exception.
3 changes: 2 additions & 1 deletion design/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ per area ([design](../plan.md)) link to the records they rest on.
| [0031](0031-no-extension-object-unconstrained-t.md) | No extension on `object` or on an unconstrained `T` | 2026-09-28 | accepted |
| [0032](0032-value-object-holds-wrapped-value-alone.md) | A value object holds its wrapped value alone, and its equality is that value's | 2026-09-28 | accepted |
| [0033](0033-analyzers-run-code-metalama-transformed.md) | The analyzers run on the code Metalama transformed | 2026-09-28 | accepted |
| [0034](0034-errors-fixed-within-major-version-warnings.md) | Errors are fixed within a major version; Warnings may learn | 2026-09-28 | accepted |
| [0034](0034-errors-fixed-within-major-version-warnings.md) | Errors are fixed within a major version; Warnings may learn | 2026-09-28 | accepted; refined by [0118](0118-an-error-may-learn-a-form-only-new-api-can-write.md) |
| [0035](0035-sqlite-s-autoincrement-application-s-line.md) | SQLite's autoincrement is the application's line, not the convention's | 2026-09-28 | accepted |
| [0036](0036-nullable-value-object-admits-null-openapi.md) | A nullable value object admits null in the OpenAPI document wherever it appears | 2026-09-28 | accepted |
| [0037](0037-refusal-s-message-landing-page.md) | A refusal's message is its landing page | 2026-09-28 | accepted |
Expand Down Expand Up @@ -124,3 +124,4 @@ per area ([design](../plan.md)) link to the records they rest on.
| [0115](0115-value-objects-can-be-inlined-everywhere.md) | `AddTypeKit(typeKit => typeKit.InlineValueObjects = true)` writes every value object in place, by its wrapped type's schema, in every position and version, with no component of its own; off by default, the document unchanged; closed sets and wrapped enums keep their components; an inlined value object claims no name (decision 55) and shares no schema between its JSON positions and its parameters (decision 106) | 2026-10-29 | accepted |
| [0116](0116-the-docs-site-is-docfx-built-from-main.md) | The docs site is DocFX, published to GitHub Pages from `main` alone: the guides, the READMEs, the changelog and the API pages of the run-time packages, from the files as they are; a link to a file that is not a page becomes a GitHub address at the built commit; a pull request touching a page builds it with warnings as errors; `site/**` is prose; DocFX 2.81.0 lists the members of C# 14 extension blocks as extension methods | 2026-10-29 | accepted |
| [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 |
8 changes: 5 additions & 3 deletions design/analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ exist.
|---|---|---|---|
| CMTK0001 | No `default`/`default(T)`/`new()`/`new T()` of a value object or a `[RequireCustomInitialization]` type. An operand of `==`/`!=`, or an argument of a call whose name has the word `Equal`, `Equals` or `Compare` or ends in `Be` (`ThrowIfEqual`, `Assert.NotEqual`, `ShouldNotBe`), is exempt: a guard check is the only defence against the defaults the rule cannot see. So is a `default` assigned to an `out` parameter of a method returning `bool`, the Try pattern. Where a sibling branch does not bind (same-project value objects), the target type is taken from the enclosing conditional, switch arm or collection | Error | shipped in 1.0.0 |
| CMTK0002 | Type implements `IValue<>`/`IValidatedValue<,,>`, but the compilation does not reference `CodoMetis.TypeKit.Generators`, so it is never woven | Error | shipped in 1.0.0 |
| CMTK0003 | `Result`/`Option` a call returns, dropped by an expression statement, awaited or not, or a `Task` of one converted to a plain `Task` (`Task Cancel() => CancelAsync();`); `_ =` opts out. `Tap`, `TapAsync`, `TapError` and `TapErrorAsync` on a stored result or task are exempt, since they return their receiver, and so is a chain of them on one (`r.Tap(log).TapError(warn)`, `(await pending).Tap(log)`, `pending.WaitAsync(t).TapAsync(log)`), measured as an Error's false report before 2.0.0 was tagged; so is such a tap converted to a plain `Task` or waited for (`Task Log() => pending.TapAsync(log);`, `held.TapAsync(log).Wait()`), on a `Result` and on a `Task` of one, which an independent review found reported on 2026-10-14. Also a collection of results the statement made (an array, `ICollection<T>` or `IReadOnlyCollection<T>`, such as `await Task.WhenAll(ids.Select(orders.CancelAsync))`), and a method group of a `Task<Result>` method converted to a delegate returning `Task`; tasks held in variables are silent, and so is a lazy `IEnumerable<T>`, where nothing has run, unless a call that was given no result returned it and the statement materialises it. Also `Wait` on a task the expression made, tasks of results the statement made converted to plain ones (`Task.WaitAll`, `WaitAny`), and a `foreach` or `await foreach` over results its header made whose body never reads the iteration variable; a variable named `_` opts out. CA1806 can only enforce this per method name, not per return type | Error | shipped in 1.0.0 at Warning; Error in 2.0.0 ([decision 50](adr/0050-warnings-cmtk0003-alone-becomes-error-2.md)), its 1.x gaps closed first |
| CMTK0003 | `Result`/`Option` a call returns, dropped by an expression statement, awaited or not, or a `Task` of one converted to a plain `Task` (`Task Cancel() => CancelAsync();`); `_ =` opts out. `Tap`, `TapAsync`, `TapError` and `TapErrorAsync` on a stored result or task are exempt, since they return their receiver, and so is a chain of them on one (`r.Tap(log).TapError(warn)`, `(await pending).Tap(log)`, `pending.WaitAsync(t).TapAsync(log)`), measured as an Error's false report before 2.0.0 was tagged; so is such a tap converted to a plain `Task` or waited for (`Task Log() => pending.TapAsync(log);`, `held.TapAsync(log).Wait()`), on a `Result` and on a `Task` of one, which an independent review found reported on 2026-10-14. Also a collection of results the statement made (an array, `ICollection<T>` or `IReadOnlyCollection<T>`, such as `await Task.WhenAll(ids.Select(orders.CancelAsync))`), and a method group of a `Task<Result>` method converted to a delegate returning `Task`; tasks held in variables are silent, and so is a lazy `IEnumerable<T>`, where nothing has run, unless a call that was given no result returned it and the statement materialises it. Also `Wait` on a task the expression made, tasks of results the statement made converted to plain ones (`Task.WaitAll`, `WaitAny`), and a `foreach` or `await foreach` over results its header made whose body never reads the iteration variable; a variable named `_` opts out. From 2.3.0, an assertion of `CodoMetis.TypeKit.Testing` on a `Task` or `ValueTask` of what it checks, dropped by an expression statement without `await`: `service.FindAsync(id).ShouldBeNone();`, `service.LoadAsync().ShouldBeSuccess().ConfigureAwait(false);`, the body of a lambda that returns nothing; it fails the test only through its task. Known by the package's assembly, so a consumer's extension of the same name on a pending result is silent; awaited, held, returned or discarded with `_ =`, it is silent too. CA1806 can only enforce this per method name, not per return type | Error | shipped in 1.0.0 at Warning; Error in 2.0.0 ([decision 50](adr/0050-warnings-cmtk0003-alone-becomes-error-2.md)), its 1.x gaps closed first; the dropped pending assertion in 2.3.0, a form only 2.3.0's API writes ([decision 118](adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md)) |
| CMTK0004 | `IValueObjectMaterializer<,>.Materialize`, `ValueObjectConverter<,>.Materialize` or a hand-written value object's public `Materialize`, called or referenced (expression trees included). The satellite's own call is compiled in the satellite; EF's compiled model is generated code, whose diagnostics are not reported (measured: reported, it is six errors in the smoke test's AOT consumer). `StoredJsonConverterFactory` goes through `IStoredJsonConverterSource`, not `Materialize` | Error | shipped in 1.0.0 |
| CMTK0005 | `new T[n]`, `stackalloc T[n]`, `GC.AllocateUninitializedArray/AllocateArray<T>` or `Array.Resize` of a no-default struct: every new slot starts as `default`. A constant length of zero (for `Array.Resize` and the `GC` methods too) or initial elements are silent | Warning | shipped in 1.0.0 |
| CMTK0006 | Field or auto-property of a no-default struct type in a class that no initializer, `required` or constructor sets; a non-public parameterless constructor (EF, serializers) and a record's copy constructor are exempt. Measured 2026-09-28 on the repository's EF entities and request types: 12 of 12 reports on settable properties of a type with only the implicit constructor, each a real way to a default. `required`, the fix the message names where it compiles, works with EF's compiled model, precompiled queries and Native AOT (the smoke test rerun with it). A `[SetsRequiredMembers]` constructor must assign the `required` members it promises | Warning | shipped in 1.0.0 |
Expand Down Expand Up @@ -78,5 +78,7 @@ still runs on the source, where the name-based forms above remain the reach.
**Release discipline.** Keep `AnalyzerReleases.Shipped/Unshipped.md` tracking (RS2008). A new rule
ships at Warning or Info in a minor version and is raised to Error only in a major. Consumers
build with warnings as errors, so a new Error rule in a minor version would break their builds on
update. Within a major version an Error never reports more than it did; a Warning or Info may learn
more forms in a minor version ([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md), stated in the analyzer README).
update. Within a major version an Error never reports more than it did, unless the form is one only
that minor version's new API can write ([decision 118](adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md),
which also records 2.3.0's one exception, #159);
a Warning or Info may learn more forms in a minor version ([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md), stated in the analyzer README).
3 changes: 2 additions & 1 deletion design/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
What is open, and nothing else. Done work is in the [changelog](../CHANGELOG.md) and in git. Each
item names the decision that scoped it, or the review that found it. A Warning may learn and a
refusal may relax in any minor version; an Error that reports more waits for a major
([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md)).
([decision 34](adr/0034-errors-fixed-within-major-version-warnings.md)), unless only that minor version's
new API can write the form ([decision 118](adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md)).

## 2.2.0, consolidation

Expand Down
19 changes: 14 additions & 5 deletions src/CodoMetis.TypeKit.Analyzers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -756,11 +756,20 @@ but a suppression to say so:
- a sequence known not to be empty (CMTK0009);
- a comparison of two identities that really are shared (CMTK0008).

**What a new version may change.** Within a major version, an Error never reports more than it did.
A new Error rule, or an Error that reports more, waits for the next major version. A Warning or an
Info may learn more forms in a minor version, such as another method that returns a default instance
for CMTK0009. In a build that treats warnings as errors, such an update can report code that built
before, as a newer compiler's warnings can.
**What a new version may change.** Within a major version, an update never makes an Error report code
that built before. A new Error rule, or an Error that reports more of the code you could already write,
waits for the next major version. A minor version may teach an Error a form that only its own new API can
write. In 2.3.0, CMTK0003 reports a `CodoMetis.TypeKit.Testing` assertion on a `Task` or `ValueTask` left
without `await`, and those assertions are new in 2.3.0. Your code from before the update can't contain
one, so it builds as before.

2.3.0 makes one exception. CMTK0003 now also reports `Task.WhenAny` over tasks of results left without
`await`, which code from before 2.3.0 can contain. Such a line loses every result, so the build points at
a real bug. Handle the results, or discard the task with `_ =`.

A Warning or an Info may learn more forms in a minor version, such as another method that returns a
default instance for CMTK0009. In a build that treats warnings as errors, such an update can report code
that built before, as a newer compiler's warnings can.

## Generated code and Razor

Expand Down
15 changes: 15 additions & 0 deletions test/oracles/claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -4743,6 +4743,21 @@
"DestructuringTests"
]
},
{
"id": "T01-an-error-learns-only-new-api-forms",
"source": "src/CodoMetis.TypeKit.Analyzers/README.md#configuration",
"quote": "A minor version may teach an Error a form that only its own new API can write.",
"claim": "Within a major version an Error reports nothing more of the code a consumer could already write; a minor version's Error reports only forms that call that version's new API, such as 2.3.0's CMTK0003 on an unawaited Testing assertion over a Task or ValueTask, recognised by the Testing assembly so that a consumer's own extension of the same name stays silent.",
"oracle": "claim",
"surfaces": [
"OptionAssertions.*",
"ResultAssertions.*",
"PatchAssertions.*"
],
"heldBy": [
"IgnoredOutcomeAnalyzerTests"
]
},
{
"id": "W01-answers-are-real",
"source": "docs/walkthrough.md#walkthrough-one-orders-service",
Expand Down
Loading