Skip to content

release: 2.2.0 - #65

Merged
CaffeinatedCoder merged 275 commits into
mainfrom
release/2.2.0
Oct 18, 2026
Merged

CaffeinatedCoder merged 275 commits into
mainfrom
release/2.2.0

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Release 2.2.0 into main

The consolidation release (decision 97): no public API change, fifteen fixes, and the probe gate binding and passing. The changelog's 2.2.0 section, dated 2026-10-18, is the release notes.

Merge with a merge commit, never squash or rebase (decision 94). Delete release/2.2.0 once merged.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 30 commits October 15, 2026 20:37
Brings in PRs #12 and #13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: 2.2.0 is a consolidation release, and the probe gate binds from it (0097)
C02OptionalTextProbe runs AsText, AsOptionalText, AsIdentifier and
AsOptionalIdentifier over the claim's inputs in five process variants;
C20ShouldHoldProbe asserts ShouldHold over two- and three-type faults
carried through each Option/Result shape. Every row passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pped type

The EF satellite's translation defects escaped most (7 of 10 in the fix
history, all silent, chains H and I), and the only catalogue so far covered
analyzer input. This is the satellite's: WrappedKinds, KeyShapes and
QueryShapes in Variants/, each case a differential against the same entity
over the wrapped type, with the README's departures from parity as oracle.

KeyGenerationVariantTests: 13 key shapes (alone, the root of TPH, TPT and
TPC, abstract or not, a shared-key, table-split and owned dependent,
composite, configured never and on add) keyed by 13 wrapped types (every
integer, char, Guid, string, decimal, DateTime) on SQLite, Npgsql and SQL
Server, 507 cases: ValueGenerated, the default SQL and the provider's
strategy equal the wrapped key's, and SQLite fills in the same keys. A char
or Guid key and a TPC root without a sequence are not generated, as the
README says.

QueryTranslationVariantTests: 5,200 queries over 22 wrapped types, three
with their own CompareTo, by accessor (.Value, GetValue, ValueOrNull, the
value object itself), site (column, nullable column, collection element,
captured list, navigation, JSON, query filter, named filter), converter
(TypeKit's, named, and a lambda, a converter type and a convention of the
application's own) and operation (equality, comparison, CompareTo, the
orderings, boxed keys, Min/Max/MinBy/MaxBy, Contains, GroupBy, Select,
Distinct, Any, Count). On SQLite the rows and, for a column, the SQL equal
the wrapped type's; where EF refuses that, memory's. It is translated for
PostgreSQL too. Expected refusals: .Value on a foreign converter's column
(or the rows memory answers, where EF evaluates the projection), and an own
CompareTo in a comparison, ordering, Min or Max, in a query or a filter.
315 cases are skipped as unclaimed (EF ordering a foreign converter's text).

Replayed at each fix of chains H and I and at its parent, all nine turn
cases green: 7732936 272, 63471e7 579, 717de54 111, 719ee82 12, 8f6af8b
12, 92dfe16 12, e644a8a 67, 3bddba7 13, 84e9776 17. Three turn cases red
that the chain's next fix repaired: 92dfe16 the abstract TPC root on Npgsql
and SQL Server (6, 3bddba7), e644a8a and 3bddba7 the char key (16 and 1,
84e9776). Seeded at HEAD: without the TPC key convention 16 key cases fail;
without the foreign-converter check in WrappedValue.Of, 579 queries; with
element columns re-typed, 272 (192 refused by PostgreSQL alone).

One divergence found and left open, skipped by name: a nullable record-class
value object compared with < keeps its null rows in memory, where the
generated operators sort null first, and SQL drops them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…catalogue

verify-the-guard's table, AGENTS.md's workflow step and the test strategy
now send a fix to an EF key or query to KeyShapes and QueryShapes, with the
chain H and I replay and the seeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s as SQL does (decision 98)

The EF catalogue's open finding: a record class's generated operators sort null
first, so in memory r.N < x is true for a null N, while EF translates "N" < @x
and SQL leaves the row out. Decided not to refuse it: the answer is the
underlying type's (an int? column's lifted < gives the same rows), a guard
cannot tell a nullable record-class property from a required one by its type,
and a refusal would break queries 2.1.0 answers with no plain rewrite. The EF
README and docs/ef-core.md now say a null passes no <, <=, > or >= in the
database, and that r.N == null || r.N < x keeps those rows.

Input class: EFT, a nullable value object compared in a query. Catalogue:
QueryShapes over WrappedKinds, now 5,732 run cases (342 unclaimed skips, no
open finding). It gained <= and r.N == null || r.N < x as operations, and a
record class over int, so the claim meets a real int? query; no sibling failed.
The open finding is now the expectation NullsLeftOut: rows as the underlying
type's query, memory's rows those plus the null rows. Seeded both ways: the
operators sorting null last, and an interceptor rewriting < to keep null rows;
each fails all six cases for that reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Own converter without .Value translation, closed sets stored by name,
UseTypeKit order, conventions by value-object type, primitive
collections, keys under inheritance, composite keys and own key
configuration, each against the plain wrapped type on SQLite. Every
pair of the eight claims is covered and every row passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n a filter, sort, grouping or aggregate, not in a final Select

The EF README and docs/ef-core.md said any query reading .Value on such a
column fails with "could not be translated". The EF catalogue measured
otherwise: a final Select or Distinct of .Value, GetValue() or ValueOrNull()
answers, and correctly, since EF reads the value object through the
application's converter and takes .Value in memory. Every other operation of
the catalogue (Where, Contains, OrderBy and its kin, Min, Max, GroupBy) fails.

The catalogue's oracle was loose to match the old sentence (refused, or
answered as memory); it now holds the narrower one: a final Select or Distinct
answers as memory does, anything else is refused. Seeded by letting .Value
translate through any converter: 627 cases fail, on both arms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Option and Result refused as columns, OrNone operators in the database,
read-back that skips validation, and the column round trip against the
plain wrapped type on SQLite. E08-E10 are fully covered; E03 covers
182 of 317 pairs, the rest are shape.site pairs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…against ASP.NET alone

Notes are a closed catalogue (NoteKeys): each key has its outcome, the detail only explains, and a key
outside it is a harness error. Substring matching on note and failure text is gone. Rec.Run fails a
violation or a harness error however it was raised, and ends every other outcome but a pass with
Assert.Skip and its reasons, so a test run counts the rows the ledger does not credit. Each row is
labelled with its index and non-default settings (TheoryDataRow.Label); build-verdict keeps the label
whole in its headings.

r.Skip(finding, why) names the open finding a row steps around. The replay fails a probe that skips
for a finding the ledger has fixed or the corpus dismissed; the gate refuses while any row skips, which
it sees from an empty state. prober status shows each claim's row outcomes and the skipped findings.

The stale F2 skip in C03 is removed (F2 was fixed in 461c920). A document that fails with AddTypeKit()
is compared with the same host without it: the same exception means the document part has nothing to
check, anything else is a violation. This replaces the Preserve, MaxDepth=3, char/WriteAsString and F2
substring rules in C03/C05. A controller left out of the API explorer is checked absent first.

Input class: a refusal the harness attributes to the framework. Sibling found and fixed:
HttpBaseline.SchemaParity isolated a failing document with AddTypeKit() on both sides, the baseline
that hid F2; it now uses the same ASP.NET-alone baseline.

Replayed from an empty state: 11 of 11 probes pass; C03 345/345 rows, C05 347/347, BaseProbe 107/346
(were 94, 81, 46); uncovered pairs C03 2049 -> 59, C05 2518 -> 79, C07 4093 -> 1737; other probes'
outcome counts unchanged. Seeded and caught: a TypeKit-only document defect (16 failing rows; the
previous harness left BaseProbe green), a key removed from the catalogue, a skip for a fixed finding
(replay exit 1), and the gate, stale-skip and status checks (4 tests fail without them). Decision 98.

Defect-Class: TOOL
Found-By: OR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…logue

test: a variant catalogue of EF keys and queries, run against the wrapped type
Each registered probe names one seeded product defect in its registry entry: a patch against src/ under
tools/probe/corpus/seeds/<claim>.patch, an environment toggle, or both, and a sentence saying what it
breaks. The seed is not part of the probe's identity.

`prober seeds [--probe P]` applies each seed in a scratch worktree of its own (`seeds`, beside the
replay's), builds, replays the probe there and counts the seed caught only by a row that ends as a
violation the unseeded probe does not have (unseeded rows from current ledger evidence, else an unseeded
run first). A missed seed, a probe without one, a seed that does not apply or patches outside src/, and a
seeded build that fails all exit 1. Nothing it runs is evidence; it writes seeds-last.json. Local only,
one build per seed. `register` takes --seed and --defect. SeedsTests covers caught, missed, the baseline,
env toggles, refused patches, build failures, undoing the seed, no evidence and the identity, and checks
without a build that every probe in the repository has a seed that still applies.

Proven by running it: 11 of 11 seeds caught at 168ac2a (this change before its record was renumbered), 553 s from an empty
state. The first run, on release/2.2.0, missed two seeds and failed, which is how they were found wrong
(E01: EF unwraps Nullable before a type mapping plugin; L02: the span parse was not seeded). Seven defects
seeded into Seeds.cs and Ledger.cs each fail a test.

The record also says pairwise coverage is a floor, not a guarantee, and sends three-way combinations to
the variant catalogue (decision 95). Numbered 0100 after PR 16 (0098) and PR 17 (0099): the gap test and
the link to 0099 fail until both are in the base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…omparison

docs: settle the EF catalogue's open finding (decision 98), and narrow the foreign-converter .Value sentence
PR 16 took decision record 0098 first, so this branch's record moves to 0099
(0099-a-probe-row-says-how-it-ended.md), and both rows stand in the index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge renamed the file to 0099 but committed its old "# 0098." title, which
DesignRecordTests refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix: a probe row says how it ended, and a refused document is judged against ASP.NET alone
# Conflicts:
#	design/adr/README.md
… 26-deep conditional

Rider froze on BuiltInConverters.For<T>, then crashed. Its redundant-cast check
computed the type of a cast over 26 nested conditionals and walked the chain
again at every level. While that held the backend's read lock, the backend
froze. The thread dump Rider took of the frozen backend then segfaulted in
libmscordaccore, six times since the chain landed on 2026-10-13, and each
crash left Rider's project-model cache corrupt. The compiler and the tests
never noticed.

For<T> now casts the result of Untyped<T>, which has one if and return per
type. The typeof(T) tests stay in a generic method, so the JIT and Native AOT
still drop every arm but the one that applies.

Input class: a conditional nested through its branches, in any source the
solution opens. Swept with the new guard over src, test, tools and
benchmarks; no other chain deeper than 8 exists.

- PatchTests.Every_built_in_type_the_context_never_saw_is_the_serializers_own
  reads and writes a Patch of all 26 types through a context that lists none
  of them, against the reflection resolver. It fails with int given the
  UInt32 converter, and with the Half arm removed.
- ConditionalChainTests holds every chain to 8 levels, with a positive
  control. It fails on the old For<T>, naming 26 levels at its line.

Defect-Class: CRP
Found-By: US
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat: every probe is shown to fail against a seeded defect (0100)
fix: BuiltInConverters picks a converter with a return per arm, not a 26-deep conditional
E08, E14 and E16 were written before decision 99 and noted keys the
catalogue does not list, so their rows ended as harness errors (20, 2
and 11 rows). The three checks ran in full; their keys join NoteKeys as
pass. E08, C20, E03, E10 and E13 built a key from text (msg:..., op-NA:,
both-threw:...), which turns into a harness error the first time the
path runs: each now notes a catalogue key with the text as its detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every registered probe now names a product defect it is shown to catch
(decision 100); `prober seeds` reports each one caught at 917a5e2:
C02 7/48 rows, C20 89/191, E03 5/90, E06 5/12, E07 3/9, E08 10/20,
E09 3/18, E10 30/30, E11 14/14, E12 4/12, E13 5/10, E14 2/9, E15 6/8,
E16 2/25. The C02 and C20 drafts were verified as written.

E03's first seed (a Guid through the byte-array mapping) was missed:
SQLite gives a BLOB column no affinity, so the Guid stayed text. A
decimal through the double mapping, a REAL column, is caught.

E11 caught its seed only by an unexpected exception, a harness error.
Each order's run now returns a refusal as its result, so the order
differential reports it, and a refusal in both orders is a violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rows appended (old rows kept) for C02, C20, E01, E02, E03, C03, C05,
  L01, L02, L03 and L04.
- shape.site=in-generic-type is refused in the axes file: the
  generators refuse a value object nested in a generic type (CMTK1005).
- C20 excludes the operations a carrier has no member for: Result.Catch
  on Option, FirstOrNone and query syntax on Result<E>.
- E01-E03 take the row's site: the nested-site shapes of the paused
  round 6 (EfShapes4.g.cs, E4.At) join the harness.
- L03's JSON half runs on every row under the row's options (the valid
  value read back, the refused one refused naming the fault), instead of
  only for input.json=refused-by-create. The own value-object converter
  and StoredJsonConverterFactory are checked for what they do, not
  passed silently. input.json is narrowed by the claim's sentence.
- An own value-object converter refused by MaxDepth is judged against
  the same converter for the wrapped type (design note D1), in L01 and
  L03, rather than reported as a violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C01 decimal spelling, C10 Preserve guard, L05 equality and L06 off the
wire, each with its rows, a seeded defect, and the shapes its rows need:
a source-generated context per JSON claim (so
json.TypeInfoResolver=source-generated-context runs as named) and L05's
value objects by kind, wrapped type, inheritance and site, generated by
harness/gen_json_family.py from the rows.

claims.json narrows C01 (shape.wrapped, input.json) and L05
(shape.refused-kind) by quoted sentences and excludes pairs no row can
have, each group with its reason in excludeReasons. NoteKeys gains
json:member-left-out-by-options (pass), json:both-directions-left-out-
by-options (inapplicable) and json:read-refused-by-stj-alone (pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second covering array of C01, C10, L05 and L06, appended after the
first replay, and the shapes the new rows need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nce handler

System.Text.Json refuses JsonObjectCreationHandling.Populate together with
a reference handler, so C01, C10 and L06 exclude those pairs. C10 checks
an array of byte value objects by its round trip, where byte[] is base64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… C19

C17 (CMTK0014) and C19 (CMTK0010 kinds) compile each row with the CMTK
analyzer attached, over the real base package; C19 reuses the analyzer
tests' TypeArgumentSites and TypeArgumentKinds catalogues. C18 (CMTK1023)
reads one throwaway consumer build of every row's declaration. C09 and
C11 run against value objects generated per row (C09Shapes.g.cs,
C11Shapes.g.cs). Each probe has a seed.

The Samples project references the analyzer assembly as a library and
Microsoft.CodeAnalysis.CSharp, and compiles the two variant catalogues in,
for the analyzer probes.

claims.json: exclusions and narrowings for the five claims, each group
with its reason in excludeWhy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaffeinatedCoder and others added 28 commits October 17, 2026 21:26
Decision 99's document rule was the last place a refusal's words decided
a row: Hx.DocRefusedByAspNetAlone and FaultClaim's copy passed a refused
document when ASP.NET alone refused it with the same message (#58, "Not
changed"). Both now compare the two refusals as side (1) and side (3)
are compared (Hx.DocAlike: Sides.Alike, the exception type and a
JsonException's Path, no message), and Hx.Unplaced, which cut the line
and byte out of a message, is gone.

Where both refuse alike the document part has nothing to check
(Hx.DocRefused):
- a check that compared nothing before the document is the framework's
  refusal, framework-refuses, an expected refusal that earns nothing
  (C21's schema checks, C12's, C16's, C13's listing where the options do
  not reject an absent patch);
- a check that compared an answer on the wire first (the fault claims,
  C03 and C05, C13's listing under RespectRequiredConstructorParameters)
  is judged by that answer, doc:refused-by-aspnet-alone, a pass, as
  decision 99 says ("judged by its other checks").

Measured: the http family replayed from empty states without footprint
at 26e51e6 before and with this change, 13 passed both times. Row for
row, 401 rows no longer pass (C12 156, C13 23, C16 93, C21 129); in each
of them every check that passed before had noted a document both hosts
refused and compared nothing else. 12 rows pass that did not (C07 1,
C14 3, C15 8): under json.MaxDepth=3 both documents are refused with an
InvalidOperationException whose messages differ, which ended the check
on record:0099 although it had compared the wire. Checks: 12482 pass
before, 10010 after. The pairs those 401 rows alone credited are no
longer covered: C12 821, C13 48, C16 455, C21 677, nearly all under
json.ReferenceHandler=Preserve or json.MaxDepth=3, where decision 99
says System.Text.Json refuses the document for plain ASP.NET too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The document the BLite port wrote on 2026-10-18, copied unchanged into design/ so the
method can be tried on this repository later without the prober, the covering arrays
and the ledger. It maps each tool function to what BLite kept, replaced or dropped,
reconstructs the process from the sessions that ran it, lists what it produced, and
ends with an adoption guide. The design map gets its row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Record 0108 (0105, the number the brief named, was taken by "every tool
is built in the session's checkout"), with its index row and status
notes on 0099 (the document rule) and 0104 (gaps reads evidence). The
roadmap's 2.2.0 section says WS2 is done, what it leaves the gate, and
that WS3 to WS6 were dropped on 2026-10-17 by the maintainer's decision.
design/plan.md lists no individual records and is unchanged.

The record's evidence: 265309f against 5c5affe replayed from empty
states (43 of 43 in 677 s, 45 of 45 in 818 s), row for row per probe
(400 regained, 166 lost, each with its reason), the document rule's
A/B, the covering array on the whole corpus, pairs per claim, C07's pass
share by check, NoteKeys 63 -> 68 -> 27, the product defects found,
seeds --all (170 of 170 caught, each by its own obligation's check) and
the gate's output with what each refusal needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling

docs: how BLite applied the probing concept by hand, as a design note
build: close WS2: its leftovers, the corpus replayed and seeded, record 0108
…tches

C22GeneratorRefusalsProbe builds its consumer in a child process, so the
coverage report of its own process shows no file under src/, and
`prober footprint` refused to record an empty footprint ("it would never go
stale"), however many paths its registry entry watched. With no footprint
the probe was never current, and the gate counted all 621 of
C22-generator-refusals' pairs uncovered.

`footprint` now records an empty footprint where the entry watches
something, and refuses only where nothing ran and nothing is watched; a
footprint is current by its watched paths when it ran nothing under src/.
C22's entry watches what its consumer compiles against: the base package,
the analyzers it carries, the generators, and src/Directory.Build.* (it
watched the first and the last two, as C18 does, and lacked the generators
and the analyzers).

Recovers 607 of C22's 621 pairs, the rest of what its rows hold; its
other 14 pairs had no row and get rows with the scattered pairs.

Tests: WatchedTests gains a probe that ran nothing under src/ (recorded by
what it watches, current, and stale when a watched path changes) and one
that also watches nothing (refused). Each fails with its half of the change
reverted, and the refusal test fails with the refusal removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for the rest of C12, C13, C16 and C21

Decision 108 stopped crediting checks that compared nothing but a document
both hosts refuse, which left 821 pairs of C12, 48 of C13, 455 of C16 and
677 of C21 uncovered.

Declarations (claims.json, `inapplicable`, cite record:0099):

- json.ReferenceHandler=Preserve on every obligation of C12, C16 and C21,
  and on C13's two document obligations (doc-required-where-rejected,
  doc-optional-where-absent); absent-is-default reads the wire and stays
  wanted there. Measured at 938d8f8 from an empty state: every row of the
  four probes under Preserve had its document refused by ASP.NET alone too
  (C12 76, C13 71, C16 93, C21 63 rows), none written.
- json.MaxDepth=3 together with source IDescribedJsonConverter or
  JsonSchemaDescriptions on C12's four obligations: the coord host's
  document is refused by ASP.NET alone in every such row (106 rows at
  938d8f8, 166 after the new rows, none written), while under the
  decimal-converter source it is written in every row (29 and 55).

No declaration under json.MaxDepth=3 alone: the documents of C12's decimal
host, of C13 (47 rows), C16 (91) and C21 (59) are written and compared
there, so it would not be true, and its pairs were covered already.

Rows: `rows --probe` for C12, C13, C16 and C21 (C13's came out the same,
since Preserve stays wanted by absent-is-default), then rows from
`rows --claim` appended until the pairs converged: C12 302 -> 462 rows, C13
296 -> 321, C16 298, C21 295 -> 435. Under MaxDepth=3 without Preserve about
two thirds of the documents are refused for ASP.NET alone too, decided by
the depth of the row's shape rather than by an axis, so those pairs needed
further rows rather than a declaration.

Recovers C12 821 (647 by rows, 174 declared), C13 48, C16 455 (283 by
rows, 172 declared), and 664 of C21's 698 (498 by rows, 166 declared).
C21's regenerated rows fail in 8 rows and its appended rows in 2 more;
their triage is in the next commits and in the pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bject admits null and ASP.NET drops it

Decision 36: "A nullable value object admits null in the OpenAPI document
wherever it appears". C21 quoted it as `a-class-value-object-admits-null`
for the class kinds only; a nullable struct value object admits null the
same way, so the entry `a-nullable-value-object-admits-null` quotes the same
sentence for http.nullability=nullable.

Triage step 3 of row 216 of C21OpenapiSchemaParityProbe (a validated struct
over double, nullable, in a read-only list, under
AllowNamedFloatingPointLiterals): ASP.NET writes the items of the twin's
IReadOnlyList<double?> without null, on the host without TypeKit as well,
and the value object's items are its component or null, as decision 36
says; the schemas are otherwise the same. No finding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at, decision 36 for an added null, and cannot tell a form-bound record class

Triage of the rows C21OpenapiSchemaParityProbe failed after its rows were
written again; each failed in three runs at 938d8f8 (two replays and a try).

- Rows 113 and 282 (triage 2, KB-001): a class value object over TimeOnly
  and over char at the root, nullable, under OpenAPI 2.0. The minimal
  schema endpoint's twin body is TimeOnly? / char?, which ASP.NET writes as
  its format and allOf [{x-nullable: true}] alone, on the host without
  TypeKit as well; the class value object's body is not nullable in a
  minimal API and keeps its schema. KB-001 (executable, KB001.cs) says a
  nullable body is written as "its format and allOf: [{"x-nullable": true}]";
  the harness cited not-promised openapi-2-nullable-body-keeps-only-its-null
  only where the body kept nothing but its null (an enum), and now cites it
  where it keeps its format too.
- Row 216 (triage 3, decision 36): the value object admits null where
  ASP.NET writes the twin's nullable type without it, the schemas otherwise
  the same: cites a-nullable-value-object-admits-null (previous commit).
- Row 62 (triage 1): a validated class over a record class, bound from an
  MVC form. ASP.NET binds the wrapped record class by its properties (A, B)
  and the value object otherwise (Value.A, Value.B); the harness already
  ended parameters that differ this way inconclusive, and now does the
  same for a form body.

Rows 38, 170, 184, 196, 330, 358, 386, 408, 415 and 422 still fail; the
pull request carries their triage (code-analyst hand-offs and the harness
gaps left).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hey substituted

The stored rows of C17Cmtk0014Probe and C19Cmtk0010KindsProbe put
shape.refused-kind values in rows whose other settings exclude them; the
probes substituted another kind there and credited values the row did not
run. `rows --probe` now leaves such an axis out of the row (decision 108),
so both files are written again whole: C17 283 -> 295 rows (14 leave an
axis out), C19 282 -> 297 (14 leave an axis out). Both replay with every
row passing.

Recovers C17-cmtk0014's 41 pairs and C19-cmtk0010-kinds' 50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and its JSON check reads a value of another kind

L02 (README "Rejections never repeat the input") left 74 pairs uncovered,
6 of them json.Converters=own-for-wrapped|own-for-value-object against
text.entry=TryFrom|ToString-format|TryFormat-short-span: in such a row the
JSON check is declared inapplicable (not-promised
the-applications-own-converter) and nothing else ran.

What is true there, and so what this commit says: TryFrom, ToString-format
and TryFormat-short-span reject no text. TryFrom answers None, and
formatting reads no input; the Generators README's "When text is rejected"
names the rejections, a FormatException from Parse and false from
TryParse. The probe already runs its parsing checks only for Parse,
TryParse, IParsable, ISpanParsable and the type converter (TextRow.Reads),
which are the `axes` both obligations list. So parsing-never-repeats and
no-inner-that-quotes declare themselves inapplicable at those three entries
(cite docs:src/CodoMetis.TypeKit.Generators/README.md#when-text-is-rejected).
The 6 pairs are then wanted by no obligation; every other pair of those
entries stays wanted by json-never-repeats. Rows could not reach them: no
check of the claim applies there.

json-never-repeats placed the secret only where the wrapped value stands,
as a string, and an unvalidated value object over string or Uri reads every
string, so such rows ended "every JSON input read, none refused". Where
every variant was read, the check now also sends an array holding the
secret in that place, which such a value object refuses, and requires the
refusal not to quote it (not at a dictionary key, which is a property
name). 22 of the 26 rows inconclusive that way now pass, none fails.

Exclusions: json.PreferredObjectCreationHandling=Populate against a
reference handler, which System.Text.Json refuses (JsonRow substitutes the
default), as twelve other claims exclude it; and a record class as a
dictionary key, which System.Text.Json writes only through a converter of
the application's (NotSupportedException for the wrapped type alike), where
the JSON half is not promised, and which has no parsing.

Rows: `rows --probe` (289 rows), then rows from `rows --claim` appended,
and 8 rows set by hand from a passing row's settings for pairs the covering
array kept placing in rows that read nothing: own-for-value-object with
Parse, a record class with a wrong token and as a nullable property, Uri
and TryFrom as a field (with IncludeFields), and an enum with ISpanParsable;
366 rows.

Recovers all 74: 6 declared, 3 excluded, 65 by rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g, bool and Uri, and holds group-by at two levels over DateTimeOffset

E02ValueTranslatesProbe built where-value-compare with
Expression.GreaterThanOrEqual and, where that throws, substituted
where-equals ("the wrapped type has no >= operator"), so 5 pairs of
ef.query=where-value-compare never ran: string, bool, Uri, enum and flags
enum. The reason is true of the first three, which have no >= in C#, and
not of an enum, which C# compares with >= over its underlying type: an
expression tree the compiler writes for `o.Status.Value >= Status.B`
converts both sides to it.

- The probe now compares an enum (or a nullable enum) as C# does, through
  the underlying type on both sides; rows 216 and 217 (an enum and a flags
  enum) run where-value-compare and pass, the rows and the SQL the twin's.
- claims.json excludes where-value-compare over string, bool and Uri with
  that reason.
- One group-by row for shape.site=nested-two-levels x
  shape.wrapped=DateTimeOffset (row 215), and the two rows above, appended
  from `rows --claim`.

Recovers all 6 of E02's pairs: 3 excluded, 3 by rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…01, L03, L04, L06 and C22

Exclusions, each with its reason:

- json.PreferredObjectCreationHandling=Populate against
  json.ReferenceHandler=Preserve|IgnoreCycles for C03, C05, L01, L03 and
  L04, the reason twelve other claims already give: System.Text.Json refuses
  Populate with a reference handler, and JsonRow substitutes the default.
  Recovers C03's 2, C05's 2, L04's 2, and 2 each of L01 and L03.
- C02: AsText and AsOptionalText in globalization-invariant mode, not
  promised (as-text-refused-in-globalization-invariant-mode): AsText()
  throws PlatformNotSupportedException there (docs/text.md, "Globalization-
  invariant mode") and AsOptionalText() calls it; the probe checks that
  refusal without credit and runs the row on the identifier counterpart.
  2 pairs.

Rows, appended from `rows --claim` and replayed:

- C02: 4 rows in globalization-invariant mode (the other 5 of its 7 pairs).
- L01: 3 rows (read-only-list-element x flags enum, wrong-token x enum,
  and a record class as a dictionary key, row 582, which ends
  framework-refuses), and row 584, set by hand from row 582: a record class
  as a dictionary key under json.Converters=own-for-wrapped, the only
  converter under which System.Text.Json writes a record class as a key
  (row 582 and every earlier row with the pair end framework-refuses,
  NotSupportedException for the twin alike). Recovers L01's other 3.
- L03: 2 rows (polymorphic-derived-property x a record class, TimeOnly x
  ToString-format, a record class x Parse). Recovers L03's other 3.
- L06: 1 row, field x string under IncludeFields; ProbeContext gains its
  carrier (Shapes.g.cs, written again by shapes.cs from L06's rows, as the
  generators' README says). Recovers L06's 1.
- C22: 9 rows for the 14 pairs no row held once its footprint was current
  (shape.inheritance, shape.kind and shape.site against DateTime,
  DateTimeOffset, DateOnly, Uri, long and char).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d C21's obligations do not apply there

Triage step 2 of row 408 of C21OpenapiSchemaParityProbe (a validated class
over char as a polymorphic derived property, json.MaxDepth=3 and
json.NumberHandling=WriteAsString); it failed in three runs of `try` at
53050b7. AddTypeKit() refused the document with a JsonException reading an
OpenApiJsonSchema, ASP.NET alone with MaxDepth's InvalidOperationException:
two refusals that differ by type, which decision 108 makes a violation
unless a sentence makes them intended. Neither is TypeKit's.

ASP.NET builds each schema with System.Text.Json's exporter under the host's
options and reads the node back as its own OpenApiJsonSchema; a char's
schema is minLength and maxLength 1, which reach the reader as strings under
WriteAsString. Measured in a host without TypeKit (KB042.cs, its fixture):
a body holding a char is refused so, one holding an int is written, and so
is a char under the default number handling. With MaxDepth left at its
default, ASP.NET alone refuses row 408's document with this same
JsonException; under MaxDepth=3 it meets the depth limit first at the
twin's endpoint, where TypeKit asks for the char while it describes the
value object's.

So no document is written for a char under WriteAsString, by either host:
each obligation of C21 declares the pair json.NumberHandling=WriteAsString x
shape.wrapped=char inapplicable, citing fact:KB-042. No finding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…first-use null, a 2.0 nullable element, or a library's converter

Triage step 3 of rows 196, 358 and 422 of C21OpenapiSchemaParityProbe, each
failing in three runs of `try` at 53050b7; the harness cites the entries in
the next commit.

- Row 196 (a validated struct over an enum written by name, a polymorphic
  derived type's property): the twin's property refers to the enum's
  component, which lists null because ASP.NET met it first as a nullable
  use, on the host without TypeKit as well; the value object's property is
  not nullable there and its component lists no null. KB-027 (executable)
  says so for every use: "met first as a nullable parameter, the component
  lists null among its values, for its non-nullable uses too". The entry
  enum-component-is-its-first-use quotes the sentence about parameters
  only; enum-component-lists-the-null-of-its-first-use quotes this one.
- Row 358 (a nullable validated struct over bool as a dictionary's value,
  OpenAPI 2.0): the value object's value is its component or null, which
  the 2.0 writer writes as allOf [{x-nullable: true}] without the
  reference (KB-026), where the twin's bool? keeps its type. The AspNetCore
  README said so of a property and named the element the component or null
  "as a nullable property is", but not that it loses its type under 2.0;
  it says so now, and openapi-2-nullable-element-loses-its-type quotes it.
- Row 422 (a nullable record struct over NodaTime's Instant in a list,
  OpenAPI 3.0): ASP.NET drops the twin's items, since it cannot see through
  NodaTime's converter, on the host without TypeKit as well, and the value
  object's items are put back. The README said so of "a JSON converter you
  write yourself", which own-converter-for-the-wrapped-type quotes under
  json.Converters=own-for-wrapped only; it now says the same of a converter
  a library registers for its own type, and
  a-librarys-converter-for-the-wrapped-type quotes it for
  shape.wrapped=NodaTime.Instant.

No finding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on and for an element, and KB-027 and a library's converter for a property

Triage of rows 196, 358, 386 and 422 of C21OpenapiSchemaParityProbe, each
failing in three runs of `try` at 53050b7; with this commit each ends
inapplicable with its citation in `try`.

- Row 386 (triage 3, KB-026): a nullable validated struct over Uri as a
  required-init property, OpenAPI 2.0. The value object's property is the
  2.0 null flag with the wrapped type's format beside it, the twin's Uri
  keeps its type. The harness cited openapi-2-nullable-component-loses-
  its-type only where side (1) wrote the same at the value object's path,
  before any normalisation; a nullable struct value object over a class
  had its null flag taken out by the spellings' normalisation first, which
  left only the missing type. It is now told by its structure: the format
  beside the null flag alone, and the twin's the same with its type.
- Row 358 (triage 3): a dictionary's nullable value is the null flag alone
  under 2.0, where ASP.NET alone drops a converter-backed element, so side
  (1) has nothing there; told by its structure, it cites
  openapi-2-nullable-element-loses-its-type.
- Row 196 (triage 3, KB-027): the branch that cites
  enum-component-is-its-first-use for a parameter now takes a property too,
  citing enum-component-lists-the-null-of-its-first-use, on the same
  evidence (the twin's enum is a resolved component, side (1) lists the
  same null).
- Row 422 (triage 3): a list of Instant whose twin has no items, side (1)
  neither, cites a-librarys-converter-for-the-wrapped-type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…get-only property, a field and an oblivious one

C21OpenapiSchemaParityProbe found, and the code-analyst confirmed on 2026-10-18 by
reproduction and a seeded fix, that a nullable value object written as a component lost
its null in three positions where its wrapped twin kept it: a get-only property bound by
the constructor, a field under IncludeFields, and a reference-type property in
#nullable disable code. The value object's property was the bare $ref; the twin's
admitted null. Decision 36 and the AspNetCore README promise null wherever a nullable
value object appears.

Cause. ASP.NET adds oneOf [null, $ref] around a component only when the schema carries
x-is-nullable-property, set by ShouldApplyNullablePropertySchema, which needs a
PropertyInfo whose NullabilityInfoContext WriteState is Nullable: not a get-only
property (Unknown), a field (a FieldInfo) or an oblivious reference type. The inlined
wrapped type takes its null from JsonPropertyInfo.IsGetNullable || IsSetNullable, true
in all three. ValueObjectSchemaTransformer left the null to ASP.NET wherever the value
object is a component.

Change. Where the value object is not inlined, has a component and the property's
contract says it is nullable, the transformer marks the schema with
x-is-nullable-property, so ASP.NET writes the null-or as it does for a settable
annotated property. That reads ASP.NET's internal key (OpenApiConstants.NullableProperty,
Microsoft.AspNetCore.OpenApi 10.0.12) the way decision 106 reads x-schema-id. It never
takes a mark away. The analyst's candidate also excluded a value object over object;
that is unreachable, since the generators refuse IValue<object> (CMTK1005), so it is
left out. By the maintainer's decision the rule holds under every OpenAPI version: under
2.0 the property becomes ASP.NET's documented form, allOf [{x-nullable: true}] without
the reference (KB-026, the loss the README states for a nullable value-object property),
as a settable annotated one already was.

Tests. ComponentNullablePropertyTests: the three positions under 3.1, 3.0 and 2.0, each
against the settable annotated property of the same value object beside it (ASP.NET's
own form) and against the twin (TimeSpan? for ProbeDuration?, string for ProbeLabel):
9 cases. Reverted, all 9 fail with the bare $ref where the null-or (3.x) or the 2.0
null flag was expected. PolymorphicBaseTests' fixture gains a get-only nullable value
object, which the base's own branch now writes as the derived branch does (it passes
either way; coverage, not a guard).

Variants (decision 95). The input class: a property's nullability as
NullabilityInfoContext reads it (ASP.NET's, for a component) against JsonPropertyInfo
(the serializer's, and ASP.NET's for an inlined schema). There is no catalogue for it, so
the test holds one: 31 members, each a value object's beside its wrapped type's, under
3.1, 3.0 and 2.0, with value objects as components and inlined, and RespectNullableAnnotations
off and on (372 cases). Struct value object: settable, get-only, computed, field,
readonly field, init, required init, positional record, a [JsonInclude] private setter,
a get-only property of a struct, and non-nullable settable, get-only and field. Class
value object: annotated settable, get-only, field, required init and positional; oblivious
settable, get-only and field; [AllowNull], [MaybeNull], [DisallowNull], [NotNull]; typed
by a type parameter as settable, get-only, field and required init; non-nullable settable
and get-only. The type-parameter members and the RespectNullableAnnotations axis follow
the catalogue of a500025, an unpushed fix of the same defect made in another session.
Reverted, 93 of the class's 381 cases fail: the 9 above and 14 members in every component
row. Besides the three named, they are siblings of the same cause, fixed by the same mark:
a computed property, a readonly field, a get-only property of a struct, [MaybeNull],
[DisallowNull], and a type parameter's get-only property and field. Already right, and
unchanged: settable, init, required init, positional, the private setter, [AllowNull],
[NotNull], a type parameter's settable and required-init property, every non-nullable
member, and every inlined row. The shape.* catalogue has no value for a get-only property,
an oblivious one or a nullability attribute; they are held in the test, not added to
.claude/probe-axes.md, since the axes file is in every probe's identity (decision 108).

AspNetCore README and docs/aspnet-core.md: no sentence changes; the README already says a
nullable property is the component or null, and loses its type under 2.0.

Defect class SDL, silent does-less: the document builds and every value object is described, and
the null is simply missing, with no error and no setting involved. Two of the three positions need
no option at all (a get-only property, an oblivious one), so the cause is not a framework
configuration (FCI), and no more specific class of decision 94's table fits.

Defect-Class: SDL
Found-By: PR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow's axes

With the fix before this commit, a nullable value object that is a component is
OpenAPI 2.0's allOf [{x-nullable: true}] without the reference wherever the serializer's
contract says the property is nullable. HttpBaseline.SchemaParity cited the not-promised
entry openapi-2-nullable-component-loses-its-type only where side (1), ASP.NET alone,
writes the value object's position the same, and ASP.NET alone adds that null only for
a settable annotated property. Rows 178 (a record class over DateTime, generic container)
and 194 (a record class over TimeSpan, nullable-property in a record class) then failed:
the twin's property keeps its type beside x-nullable, the value object's has the null
flag alone.

The check now cites the same entry by the row's axes as well: OpenAPI 2.0, a nullable
value object, not inlined (CreateSchemaReferenceId other than null-inlines-all, the
entry's own axes), where the value object's property is the null flag without its
reference or type and the twin's property admits null.

prober try --probe C21OpenapiSchemaParityProbe, with the rows file of release/2.2.0:
release/2.2.0 (938d8f8): expected-refusal 129, inapplicable 4, inconclusive 6, pass 155.
With the fix alone: violation 2 (rows 178, 194), pass 154, inapplicable 3. With this
commit: expected-refusal 129, inapplicable 5, inconclusive 6, pass 154, exit 0; row 181
(a validated class over Uri) went from inapplicable to pass, and 178 and 194 are the
cited loss.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix's new lines in ValueObjectSchemaTransformer moved the context of
the seed for nullable-admits-null-as-the-wrapped-does, and `prober seeds
--probe C21OpenapiSchemaParityProbe` reported it as not applying (patch
failed at line 98). The patch is made again against the new code, with the
same defect: a nullable value object's position is Property wherever it is
Nullable<T> or its property is nullable, and None as a parameter, which puts
the null where ASP.NET's exporter does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re MVC's formatters decide them, and tells KB-026 by the null flag beside a format

With F4's fix applied (its own pull request, fix/component-keeps-null),
`try` of all of C21OpenapiSchemaParityProbe failed rows 38, 178, 181 and 243;
each now passes or cites, and so does the whole probe.

- Row 38 (triage 1): with the null in place, the next difference was the
  MVC body's media types: the value object's lists application/xml,
  text/xml and application/*+xml, the twin's does not. Under
  mvc.InputFormatters=xml-added, XmlSerializerInputFormatter reads the
  sealed record class value object and not the Uri it wraps; ASP.NET alone,
  with both sides' endpoints, lists the same for each side. The harness
  compared which media types MVC's formatters accept for two .NET types,
  which the claim does not speak to. Where side (1) lists each side's media
  types as the document does, the schemas are now compared under the media
  types both list; otherwise as before, so a media type lost to TypeKit
  still fails.
- Rows 178, 181 and 243 (triage 3, KB-026, openapi-2-nullable-component-
  loses-its-type): class value objects over long, bool and int whose
  property admits null as a type parameter's or set through the
  constructor, OpenAPI 2.0. With F4's null around the reference, the 2.0
  writer writes the property as allOf [{x-nullable: true}] beside the
  wrapped type's format, without the type and pattern the twin keeps, as
  the AspNetCore README says. The branch row 386 needed is told the same
  way for any content the writer drops: the value object's property is the
  null flag, alone or beside a format, and the twin's admits null with the
  same format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…twin is a component itself

`prober status` after a replay of the http family, with F4's fix applied: C21 had
13,677 of its 13,716 wanted pairs current, 39 uncovered. `rows --claim
C21-openapi-schema-parity` proposed 14 rows, 5 of them the same as stored
rows that earned nothing: under json.MaxDepth=3 both hosts refuse the
document (409, 426, 430), a record class bound from an MVC form is
inconclusive (411), and two nullable arrays ASP.NET names as one component
(425). In each proposed row the axes no target pair names were set apart
from those: json.MaxDepth and openapi.CreateSchemaReferenceId to their
defaults, an own-record-class's form binding to the body, and the array's
nullability to as-declared. The 14 are appended as rows 435 to 448.

Row 444 (triage 3, KB-026): a validated class over a record class as a
property, OpenAPI 2.0. The value object's property is the null flag alone;
the twin's is the record class's component, whose reference ASP.NET's own
rule leaves without a null, where the value object admits one (decision 36).
The structural KB-026 branch now takes a twin that is a resolved component
beside one that admits null.

`try` of all 449 rows: pass 292, expected-refusal 112, inapplicable 26,
inconclusive 19, no violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…MaxDepth=3, and rows for its last 12 pairs

`prober gate --since v2.1.0` after `replay --all` with F4's fix applied: 45 probes
passed, C21 had 12 of its 13,716 wanted pairs uncovered, from rows 436, 440,
443 and 446 of the previous commit, which earned nothing.

- json.MaxDepth=3 x shape.wrapped=own-record-class: every one of the 26
  stored rows that holds the pair ends as ASP.NET alone refusing the
  document (the schema exporter's MaxDepth InvalidOperationException, at
  every position and host) or inconclusive; none can compare a document.
  Each obligation declares the pair inapplicable, citing record 0099, as
  it does json.ReferenceHandler=Preserve.
- The other 11 pairs get five rows (449 to 453), each from `rows --claim`
  with the axes its target pairs do not name set where the document
  compares like with like: json.MaxDepth at its default; an own record
  class or an Instant as declared, not nullable, since F4 has a nullable
  value object admit the null its twin's component lacks (decision 36);
  a struct value object over a record class made a validated class with
  everything inlined, since a struct over a class has no twin at its
  nullability; a list of Instant on the host with controllers alone, whose
  minimal schema endpoint drops the twin's items (not-promised
  a-librarys-converter-for-the-wrapped-type); bodies rather than a query
  for a record class, which ASP.NET binds by its properties.

`try` of rows 449 to 453: 5 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`prober gate --since v2.1.0` passes on probes alone, with no waiver: after
`replay --all` from an empty PROBE_STATE_DIR with F4's fix applied (its own
pull request), "44 claim(s) touched and fully covered, every probe replayed
and passing, no open finding". The item "Grow the probe corpus until
`prober gate` passes" goes, and so does the WS2 item's list of what the
harness left the gate (C12, C13, C16 and C21's document-only pairs, C22's
footprint), which this pull request answered. The roadmap holds what is
open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix: a nullable value object that is a component keeps its null as a get-only property, a field and an oblivious one
F4's fix (#63), which C21's rows 38, 170, 184, 330 and 415 need.

# Conflicts:
#	tools/probe/corpus/harness/HttpBaseline.cs
build: answer the gate's refusals with rows, declarations and exclusions; the gate passes
…eclared response kept as declared

The 2.2.0 section of CHANGELOG.md is dated 2026-10-18, which the release workflow reads as
the release notes, and AnalyzerReleases.Shipped.md gets its empty "Release 2.2.0" section,
since the consolidation release ships no rule. Record 0108 and its index row say the gate
passed on release/2.2.0 on 2026-10-18, by #62 (declarations, exclusions and rows for the
17 refusals) and #63 (the fix for F4, which C21's regenerated rows raised and the
code-analyst confirmed).

The AspNetCore README says what the code-analyst's A4 found undocumented and the maintainer
decided on 2026-10-18 to keep: a response an endpoint declares itself at a fault status,
with Produces(404) or [ProducesResponseType(404)], is kept as declared, and the document
adds no problem schema there, though the wire still answers a problem
(FaultResponsesOperationTransformer: "A response the operation already has is kept").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs: close 2.2.0: the changelog dated, the gate's pass recorded, a declared response kept as declared
@CaffeinatedCoder
CaffeinatedCoder merged commit aa5e68f into main Oct 18, 2026
31 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the release/2.2.0 branch October 18, 2026 13:34

This branch was successfully deployed

1 active deployment
nuget — f17e7605 Deployed Oct 18, 2026 by CaffeinatedCoder via Publish #10
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.

1 participant