Skip to content

test: the three-sided differential in the http, json and text harness, and no text-matching site left - #58

Merged
CaffeinatedCoder merged 5 commits into
release/2.2.0from
test/ws2-three-sided
Oct 17, 2026
Merged

CaffeinatedCoder merged 5 commits into
release/2.2.0from
test/ws2-three-sided

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

WS2's last harness step: the http, json and text harness compares a value object with its wrapped twin on three sides, and every text-matching site of #53's and #57's "kept" tables is gone. (1) is the wrapped twin without TypeKit, (2) the twin with TypeKit as before, (3) the value object. The rules live in one place, harness/Sides.cs; Row.Check's contract is unchanged. Side (1) comes from a fact's fixture where one covers the row, else it is measured, and two configurations measured here are pinned as facts with a test and a fixture: KB-040 (a converter's value is refused at the depth limit) and KB-041 (MVC's 400 under a source-generated context). Replayed: 29 probes passed in the http, json, text and build families. Every seed of every http, json and text probe is caught by its own obligation's check (91 seeds of 23 probes, 91 caught). docs-query check is clean.

The rules (harness/Sides.cs)

  1. The three sides.
    • Side (1), http: a host with HostCfg.WithoutTypeKit: no AddTypeKit(), none of TypeKit's JSON converters on either set of options, and no fault responses. It maps the twin's endpoints alone (PayloadOnly = "plain", now minimal and MVC). The reason for twin-only: in a document that also describes the value object's types, ASP.NET can give both shapes one component, so the twin's schema there is not its own (C21 row 104, below). A host with both sides' endpoints is started only for the value object's position in a schema difference.
    • Side (1), json and text, in process: the twin under the application's options less TypeKit's converters (Sides.WithoutTypeKit, told apart by assembly).
    • Side (2) is the twin as today, and side (3) the value object.
  2. Outcomes.
    • (1) and (3) refuse alike: the same operation, the same stage, the same exception type and the same status. The stage is a JsonException's Path, or, where one side writes through a nested serializer call, the other's path ending in it. No message is compared. The check ends expected-refusal with the new note key framework-refuses, against Baseline.Framework, or against Baseline.Fact("KB-NNN") when side (1) came from a fixture.
    • (1) answers and (3) refuses, or the reverse: a violation, unless the check cites the sentence that makes the difference intended. The cited cases were JsonParity.ReadMayDiffer's; that function was unused and is gone. Its cases were already cited in HttpBaseline.ReadRefusedAsDocumented and JsonParity.Read.
    • (2) differs from (1): a violation, "TypeKit changed the wrapped type's behaviour" (Sides.Interferes). A refusal's body, such as a problem's trace id, is not compared.
    • Both refuse, but not alike: inconclusive. Before, this was often a pass with nothing compared.
  3. When side (1) runs.
    • On every row of the obligations that promise non-interference. These are C21's five ("the one ASP.NET Core publishes for the underlying type"), L01's three ("Its JSON is the underlying type's", "keeps the JSON") and C10's three "unaffected" obligations (Sides.NonInterference).
    • Already covered: C12's parameter-keeps-aspnet and C01's covers-every-decimal compared with the framework without TypeKit's converters on every row before this step (SchemaClaim's Baseline.Framework, DecimalSpelling's serializer's own decimal).
    • Elsewhere, lazily: only where (2) and (3) disagree or refuse.
  4. Fixtures.
    • How the harness reads them: through FactFixture, which is compiled into the Samples as it is (<Compile Include="..\CodoMetis.TypeKit.Facts\FactFixture.cs">, as the analyzer tests' variant catalogues are). The Facts project is not referenced, since it is a test project of its own. The fixtures are read from the repository, only where the fact's entry names them on its fixture: line.
    • The settings an answer covers: the row's axes at the values that applied, plus what the harness measured: written-by, writer-depth, endpoint, binding, text.
    • Where they are read: KB-040 for a converter's value under a depth limit, KB-041 for a refused MVC binding under a source-generated context, and KB-037 for a binding under MaxValidationDepth = 1. No row's configuration is covered by KB-026's, KB-001's or KB-027's answers, so the C21 sites measure their side (1).
    • Staleness: the probes that read a fixture list it, its entry and FactFixture.cs as watched in registry.json, so a changed fixture stales them.
  5. EF stays two-sided. Nothing under the EF harness or probes changed.

Text-matching sites removed

Site (#53, #57) Now
HttpBaseline.cs:45, :59: stj-maxdepth, both sides' "maximum allowed depth" Value object refused: side (1) for a converter's value under the row's json.MaxDepth (Sides.WrittenByAConverter). The harness reads the depth the serializer hands the wrapped value to a converter at, then takes KB-040's fixture where it covers that depth (3 rows, fact:KB-040), else measures the twin through a converter of the application's.
Twin refused: the twin on the host without TypeKit refusing alike (1 row), or "Your own converter wins" where the application's converter writes the value object.
HttpBaseline.cs:290: MVC's 500 naming ValidationProblemDetails Side (1) of a refused binding: the twin on the host without TypeKit, sent a text it cannot read. Read from KB-041's fixture where it covers the row (json.TypeInfoResolver=source-generated-context, an [ApiController] endpoint, a failed binding), so rows 286 and 309 end framework-refuses against fact:KB-041.
HttpBaseline.cs:536: OpenAPI 2.0's .allOf and [{"x-nullable":true}] The value object's node at the same path in side (1)'s document (both sides' endpoints, no TypeKit) equals side (3)'s, where the twin has none. Row 38 still cites openapi-2-nullable-component-loses-its-type.
HttpBaseline.cs:544: the wrapped side's "schema":{"allOf":[{"x-nullable":true}]} Side (1)'s twin and side (2)'s both hold a body whose schema is its null alone, told by the JSON's structure (OnlyItsNull), not a search of the text. Row 257 still cites openapi-2-nullable-body-keeps-only-its-null.
HttpBaseline.cs:554: the $.parameters…enum path The difference's keys, as structure (Diff.Keys is ["parameters", .., "enum"]), and side (1)'s twin listing the same null through the same resolved component. Row 285 still cites enum-component-is-its-first-use.
Positions.cs:239, :268: L01's write and read by depth Sides.Framework on the write, and on the read and its write back (Sides.Read names a write back as its own operation). 4 checks now end against fact:KB-040.
JsonFamily.cs:62: Jx.Depth (C10) Removed. C10's write, read and own-converter read are three-sided; 11 checks end against fact:KB-040, and the others refused alike against framework.
TextRow.cs:285, :294: L04's depth Sides.Framework on L04's write and read back; 5 checks end against fact:KB-040.
TextRow.cs:374: L02's Path cut off by its words JsonException.Path is read as the property it is. The message must hold the input no more often than its Path does, and side (1), the twin reading the same text, shows whether the serializer puts the key in its Path too.
OffTheWire.cs:67: "not a wire type" Checked by type: one exception in the chain was thrown (TargetSite) by a type nested in NotWireTypeJsonConverterFactory, compared as a Type, with nameof in the failure.
.Message read to excuse a row elsewhere json:read-refused-by-stj-alone, from message equality in OffTheWire, PreserveGuard and ReadAlike, is now framework-refuses, decided by type and path. So is json-half:wrapped-alone-refuses in L03.
FaultClaim's StartRefused, SchemaClaim's C16 refusal, C06's NoKindRefused and C18's NamesEach now read a message only to fail a row.

The note keys these sites called stay in NoteKeys, each marked "not called since the three-sided differential": stj-maxdepth, mvc-400-unwritable-under-source-gen, json:read-refused-by-stj-alone, json-half:wrapped-alone-refuses. One key is new: framework-refuses (expected-refusal).

A conventions test, ProbeMessageTextTests, holds the rule. Reading syntax, it refuses a .Message.Contains(, StartsWith(, EndsWith( or IndexOf( in harness/ or probes/ unless it stands in the condition of a failure text:

  • an if whose branch returns a failure or raises a harness error;
  • a ?: between null and a failure.

A lambda inside such a condition counts as part of it. An assignment, a catch … when or a branch that passes is refused. It has a positive control and a floor of 30 reads (33 today). It keeps one EF site, listed with its reason, and fails once that site is gone; that site is listed under "Not changed" below.

Facts pinned

Fact Test Fixture answers Decides
KB-040: under MaxDepth, System.Text.Json refuses a value a converter of one's own writes at the limit, where its own converter writes it; reading is not charged KB040 (4 cases) a-converter-at-the-limit (exception, JsonException at write), the-serializer-at-the-limit and a-converter-below-the-limit (documents) side (1) of 28 checks (fact:KB-040): BaseProbe 3, C10 11, L01 4, L03 5, L04 5; where the twin itself is refused at that depth, it refuses alike and the check ends against framework
KB-041: MVC cannot write its automatic 400 under a source-generated context that does not list ValidationProblemDetails (NotSupportedException; 400 under the default resolver) KB041 (2 cases) source-generated-context-refused-binding (exception), default-resolver-refused-binding (status) side (1) of BaseProbe rows 286 and 309 (fact:KB-041)

design/knowledge/README.md has both rows. docs-query check: 0 problems in 728 links, 512 ids, 11 fact citations (0 refused, 2 not-promised, 4 obligation, 5 harness) and 41 facts. Every cited fact is executable.

Before and after, per probe

  • Before: release/2.2.0 at 782026f, replayed from an empty state in a worktree of its own with replay --all --family http --parallel 1, then json, text and build with --parallel 4: 29 passed.
  • After: this branch at abd3238, the same four replays: 29 passed (http 13, json 5, text 4, build 7).
  • How to read the table: a row that runs no Row.Check counts as one check, its row. "Rows passing" is what Rec.Run passes: no violation or harness error in the row or any of its checks, and at least one check that passed.
  • Probes left out: the build family's C17, C18 and C19 run no Row.Check of this harness and are unchanged. They passed in both replays.
Probe rows before checks before rows after checks after rows passing, before → after regained (category before) lost (category after)
BaseProbe expected-refusal 18, inapplicable 127, inconclusive 55, pass 211 expected-refusal 27, inapplicable 389, inconclusive 77, pass 1123 expected-refusal 126, inapplicable 111, inconclusive 43, pass 131 expected-refusal 182, inapplicable 390, inconclusive 63, pass 981 338 → 333 0 5 (inconclusive 5)
C03ComposedProbe pass 648 pass 2919 pass 648 pass 2919 648 → 648 0 0
C04WithoutFaultsProbe expected-refusal 2, pass 394 expected-refusal 2, pass 835 expected-refusal 2, pass 394 expected-refusal 2, pass 835 396 → 396 0 0
C05RuleProbe pass 645 pass 1666 pass 645 pass 1666 645 → 645 0 0
C06FaultKindsProbe expected-refusal 4, pass 404 expected-refusal 4, pass 1628 expected-refusal 4, pass 404 expected-refusal 4, pass 1628 408 → 408 0 0
C07BlanketMappingProbe expected-refusal 1, pass 410 expected-refusal 1, pass 474 expected-refusal 1, pass 410 expected-refusal 1, pass 474 410 → 410 0 0
C08SameNameWarningProbe pass 318 pass 545 pass 318 pass 545 318 → 318 0 0
C12DescribedConverterProbe pass 332 pass 789 pass 332 pass 789 332 → 332 0 0
C13PatchRequiredProbe inconclusive 55, pass 301 inconclusive 55, pass 657 inconclusive 55, pass 301 inconclusive 55, pass 657 356 → 356 0 0
C14DeclaredProblemProbe expected-refusal 3, pass 418 expected-refusal 12, pass 1385 expected-refusal 3, pass 418 expected-refusal 12, pass 1385 418 → 418 0 0
C15MvcFaultStatusDocProbe expected-refusal 7, pass 398 expected-refusal 8, pass 543 expected-refusal 7, pass 398 expected-refusal 8, pass 543 398 → 398 0 0
C16OptionsAgreementProbe pass 310 pass 420 pass 310 pass 420 310 → 310 0 0
C21OpenapiSchemaParityProbe inapplicable 3, inconclusive 6, pass 284 inapplicable 6, inconclusive 10, pass 478 inapplicable 3, inconclusive 7, pass 283 inapplicable 6, inconclusive 13, pass 475 284 → 283 0 1 (inconclusive 1)
C01DecimalSpellingProbe inconclusive 20, pass 97 inconclusive 57, pass 528 expected-refusal 5, inconclusive 15, pass 97 expected-refusal 25, inconclusive 32, pass 528 111 → 111 0 0
C10PreserveGuardProbe expected-refusal 13, inapplicable 3, inconclusive 2, pass 288 expected-refusal 25, inapplicable 3, inconclusive 2, pass 582 expected-refusal 101, inapplicable 3, inconclusive 2, pass 200 expected-refusal 113, inapplicable 3, inconclusive 2, pass 494 294 → 294 0 0
L01JsonBytesProbe expected-refusal 28, inapplicable 120, inconclusive 180, pass 253 expected-refusal 33, inapplicable 136, inconclusive 192, pass 801 expected-refusal 187, inapplicable 120, inconclusive 21, pass 253 expected-refusal 203, inapplicable 136, inconclusive 22, pass 801 548 → 548 0 0
L02NoQuoteProbe inapplicable 218, inconclusive 132, pass 236 inapplicable 302, inconclusive 159, pass 843 inapplicable 218, inconclusive 132, pass 236 inapplicable 302, inconclusive 159, pass 843 434 → 434 0 0
L06OffTheWireProbe inapplicable 3, inconclusive 29, pass 293 inapplicable 15, inconclusive 38, pass 597 expected-refusal 21, inapplicable 3, inconclusive 29, pass 272 expected-refusal 22, inapplicable 15, inconclusive 38, pass 575 304 → 303 0 1 (inconclusive 1)
C02OptionalTextProbe pass 46 pass 52 pass 46 pass 52 46 → 46 0 0
C11ParseShapesBuildProbe inapplicable 4, pass 243 inapplicable 4, pass 243 inapplicable 4, pass 243 inapplicable 4, pass 243 243 → 243 0 0
L03OneRuleProbe expected-refusal 5, inapplicable 80, inconclusive 29, pass 472 expected-refusal 9, inapplicable 95, inconclusive 29, pass 1389 expected-refusal 56, inapplicable 79, inconclusive 21, pass 430 expected-refusal 75, inapplicable 95, inconclusive 22, pass 1330 581 → 561 0 20 (expected-refusal 14, inconclusive 6)
L04RoundTripProbe expected-refusal 9, inapplicable 133, inconclusive 164, pass 280 expected-refusal 10, inapplicable 222, inconclusive 183, pass 1989 expected-refusal 46, inapplicable 133, inconclusive 127, pass 280 expected-refusal 56, inapplicable 222, inconclusive 137, pass 1989 586 → 586 0 0
C09BaseRecordEqualityProbe pass 101 pass 202 pass 101 pass 202 101 → 101 0 0
C20ShouldHoldProbe pass 261 pass 727 pass 261 pass 727 261 → 261 0 0
L05EqualityProbe pass 309 pass 793 pass 309 pass 793 309 → 309 0 0
C22GeneratorRefusalsProbe pass 144 pass 432 pass 144 pass 432 144 → 144 0 0

Nothing that passed before is lost without a reason. 27 rows no longer pass (BaseProbe 5, C21 1, L06 1, L03 20). In 26 of them, the only check that passed did so with nothing compared:

  • BaseProbe #74, #135, #160, #339, #377: "compared(… both refused)". The old Same passed a check where the twin and the value object both refused. Now:
    • #135, #377: side (1) refuses alike, so they end framework-refuses.
    • #74, #160, #339: they refuse differently, so they end inconclusive. #74 and #160: the twin's JsonException at $.P.A by depth, against the value object's NotSupportedException from the Preserve guard. #339: a 400 against a 500.
  • L06 #240: json:read-refused-by-stj-alone. That pass compared the two refusals' messages. It is now framework-refuses, by type and path.
  • L03, 20 rows: json-half:wrapped-alone-refuses. That pass took any two refusals for the framework's:
    • 14 rows refuse alike and end framework-refuses.
    • 6 rows refuse differently and end inconclusive. In five of them the paths differ: a closed set's key is renamed by the key policy for the twin ($.p.ALPHA) and not for the value object ($.p.Alpha). In the sixth, the value object's Preserve guard throws NotSupportedException where the twin is refused by depth.

The 27th is C21 row 104, a harness fault (triage 1) that side (1) exposed.

  • What side (1) showed: the check failed "TypeKit changed the wrapped type's schema": the twin's DateTime?[] items lost type under OpenAPI 2.0.
  • The cause: the row's own-prefixing reference ids give the twin's container and the value object's container the same component name, XCn_record_class_POfNullable1[], because ASP.NET's default id for an array of a generic type names Nullable1[]. So the document held one schema for both shapes.
  • The old comparison: it compared that component with itself, and passed.
  • Now: a pair whose operations refer to one component at the same place is inconclusive, "nothing to compare".

Pairs: C21 13802 → 13798, L03 7059 → 7057 and L06 8022 → 8021, from those rows. Every other claim holds its pairs, and every http, json and text claim still has 0 uncredited obligations. Totals: 239508 → 239501 of 242511 pairs current, 0 open findings.

What side (1) changed

Probe checks ending framework-refuses (baseline) inconclusive checks decided
BaseProbe 161 (fact:KB-040 3, fact:KB-041 2, framework 156) 17
C03ComposedProbe 0 0
C04WithoutFaultsProbe 0 0
C05RuleProbe 0 0
C06FaultKindsProbe 0 0
C07BlanketMappingProbe 0 0
C08SameNameWarningProbe 0 0
C12DescribedConverterProbe 0 0
C13PatchRequiredProbe 0 0
C14DeclaredProblemProbe 0 0
C15MvcFaultStatusDocProbe 0 0
C16OptionsAgreementProbe 0 0
C21OpenapiSchemaParityProbe 0 0
C01DecimalSpellingProbe 25 (framework 25) 25
C10PreserveGuardProbe 113 (fact:KB-040 11, framework 102) 0
L01JsonBytesProbe 174 (fact:KB-040 4, framework 170) 170
L02NoQuoteProbe 0 0
L06OffTheWireProbe 22 (framework 22) 0
C02OptionalTextProbe 0 0
C11ParseShapesBuildProbe 0 0
L03OneRuleProbe 68 (fact:KB-040 5, framework 63) 13
L04RoundTripProbe 51 (fact:KB-040 5, framework 46) 46
C09BaseRecordEqualityProbe 0 0
C20ShouldHoldProbe 0 0
L05EqualityProbe 0 0
C22GeneratorRefusalsProbe 0 0
  • "Inconclusive checks decided": checks that were inconclusive before and are not now, for the same row and obligation, nearly all now framework-refuses.
    • L01's 170 are its "read: both refuse".
    • L04's 46 and C01's 25 are reads or writes both sides refused.
    • BaseProbe's 17 are bindings the wrapped type refused too.
  • The retired excuses:
    • stj-maxdepth (38 checks): BaseProbe 4, C10 25, L01 4, L04 5. They now end against KB-040's side (1) or the twin refusing alike. Where the twin itself is refused by depth and the value object is written by the application's converter, they end in that converter's own round trip (C10 #156) or its citation (BaseProbe #186).
    • mvc-400-unwritable-under-source-gen (2): now against KB-041.
    • json:read-refused-by-stj-alone (C10 32, L06 22): now framework-refuses.
    • json-half:wrapped-alone-refuses (L03 59): now framework-refuses, or inconclusive where the two refusals differ.
  • The OpenAPI 2.0 and enum citations (C21 rows 38, 257, 285): kept, now decided by side (1).
  • New violations: none remain. Side (1) found 18 rows across C21 and C10, each triaged with .claude/skills/probe-triage. None reached step 2, 3 or 4, so no step-4 finding went to the steering session. All are triage 1, harness:
    • C21, 11 rows at in-patch: the twin holds a Patch<T>, which is TypeKit's own type and which no host without TypeKit describes. Two changes settled them. The non-interference comparison skips a twin that holds one of TypeKit's types (Sides.HoldsTypeKit). And side (1)'s host maps the twin's endpoints alone: the first one mapped both sides', and ASP.NET alone described the value object's types beside the twin's.
    • C21 row 104: the shared component above.
    • C10 fix: a declared ProblemDetails response under application/json describes fault and names its problem types #24, #190: a sealed record reading null was judged on the options' output, which leaves the null out. It is now judged on the plain options again.
    • C10 #72, #112, #302: a nested serializer call's path, $.A, against the twin's $.P.A. The stage now accepts a path ending in the other.
    • C10 #156: the application's converter for the value object had been held to a twin refused by depth. That converter's own round trip is its baseline.

Cost: replay time per http claim

Each probe's two variants, replayed one probe at a time (--parallel 1) on the same host, before and after. Side (1) costs a host start per row:

  • C21: on every row (non-interference).
  • BaseProbe: only where the twin and the value object disagree or refuse.
  • The other http probes: they run no side (1); their differences are noise.
Probe (claim) before, s after, s change
BaseProbe (C07 (calibration)) 21.4 24.4 +3.0 s (+14%)
C03ComposedProbe (C03) 66.6 67.2 +0.6 s (+1%)
C04WithoutFaultsProbe (C04) 26.6 26.6 +0.0 s (+0%)
C05RuleProbe (C05) 47.1 46.7 -0.4 s (-1%)
C06FaultKindsProbe (C06) 25.8 25.7 -0.1 s (-0%)
C07BlanketMappingProbe (C07) 20.1 19.8 -0.3 s (-1%)
C08SameNameWarningProbe (C08) 14.6 14.7 +0.1 s (+1%)
C12DescribedConverterProbe (C12) 15.4 15.3 -0.1 s (-1%)
C13PatchRequiredProbe (C13) 14.6 14.6 +0.0 s (+0%)
C14DeclaredProblemProbe (C14) 41.8 41.3 -0.5 s (-1%)
C15MvcFaultStatusDocProbe (C15) 32.2 32.7 +0.5 s (+2%)
C16OptionsAgreementProbe (C16) 15.7 15.6 -0.1 s (-1%)
C21OpenapiSchemaParityProbe (C21) 22.0 25.0 +3.0 s (+14%)
13 probes 363.9 369.6 +5.7 s (+2%)

The json and text families run side (1) in process. json 42.8 s → 42.5 s, text 75.8 s → 77.0 s, at --parallel 4.

Seeds

prober seeds --probe P, one probe at a time, at abd3238. It ran every http, json and text probe (23, with C18, whose NamesEach changed), 91 seeds in all. 91 caught, each by its own obligation's check: each line names a row, then the obligation of the check that caught it, and that is always the seed's own obligation. BaseProbe's one seed names no obligation, so its claim check catches it. None was missed and no build failed.

seeds output
  BaseProbe (C07-fault-responses-blanket): caught, 55 of 411 rows violate; e.g. #10 claim
  C03ComposedProbe/each-error-own-set (C03-composed-faults): caught, 487 of 648 rows violate; e.g. #0 each-error-own-set
  C03ComposedProbe/detail-and-rule-intact (C03-composed-faults): caught, 163 of 648 rows violate; e.g. #2 detail-and-rule-intact
  C03ComposedProbe/statuses-ascending (C03-composed-faults): caught, 182 of 648 rows violate; e.g. #1 statuses-ascending
  C03ComposedProbe/shared-status-types (C03-composed-faults): caught, 182 of 648 rows violate; e.g. #1 shared-status-types
  C03ComposedProbe/unmapped-set-stops-start (C03-composed-faults): caught, 648 of 648 rows violate; e.g. #0 unmapped-set-stops-start
  C04WithoutFaultsProbe/doc-omits-left-out (C04-without-faults): caught, 96 of 396 rows violate; e.g. #1 doc-omits-left-out
  C04WithoutFaultsProbe/reporting-left-out-throws (C04-without-faults): caught, 312 of 396 rows violate; e.g. #0 reporting-left-out-throws
  C04WithoutFaultsProbe/narrows-wider-status (C04-without-faults): caught, 129 of 396 rows violate; e.g. #0 narrows-wider-status
  C04WithoutFaultsProbe/left-out-everywhere-stops-start (C04-without-faults): caught, 84 of 396 rows violate; e.g. #11 left-out-everywhere-stops-start
  C05RuleProbe/rule-qualified (C05-with-rule): caught, 129 of 645 rows violate; e.g. #4 rule-qualified
  C05RuleProbe/nested-chain (C05-with-rule): caught, 125 of 645 rows violate; e.g. #3 nested-chain
  C05RuleProbe/stays-the-errors (C05-with-rule): caught, 376 of 645 rows violate; e.g. #2 stays-the-errors
  C05RuleProbe/unnamed-value-throws (C05-with-rule): caught, 269 of 645 rows violate; e.g. #0 unnamed-value-throws
  C05RuleProbe/no-namespace (C05-with-rule): caught, 247 of 645 rows violate; e.g. #2 no-namespace
  C06FaultKindsProbe/kind-gives-status (C06-fault-kinds): caught, 189 of 408 rows violate; e.g. #3 kind-gives-status
  C06FaultKindsProbe/description-as-detail (C06-fault-kinds): caught, 344 of 408 rows violate; e.g. #1 description-as-detail
  C06FaultKindsProbe/no-description-no-detail (C06-fault-kinds): caught, 408 of 408 rows violate; e.g. #0 no-description-no-detail
  C06FaultKindsProbe/member-must-have-kind (C06-fault-kinds): caught, 408 of 408 rows violate; e.g. #0 member-must-have-kind
  C07BlanketMappingProbe/by-kind (C07-fault-responses-blanket): caught, 40 of 411 rows violate; e.g. #22 by-kind
  C07BlanketMappingProbe/own-mapping (C07-fault-responses-blanket): caught, 27 of 411 rows violate; e.g. #2 own-mapping
  C07BlanketMappingProbe/own-set-keeps-mapping (C07-fault-responses-blanket): caught, 64 of 411 rows violate; e.g. #1 own-set-keeps-mapping
  C07BlanketMappingProbe/enum-needs-own-mapping (C07-fault-responses-blanket): caught, 66 of 411 rows violate; e.g. #0 enum-needs-own-mapping
  C07BlanketMappingProbe/member-without-kind-stops-start (C07-fault-responses-blanket): caught, 33 of 411 rows violate; e.g. #7 member-without-kind-stops-start
  C07BlanketMappingProbe/mapping-refusal-stops-start (C07-fault-responses-blanket): caught, 70 of 411 rows violate; e.g. #4 mapping-refusal-stops-start
  C08SameNameWarningProbe/bare-names-warn (C08-same-name-warning): caught, 77 of 318 rows violate; e.g. #0 bare-names-warn
  C08SameNameWarningProbe/qualified-chain-warns (C08-same-name-warning): caught, 75 of 318 rows violate; e.g. #5 qualified-chain-warns
  C08SameNameWarningProbe/warning-names-both (C08-same-name-warning): caught, 152 of 318 rows violate; e.g. #0 warning-names-both
  C08SameNameWarningProbe/qualified-names-fix (C08-same-name-warning): caught, 75 of 318 rows violate; e.g. #5 qualified-names-fix
  C08SameNameWarningProbe/leave-one-out-fixes (C08-same-name-warning): caught, 166 of 318 rows violate; e.g. #1 leave-one-out-fixes
  C12DescribedConverterProbe/own-converter-describes-itself (C12-described-converter): caught, 119 of 332 rows violate; e.g. #1 own-converter-describes-itself
  C12DescribedConverterProbe/options-describe (C12-described-converter): caught, 28 of 332 rows violate; e.g. #19 options-describe
  C12DescribedConverterProbe/every-position (C12-described-converter): caught, 147 of 332 rows violate; e.g. #1 every-position
  C12DescribedConverterProbe/parameter-keeps-aspnet (C12-described-converter): caught, 81 of 332 rows violate; e.g. #2 parameter-keeps-aspnet
  C13PatchRequiredProbe/doc-required-where-rejected (C13-patch-required): caught, 51 of 356 rows violate; e.g. #3 doc-required-where-rejected
  C13PatchRequiredProbe/doc-optional-where-absent (C13-patch-required): caught, 119 of 356 rows violate; e.g. #0 doc-optional-where-absent
  C13PatchRequiredProbe/absent-is-default (C13-patch-required): caught, 237 of 356 rows violate; e.g. #0 absent-is-default
  C14DeclaredProblemProbe/declared-describes-fault (C14-declared-problem): caught, 30 of 421 rows violate; e.g. #15 declared-describes-fault
  C14DeclaredProblemProbe/declared-names-types (C14-declared-problem): caught, 77 of 421 rows violate; e.g. #1 declared-names-types
  C14DeclaredProblemProbe/ascending-with-declared (C14-declared-problem): caught, 105 of 421 rows violate; e.g. #1 ascending-with-declared
  C14DeclaredProblemProbe/application-json-declared (C14-declared-problem): caught, 38 of 421 rows violate; e.g. #1 application-json-declared
  C15MvcFaultStatusDocProbe/builder-reaches-doc (C15-mvc-fault-status-doc): caught, 24 of 405 rows violate; e.g. #30 builder-reaches-doc
  C15MvcFaultStatusDocProbe/without-faults-leaves-doc (C15-mvc-fault-status-doc): caught, 45 of 405 rows violate; e.g. #20 without-faults-leaves-doc
  C15MvcFaultStatusDocProbe/status-per-scope (C15-mvc-fault-status-doc): caught, 76 of 405 rows violate; e.g. #0 status-per-scope
  C16OptionsAgreementProbe/differing-refused (C16-options-agreement): caught, 37 of 310 rows violate; e.g. #7 differing-refused
  C16OptionsAgreementProbe/error-names-type-and-descriptions (C16-options-agreement): caught, 37 of 310 rows violate; e.g. #7 error-names-type-and-descriptions
  C16OptionsAgreementProbe/patch-held-refused (C16-options-agreement): caught, 37 of 310 rows violate; e.g. #7 patch-held-refused
  C16OptionsAgreementProbe/same-converters-build (C16-options-agreement): caught, 77 of 310 rows violate; e.g. #1 same-converters-build
  C16OptionsAgreementProbe/minimal-host-not-compared (C16-options-agreement): caught, 33 of 310 rows violate; e.g. #0 minimal-host-not-compared
  C21OpenapiSchemaParityProbe/the-underlying-types-schema (C21-openapi-schema-parity): caught, 10 of 293 rows violate; e.g. #15 the-underlying-types-schema
  C21OpenapiSchemaParityProbe/container-elements (C21-openapi-schema-parity): caught, 16 of 293 rows violate; e.g. #0 container-elements
  C21OpenapiSchemaParityProbe/dictionary-key (C21-openapi-schema-parity): caught, 1 of 293 rows violate; e.g. #137 dictionary-key
  C21OpenapiSchemaParityProbe/every-document-version (C21-openapi-schema-parity): caught, 53 of 293 rows violate; e.g. #5 every-document-version
  C21OpenapiSchemaParityProbe/nullable-admits-null-as-the-wrapped-does (C21-openapi-schema-parity): caught, 6 of 293 rows violate; e.g. #63 nullable-admits-null-as-the-wrapped-does
  C01DecimalSpellingProbe/smallest-scale (C01-decimal-spelling): caught, 57 of 117 rows violate; e.g. #0 smallest-scale
  C01DecimalSpellingProbe/number-is-a-number (C01-decimal-spelling): caught, 43 of 117 rows violate; e.g. #0 number-is-a-number
  C01DecimalSpellingProbe/string-is-a-string (C01-decimal-spelling): caught, 54 of 117 rows violate; e.g. #1 string-is-a-string
  C01DecimalSpellingProbe/reads-a-number-or-a-string (C01-decimal-spelling): caught, 101 of 117 rows violate; e.g. #0 reads-a-number-or-a-string
  C01DecimalSpellingProbe/keeps-the-scale-it-reads (C01-decimal-spelling): caught, 64 of 117 rows violate; e.g. #0 keeps-the-scale-it-reads
  C01DecimalSpellingProbe/covers-every-decimal (C01-decimal-spelling): caught, 4 of 117 rows violate; e.g. #32 covers-every-decimal
  C10PreserveGuardProbe/refused-where-references-could-be-written (C10-preserve-guard): caught, 4 of 306 rows violate; e.g. #221 refused-where-references-could-be-written
  C10PreserveGuardProbe/value-converters-unaffected (C10-preserve-guard): caught, 4 of 306 rows violate; e.g. #59 value-converters-unaffected
  C10PreserveGuardProbe/own-converters-unaffected (C10-preserve-guard): caught, 15 of 306 rows violate; e.g. #1 own-converters-unaffected
  C10PreserveGuardProbe/ignore-cycles-unaffected (C10-preserve-guard): caught, 4 of 306 rows violate; e.g. #72 ignore-cycles-unaffected
  L01JsonBytesProbe/as-a-dictionary-key-too (L01-json-bytes): caught, 11 of 581 rows violate; e.g. #56 as-a-dictionary-key-too
  L01JsonBytesProbe/the-underlying-types-json (L01-json-bytes): caught, 12 of 581 rows violate; e.g. #11 the-underlying-types-json
  L01JsonBytesProbe/read-as-the-underlying-type-reads (L01-json-bytes): caught, 26 of 581 rows violate; e.g. #20 read-as-the-underlying-type-reads
  L02NoQuoteProbe/parsing-never-repeats (L02-no-quote): caught, 183 of 586 rows violate; e.g. #0 parsing-never-repeats
  L02NoQuoteProbe/json-never-repeats (L02-no-quote): caught, 199 of 586 rows violate; e.g. #0 json-never-repeats
  L02NoQuoteProbe/no-inner-that-quotes (L02-no-quote): caught, 22 of 586 rows violate; e.g. #5 no-inner-that-quotes
  L06OffTheWireProbe/never-written (L06-off-the-wire): caught, 268 of 325 rows violate; e.g. #0 never-written
  L06OffTheWireProbe/never-read (L06-off-the-wire): caught, 267 of 325 rows violate; e.g. #0 never-read
  C02OptionalTextProbe/hidden-is-an-error-in-both (C02-optional-text): caught, 7 of 46 rows violate; e.g. #3 hidden-is-an-error-in-both
  C02OptionalTextProbe/required-blank-is-an-error (C02-optional-text): caught, 11 of 46 rows violate; e.g. #2 required-blank-is-an-error
  C02OptionalTextProbe/optional-blank-is-no-value (C02-optional-text): caught, 14 of 46 rows violate; e.g. #0 optional-blank-is-no-value
  C02OptionalTextProbe/null-is-blank (C02-optional-text): caught, 6 of 46 rows violate; e.g. #6 null-is-blank
  C11ParseShapesBuildProbe/implicit-result (C11-parse-shapes-build): caught, 28 of 247 rows violate; e.g. #2 implicit-result
  C11ParseShapesBuildProbe/ref-text (C11-parse-shapes-build): caught, 22 of 247 rows violate; e.g. #4 ref-text
  C11ParseShapesBuildProbe/abstract-string-constructor (C11-parse-shapes-build): caught, 22 of 247 rows violate; e.g. #3 abstract-string-constructor
  C11ParseShapesBuildProbe/parsing-where-it-can-be-parsed (C11-parse-shapes-build): caught, 16 of 247 rows violate; e.g. #37 parsing-where-it-can-be-parsed
  L03OneRuleProbe/parsing-runs-create (L03-one-rule): caught, 109 of 586 rows violate; e.g. #3 parsing-runs-create
  L03OneRuleProbe/tryfrom-runs-create (L03-one-rule): caught, 30 of 586 rows violate; e.g. #7 tryfrom-runs-create
  L03OneRuleProbe/json-runs-create (L03-one-rule): caught, 157 of 586 rows violate; e.g. #0 json-runs-create
  L03OneRuleProbe/refusals-name-the-fault (L03-one-rule): caught, 196 of 586 rows violate; e.g. #0 refusals-name-the-fault
  L04RoundTripProbe/invariant-whatever-the-culture (L04-round-trip): caught, 37 of 586 rows violate; e.g. #24 invariant-whatever-the-culture
  L04RoundTripProbe/the-type-converter-writes-the-same (L04-round-trip): caught, 9 of 586 rows violate; e.g. #16 the-type-converter-writes-the-same
  L04RoundTripProbe/text-reads-back (L04-round-trip): caught, 4 of 586 rows violate; e.g. #118 text-reads-back
  L04RoundTripProbe/json-refusal-is-a-json-exception (L04-round-trip): caught, 86 of 586 rows violate; e.g. #3 json-refusal-is-a-json-exception
  C18Cmtk1023Probe/names-each-base-member (C18-cmtk1023): caught, 130 of 280 rows violate; e.g. #0 names-each-base-member
  C18Cmtk1023Probe/a-warning-and-generated (C18-cmtk1023): caught, 280 of 280 rows violate; e.g. #0 a-warning-and-generated
  C18Cmtk1023Probe/generated-equality-never-calls-the-base (C18-cmtk1023): caught, 83 of 280 rows violate; e.g. #4 generated-equality-never-calls-the-base

Verdicts

Through build-verdict, at abd3238:

  • Conventions tests: test --project test/CodoMetis.TypeKit.Conventions.Tests, TESTS OK: 1491 passed, 0 skipped, ProbeMessageTextTests among them.
  • Prober tests: test --project test/CodoMetis.TypeKit.Prober.Tests, TESTS OK: 257 passed, 0 skipped.
  • Facts tests: test --project test/CodoMetis.TypeKit.Facts, TESTS OK: 87 passed, 9 skipped. The 9 skipped are the PostgreSQL facts, without TYPEKIT_PG. KB040 has 4 cases and KB041 2.
  • docs-query check: 0 problems; 5 harness fact citations, among them Baseline.Fact("KB-040") and Baseline.Fact("KB-041").

Commits:

  • 9553eab: KB-040 (docs:)
  • 3000d79: the Samples compile FactFixture.cs
  • bf6db0b: KB-041 (docs:)
  • 9974f46: the three-sided harness, the triage fixes and watched
  • abd3238: ProbeMessageTextTests

release/2.2.0 has not moved since 782026f.

Not changed, or noticed

  • Decision 99's document rule is as it was. Hx.DocRefusedByAspNetAlone and FaultClaim's copy still compare the two documents' refusal messages and pass a refused document (doc:refused-by-aspnet-alone; C21 has 128 such rows, the fault claims more).
    • Rule 2 would make those expected refusals compared by type, which loses about 128 of C21's rows and more of the fault claims'.
    • That is a change to a decision record, so it is left to steering.
    • They compare by equality, not Contains, so the conventions rule does not reach them.
  • Rule 2 lets a citation excuse a disagreement either way. It names the "(1) refuses and (3) answers" direction, but the documented refusals it points to (JsonParity.ReadMayDiffer's: a validated value object refusing what Create refuses, a struct refusing null, a closed set's unknown name) are "(1) answers and (3) refuses". So both directions accept a citation.
  • Two refusals that are not alike are inconclusive. That is the same exception type with a different stage, or another type. Rule 2 does not decide them; before, many were passes with nothing compared.
  • The EF family is untouched and stays two-sided. Its probes' identities change with the shared harness files, so a replay will run them again. Their code and rows are as they were. E06OwnConverterNoValueProbe.cs:72 still excuses a row by "could not be translated", the one site ProbeMessageTextTests keeps.
  • FaultClaim.cs still tells a refusal's type by Wire.Thrown's text (StartsWith("InvalidOperationException:")), only to fail a row. Wire.Ex now carries the exception, for a later change.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 5 commits October 17, 2026 13:44
… the serializer's own is written

Measured with System.Text.Json of the pinned SDK: under MaxDepth = 3, a list element at the writer's
depth 3 is written by the serializer's own converter for int and refused with a JsonException at the
list's path when a converter of one's own writes it; one level less deep it is written, and reading is
not charged. Its fixture is the probes' side (1) for a converter-written value under a depth limit,
which the stj-maxdepth note told by the refusal's message until now.

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

The three-sided differential reads side (1) from a fact's fixture where one covers a row
(FactFixture, design/adr/0107). FactFixture.cs is BCL only, so it is compiled in as it is, as the
analyzer tests' variant catalogues are, rather than the Facts project referenced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ated context without ValidationProblemDetails

Measured in a host without TypeKit: an [ApiController] action whose int parameter is sent abc answers 400
under the serializer's default resolver, and fails with System.Text.Json's NotSupportedException when MVC's
JSON options resolve through a source-generated context that lists int alone. Its fixture is the probes'
side (1) for a refused binding under such a context, which the note mvc-400-unwritable-under-source-gen
told by the exception's words until now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every comparison of a value object with its wrapped twin now has three sides (harness/Sides.cs, the rules
in one place): (1) the wrapped twin without TypeKit, a host without AddTypeKit() and TypeKit's JSON
converters (HostCfg.WithoutTypeKit, the twin's endpoints alone) or, in process, the application's options
less TypeKit's converters; (2) the twin with TypeKit, as before; (3) the value object. Where (1) and (3)
refuse alike (the same operation, stage and exception type, and status; no message compared) the check is
the framework's refusal, expected-refusal against Baseline.Framework, or Baseline.Fact where a fact's
fixture answered for side (1) (KB-040, KB-041, KB-037); (2) differing from (1) is "TypeKit changed the
wrapped type's behaviour". Side (1) runs on every row of C21, L01 and C10, whose obligations promise the
underlying type's schema or JSON or that it is unaffected, and elsewhere only where (2) and (3) disagree
or refuse.

The text-matching sites of #53 and #57 are gone: HttpBaseline's stj-maxdepth (both sides) and MVC's 500
under a source-generated context, the three OpenAPI 2.0 path and shape checks (now side (1)'s document at
the same path, and the structure of the difference), Positions.cs's and TextRow.cs's depth checks and
JsonFamily.cs's Jx.Depth, TextRow.cs's Path cut by its words (now JsonException.Path, with the twin's Path
beside it), OffTheWire.cs's "not a wire type" (now the refusal's TargetSite, by type), and the message
comparisons that made a read refused alike a pass (json:read-refused-by-stj-alone, json-half:wrapped-
alone-refuses). FaultClaim, SchemaClaim, C06 and C18 read a message only to fail a row now. The note keys
those sites called stay in NoteKeys; framework-refuses is new. JsonParity.ReadMayDiffer, unused, is gone.

Triage 1, harness, found by side (1): C21 row 104 compared the twin's and the value object's shapes where
the row's CreateSchemaReferenceId gave both one component (an array of Nullable<T>), so the document held
one schema for the two; such a pair is now inconclusive. C10 rows 24 and 190 (a sealed record reading
null) and 156 (the application's converter for the value object beside a twin refused by depth) were
judged against the wrong baseline once the depth note no longer hid them.

The probes reading a fact's fixture watch it and the reader (registry.json).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProbeMessageTextTests in the conventions tests reads the probe corpus's syntax: a .Message.Contains(,
StartsWith(, EndsWith( or IndexOf( stands only in the condition of a failure text (an if whose branch
returns one or raises a harness error, a ?: between null and one), never in an assignment, a catch filter
or a branch that passes. A positive control and a floor of 30 reads (33 when written) keep it from passing
on a walker that finds nothing. The EF family stays two-sided in this step: its one excuse by a message
(E06's "could not be translated") is listed with its reason, and the list fails once the site is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder
CaffeinatedCoder merged commit e01932b into release/2.2.0 Oct 17, 2026
13 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the test/ws2-three-sided branch October 17, 2026 14:12
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