Skip to content

test: the json and text families check each obligation (WS2 step 4) - #57

Merged
CaffeinatedCoder merged 5 commits into
release/2.2.0from
test/ws2-json-text-checks
Oct 17, 2026
Merged

CaffeinatedCoder merged 5 commits into
release/2.2.0from
test/ws2-json-text-checks

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

WS2 step 4, the json and text families: every probe whose claim's first surface is json or text (C01, C02, C10, C11, L01 to L04, L06), and the three probes whose harness is the family's (C09, C20 and L05, whose claims' first surface is generation or option-result), runs a Row.Check per obligation its row reaches, named by the claim's obligation id and given its baseline. Every excuse of outcome inapplicable, expected-refusal or inconclusive is a cited c.Inapplicable(Cite…) / c.Refused(Cite…), a comparison, or a c.Inconclusive(reason), except the text-matching sites this step keeps. prober status shows 0 uncredited for all 12 claims (before: every obligation of the 11 converted claims uncredited; C11 was converted in #54), and every seed of every probe is caught by its own obligation's check.

What changed

  • JsonParity (L01, Positions.cs). The write is one check against the wrapped twin, the-underlying-types-json or, where the value object is a dictionary key, as-a-dictionary-key-too. The read of the twin's document with the row's input.json is read-as-the-underlying-type-reads, against the twin. Before, a refused read voided the row's passing write; now the write passes beside it (160 rows regained). Under an application's converter for the value object, the read is that converter's own document read back (RoundTrip): "Your own converter wins" (AspNetCore README) in both directions.
  • DecimalSpelling (C01). One write and one read of the row's shape, computed once, judged by six checks: smallest-scale and number-is-a-number / string-is-a-string against the docs' minimal spelling worked out here (Docs(docs/json.md#decimals-in-one-spelling)), covers-every-decimal against the plain decimal (keys, the value object's document, its read), reads-a-number-or-a-string and keeps-the-scale-it-reads.
  • PreserveGuard (C10). A write check and a read check per row, under the obligation the row's handler and converters select: a converter of the application's (own-converters-unaffected), the guarded class under Preserve (refused-where-references-could-be-written, against Refusal), IgnoreCycles (ignore-cycles-unaffected), otherwise value-converters-unaffected.
  • OffTheWire (L06). never-written and never-read, each against Refusal, each with the application's own converter as its precedence baseline.
  • TextClaims (L02, L03, L04, TextRow.cs). L04: invariant-whatever-the-culture (the generated ToString() against the wrapped type's invariant text, on every text row), the-type-converter-writes-the-same (the row's writing entry), text-reads-back (that text, or TryFrom's value, read back), and json-refusal-is-a-json-exception twice: the value object's document read back (RoundTrip) and the changed input refused (Refusal). L02: parsing-never-repeats and no-inner-that-quotes split what one comparison held, and json-never-repeats. L03: tryfrom-runs-create, parsing-runs-create, json-runs-create (the read back, then the refused value) and refusals-name-the-fault (FromKnownGood's, the FormatException's, the JsonException's).
  • A closed set parses by name. TextOps now reaches its Parse(string) and TryParse(string, out). Before, HasText looked only for Parse(string, IFormatProvider), so every closed-set text row of L04 ended no-text and L02 and L03 skipped their parsing checks for a closed set. Those checks now run.
  • C02, L05, C09, C20. Each comparison is split by the obligation it holds: C02's by the input (hidden, blank, null), L05's into equality, ordering and the bases, C09's into the value object and its bases, C20's into its five promises.
  • NoteKeys is untouched. The families call no outcome key except stj-maxdepth at the text-matching sites below; the pass keys stay as c.Note.

Commits: f82f583 (the conversion and the declarations), a4e0e7d (a JSON half the framework alone refuses is a pass, as doc:refused-by-aspnet-alone is: the first conversion had lost 21 rows of L03 and L06 on it), 2002740 (no outcome key noted beside a check's own outcome; the last two sites marked), a67ac5a (release/2.2.0 merged in, with #55).

Obligation decisions

  • C10, handler=none: checked as value-converters-unaffected, the control without a handler. No obligation's axes name it.
  • C02, input=word: checked as the blank obligation of its entry (required-blank-is-an-error or optional-blank-is-no-value), as the control: a word is no blank. A null row runs null-is-blank beside it.
  • L04, TryFrom: text-reads-back (the value given back is the value given). L04's JSON round trip is json-refusal-is-a-json-exception with RoundTrip, the read back the refusal is measured from.
  • L02's TryFrom(refused) comparison is gone. It held L03's tryfrom-runs-create, which L03 checks; L02 lists no obligation for it.
  • L05 through-every-base runs only where the value object has a base record; object's and ValueType's comparers are its own equality.
  • C01 covers-every-decimal and L01's read under the application's converter for the value object compare that converter's document read back (RoundTrip), which shows it wins in both directions.

Before and after, per probe

Before: release/2.2.0 at f8993e3, replayed from an empty state in a worktree of its own, replay --all --family json, text and build, with footprints: 16 passed. After: this branch at a67ac5a, the same three replays: 16 passed. 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. The build family's other probes (C17, C18, C19, C22) are unchanged and left out.

Probe rows before checks before rows after checks after rows passing, before → after regained (category before) lost
C01DecimalSpellingProbe expected-refusal 5, inapplicable 4, pass 108 expected-refusal 5, inapplicable 4, pass 108 inconclusive 20, pass 97 inconclusive 57, pass 528 108 → 111 3 (inapplicable 3) 0
C10PreserveGuardProbe expected-refusal 14, pass 292 expected-refusal 14, pass 292 expected-refusal 13, inapplicable 3, inconclusive 2, pass 288 expected-refusal 25, inapplicable 3, inconclusive 2, pass 582 292 → 294 2 (expected-refusal 2) 0
L01JsonBytesProbe expected-refusal 180, inapplicable 7, inconclusive 1, pass 393 expected-refusal 180, inapplicable 7, inconclusive 1, pass 393 expected-refusal 28, inapplicable 120, inconclusive 180, pass 253 expected-refusal 33, inapplicable 136, inconclusive 192, pass 801 393 → 548 160 (expected-refusal 160) 5 (below)
L02NoQuoteProbe expected-refusal 29, inapplicable 9, pass 548 expected-refusal 29, inapplicable 9, pass 548 inapplicable 218, inconclusive 132, pass 236 inapplicable 302, inconclusive 159, pass 843 548 → 434 16 (expected-refusal 15, inapplicable 1) 130 (below)
L06OffTheWireProbe expected-refusal 9, inapplicable 12, pass 304 expected-refusal 9, inapplicable 12, pass 304 inapplicable 3, inconclusive 29, pass 293 inapplicable 15, inconclusive 38, pass 597 304 → 304 0 0
C02OptionalTextProbe pass 46 pass 46 pass 46 pass 52 46 → 46 0 0
L03OneRuleProbe expected-refusal 5, inapplicable 13, pass 568 expected-refusal 5, inapplicable 13, pass 568 expected-refusal 5, inapplicable 80, inconclusive 29, pass 472 expected-refusal 9, inapplicable 95, inconclusive 29, pass 1389 568 → 581 13 (expected-refusal 2, inapplicable 11) 0
L04RoundTripProbe expected-refusal 78, inapplicable 127, pass 381 expected-refusal 78, inapplicable 127, pass 381 expected-refusal 9, inapplicable 133, inconclusive 164, pass 280 expected-refusal 10, inapplicable 222, inconclusive 183, pass 1989 381 → 586 205 (expected-refusal 78, inapplicable 127) 0
C11ParseShapesBuildProbe inapplicable 4, pass 243 inapplicable 4, pass 243 inapplicable 4, pass 243 inapplicable 4, pass 243 243 → 243 0 0
C09BaseRecordEqualityProbe pass 101 pass 101 pass 101 pass 202 101 → 101 0 0
C20ShouldHoldProbe pass 261 pass 261 pass 261 pass 727 261 → 261 0 0
L05EqualityProbe pass 309 pass 309 pass 309 pass 793 309 → 309 0 0
12 probes 4051 4051 (3554 pass) 4051 10260 (8746 pass) 3554 → 3818 399 135

Checks by obligation, after:

Probe after: checks by obligation
C01DecimalSpellingProbe covers-every-decimal inconclusive 6, pass 111; keeps-the-scale-it-reads inconclusive 9, pass 108; number-is-a-number inconclusive 13, pass 43; reads-a-number-or-a-string inconclusive 9, pass 108; smallest-scale inconclusive 13, pass 104; string-is-a-string inconclusive 7, pass 54
C10PreserveGuardProbe ignore-cycles-unaffected expected-refusal 4, pass 112; own-converters-unaffected expected-refusal 5, inapplicable 1, inconclusive 1, pass 237; refused-where-references-could-be-written inconclusive 1, pass 7; value-converters-unaffected expected-refusal 16, inapplicable 2, pass 226
L01JsonBytesProbe as-a-dictionary-key-too inapplicable 12, inconclusive 1, pass 28; read-as-the-underlying-type-reads expected-refusal 27, inapplicable 17, inconclusive 180, pass 357; the-underlying-types-json expected-refusal 6, inapplicable 107, inconclusive 11, pass 416
L02NoQuoteProbe json-never-repeats inapplicable 238, inconclusive 69, pass 279; no-inner-that-quotes inapplicable 32, inconclusive 78, pass 249; parsing-never-repeats inapplicable 32, inconclusive 12, pass 315
L06OffTheWireProbe never-read inapplicable 15, inconclusive 9, pass 301; never-written inconclusive 29, pass 296
C02OptionalTextProbe hidden-is-an-error-in-both pass 13; null-is-blank pass 6; optional-blank-is-no-value pass 17; required-blank-is-an-error pass 16
L03OneRuleProbe json-runs-create expected-refusal 9, inapplicable 40, inconclusive 29, pass 739; parsing-runs-create inapplicable 32, pass 327; refusals-name-the-fault inapplicable 10, pass 262; tryfrom-runs-create inapplicable 13, pass 61
L04RoundTripProbe invariant-whatever-the-culture pass 512; json-refusal-is-a-json-exception expected-refusal 10, inapplicable 93, inconclusive 160, pass 822; text-reads-back inapplicable 98, inconclusive 8, pass 480; the-type-converter-writes-the-same inapplicable 31, inconclusive 15, pass 175
C11ParseShapesBuildProbe abstract-string-constructor pass 22; implicit-result pass 28; parsing-where-it-can-be-parsed inapplicable 4, pass 171; ref-text pass 22
C09BaseRecordEqualityProbe equal-by-the-wrapped-value pass 101; in-a-collection-of-the-base pass 101
C20ShouldHoldProbe a-failure-says-the-outcome pass 122; fails-unless-it-holds-that-reason pass 233; hands-back-detail-and-rule pass 111; uninitialized-fails pass 28; whichever-of-its-types pass 233
L05EqualityProbe equality-is-the-underlying-values pass 309; ordering-follows-the-underlying-type pass 309; through-every-base pass 175

Rows regained: 399. Every one ended expected-refusal or inapplicable before.

  • L04, 205 (expected-refusal 78, inapplicable 127). 120 of them ended no-text, a closed set (its text is now read by name) or a record class (its JSON half now runs beside the cited text checks). The rest ended on one refusal of the JSON half; the text checks now pass beside it.
  • L01, 160 (expected-refusal 160), all but one read-both-threw: the read refused for the wrapped type too, which voided a passing write.
  • L02, 16; L03, 13; C01, 3; C10, 2. These are rows where one comparison's excuse voided the others.

Rows lost: 135 (L02 130, L01 5). All but one passed before on no comparison, by a silent return null:

  • L02, 99 rows: json-never-repeats under an application's converter, the row's only check (TryFrom and the formatting entries have no parsing check), or beside parsing checks that are themselves cited or refuse nothing. The old JSON half returned null there, its comment naming not-promised the-applications-own-converter. It is now that citation, declared in claims.json.
  • L02, 31 rows that refuse nothing. In 19, every JSON input reads: a value object over string or Uri takes the secret as a value (a Uri reads any text as a relative one). In 12, the document has no member to put the input in: the options leave it out ({}, a field without IncludeFields). Where the row has a parsing entry, it reads every text too. The old checks returned null with no rejection seen; they now end inconclusive, since there was nothing to quote.
  • L01, 4 rows under an application's converter for the value object, where that converter cannot write the value object and a converter for the wrapped type cannot either: the old branch fell through to a pass with nothing read back. Now inconclusive.
  • L01, 1 row (#363), a closed-set key under a key policy with an application's converter: both checks are declared inapplicable (the key policy's entry and "Your own converter wins"), where the old branch passed on that converter's round trip.

Apart from #363, no row that passed on a comparison fails or skips now. A pass key (json:member-left-out-by-options) whose own comment says a row with both directions left out earns nothing is now an inconclusive check per direction, which loses no row: such a row was inapplicable before.

prober status, the 12 claims, at a67ac5a:

prober status, the 12 claims, at a67ac5a
test/ws2-json-text-checks at a67ac5a: 57516 of 242511 pairs current (23.7%), 0 open finding(s) -> not done
  C01-decimal-spelling: 4344/4344  (rows: inconclusive 20, pass 97; checks: inconclusive 57, pass 528)  obligations 6, each seeded, 0 uncredited
  C02-optional-text: 119/126  (rows: pass 46; checks: pass 52)  obligations 4, each seeded, 0 uncredited
  C09-base-record-equality: 487/487  (rows: pass 101; checks: pass 202)  obligations 2, each seeded, 0 uncredited
  C10-preserve-guard: 6797/6797  (rows: expected-refusal 13, inapplicable 3, inconclusive 2, pass 288; checks: expected-refusal 25, inapplicable 3, inconclusive 2, pass 582)  obligations 4, each seeded, 0 uncredited
  C11-parse-shapes-build: 1044/1044  (rows: inapplicable 4, pass 243; checks: inapplicable 4, pass 243)  obligations 4, each seeded, 0 uncredited
  C20-testing-shouldhold: 507/507  (rows: pass 261; checks: pass 727)  obligations 5, each seeded, 0 uncredited
  L01-json-bytes: 6456/6478  (rows: expected-refusal 28, inapplicable 120, inconclusive 180, pass 253; checks: expected-refusal 33, inapplicable 136, inconclusive 192, pass 801)  obligations 3, each seeded, 0 uncredited
  L02-no-quote: 8215/8314  (rows: inapplicable 218, inconclusive 132, pass 236; checks: inapplicable 302, inconclusive 159, pass 843)  obligations 3, each seeded, 0 uncredited
  L03-one-rule: 7059/7084  (rows: expected-refusal 5, inapplicable 80, inconclusive 29, pass 472; checks: expected-refusal 9, inapplicable 95, inconclusive 29, pass 1389)  obligations 4, each seeded, 0 uncredited
  L04-round-trip: 8290/8314  (rows: expected-refusal 9, inapplicable 133, inconclusive 164, pass 280; checks: expected-refusal 10, inapplicable 222, inconclusive 183, pass 1989)  obligations 4, each seeded, 0 uncredited
  L05-equality: 1076/1076  (rows: pass 309; checks: pass 793)  obligations 3, each seeded, 0 uncredited
  L06-off-the-wire: 8022/8022  (rows: inapplicable 3, inconclusive 29, pass 293; checks: inapplicable 15, inconclusive 38, pass 597)  obligations 2, each seeded, 0 uncredited
Claim pairs before pairs after obligations uncredited, before → after
C01 4344/4344 4344/4344 6 → 0
C02 119/126 119/126 4 → 0
C09 487/487 487/487 2 → 0
C10 6797/6797 6797/6797 4 → 0
C11 1044/1044 1044/1044 0 → 0
C20 507/507 507/507 5 → 0
L01 6367/6478 6456/6478 3 → 0
L02 8281/8314 8215/8314 3 → 0
L03 7055/7084 7059/7084 4 → 0
L04 8025/8314 8290/8314 4 → 0
L05 1076/1076 1076/1076 3 → 0
L06 8022/8022 8022/8022 2 → 0

Seeds

prober seeds --probe P for each of the 12 probes, one at a time: C01 and C10 at a4e0e7d, the others at 2002740. The merge of #55 changed no product file and no corpus file, so no seed ran again. 44 seeds, 44 caught, each by the check of its own obligation: the detail's example names it. C11's four are the control: its checks are #54's, and only TextOps changed under it.

seeds output
  C01DecimalSpellingProbe/smallest-scale (C01-decimal-spelling): caught, 57 of 117 rows violate; e.g. #0 smallest-scale: violation:write: Number 0.00, expected the smallest scale 0, in {"$id":"1","p":0.00}…
  C01DecimalSpellingProbe/number-is-a-number (C01-decimal-spelling): caught, 43 of 117 rows violate; e.g. #0 number-is-a-number: violation:write: String 0, expected the Number 0, in {"$id":"1","p":"0"}…
  C01DecimalSpellingProbe/string-is-a-string (C01-decimal-spelling): caught, 54 of 117 rows violate; e.g. #1 string-is-a-string: violation:write: Number 7.8, expected the String 7.8, in {"P_x":{"$type":"d","VALUE_x":7.8}}…
  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: violation:read string: the decimal with DecimalNumberJsonConverter threw JsonException: The JSON val…
  C01DecimalSpellingProbe/keeps-the-scale-it-reads (C01-decimal-spelling): caught, 64 of 117 rows violate; e.g. #0 keeps-the-scale-it-reads: violation:read number: read 0, expected 0.00 with its scale text={"$id":"1","p":0.00}…
  C01DecimalSpellingProbe/covers-every-decimal (C01-decimal-spelling): caught, 4 of 117 rows violate; e.g. #32 covers-every-decimal: violation:write: key 7.8000000000000000000000000000, expected 7.8, in {"P_x":{"7.8000000000000000000…
prober seeds: 6 caught, 589.6 s in all
  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: violation:write: expected NotSupportedException naming PRec under ReferenceHandler.Preserve, got no …
  C10PreserveGuardProbe/value-converters-unaffected (C10-preserve-guard): caught, 4 of 306 rows violate; e.g. #59 value-converters-unaffected: violation:write: the value object threw NotSupportedException: A value object over System.Uri cannot…
  C10PreserveGuardProbe/own-converters-unaffected (C10-preserve-guard): caught, 15 of 306 rows violate; e.g. #1 own-converters-unaffected: violation:write: the value object threw NotSupportedException: A value object over System.Guid canno…
  C10PreserveGuardProbe/ignore-cycles-unaffected (C10-preserve-guard): caught, 2 of 306 rows violate; e.g. #87 ignore-cycles-unaffected: violation:write: the value object threw NotSupportedException: A value object over Probes.Shapes.PRe…
prober seeds: 4 caught, 377.4 s in all
  L01JsonBytesProbe/as-a-dictionary-key-too (L01-json-bytes): caught, 11 of 581 rows violate; e.g. #56 as-a-dictionary-key-too: violation:JSON differs: wrapped {"$id":"1","p":{"$id":"2","own:\"2026-10-15T08:30:00+00:00\"":1}} vs…
  L01JsonBytesProbe/the-underlying-types-json (L01-json-bytes): caught, 12 of 581 rows violate; e.g. #11 the-underlying-types-json: violation:JSON differs: wrapped {"p":"7"} vs value object {"p":7}…
  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: violation:input valid: wrapped read it, value object threw JsonException: V_sealed_record_class_Noda…
prober seeds: 3 caught, 335.6 s in all
  L06OffTheWireProbe/never-written (L06-off-the-wire): caught, 268 of 325 rows violate; e.g. #0 never-written: violation:write: Option<V_validated_struct_TimeSpan> was written: {"P":[{}]}…
  L06OffTheWireProbe/never-read (L06-off-the-wire): caught, 274 of 325 rows violate; e.g. #0 never-read: violation:read duplicate-property: expected NotSupportedException naming Option<V_validated_struct_T…
prober seeds: 2 caught, 214.0 s in all
  L02NoQuoteProbe/parsing-never-repeats (L02-no-quote): caught, 183 of 586 rows violate; e.g. #0 parsing-never-repeats: violation:TryParse threw FormatException on 'S3CR3T-xyz': The input string 'S3CR3T-xyz' was not in a…
  L02NoQuoteProbe/json-never-repeats (L02-no-quote): caught, 211 of 586 rows violate; e.g. #0 json-never-repeats: violation:JSON read: JsonException quotes the input: V_validated_struct_ulong could not read the JSO…
  L02NoQuoteProbe/no-inner-that-quotes (L02-no-quote): caught, 22 of 586 rows violate; e.g. #5 no-inner-that-quotes: violation:Parse: inner FormatException quotes the input…
prober seeds: 3 caught, 288.9 s in all
  L03OneRuleProbe/parsing-runs-create (L03-one-rule): caught, 109 of 586 rows violate; e.g. #3 parsing-runs-create: violation:ISpanParsable accepted 'False' that Create refuses…
  L03OneRuleProbe/tryfrom-runs-create (L03-one-rule): caught, 30 of 586 rows violate; e.g. #7 tryfrom-runs-create: violation:TryFrom accepted the value Create refuses…
  L03OneRuleProbe/json-runs-create (L03-one-rule): caught, 157 of 586 rows violate; e.g. #0 json-runs-create: violation:JSON read accepted the value Create refuses: {"P_x":0}…
  L03OneRuleProbe/refusals-name-the-fault (L03-one-rule): caught, 196 of 586 rows violate; e.g. #0 refusals-name-the-fault: violation:JsonException does not name the fault: V_validated_struct_ulong refused the JSON value (Sh…
prober seeds: 4 caught, 391.8 s in all
  L04RoundTripProbe/invariant-whatever-the-culture (L04-round-trip): caught, 37 of 586 rows violate; e.g. #24 invariant-whatever-the-culture: violation:ToString(): text '7,80' differs from the wrapped type's invariant text '7.80' under cultur…
  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: violation:type-converter: text '15.10.2026 08:30:00 +00:00' differs from the wrapped type's invarian…
  L04RoundTripProbe/text-reads-back (L04-round-trip): caught, 4 of 586 rows violate; e.g. #118 text-reads-back: violation:type-converter: '7.80' read back as 780, expected 7.80…
  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: violation:input empty-string at the root: the JsonException carries an inner InvalidOperationExcepti…
prober seeds: 4 caught, 431.7 s in all
  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: violation:AsOptionalIdentifier(ZWSP-only) = None; expected Error:Hidden…
  C02OptionalTextProbe/required-blank-is-an-error (C02-optional-text): caught, 11 of 46 rows violate; e.g. #2 required-blank-is-an-error: violation:AsIdentifier(spaces) = ok:; expected Error:Blank…
  C02OptionalTextProbe/optional-blank-is-no-value (C02-optional-text): caught, 14 of 46 rows violate; e.g. #0 optional-blank-is-no-value: violation:AsOptionalText(nbsp-only) = Error:Blank; expected None…
  C02OptionalTextProbe/null-is-blank (C02-optional-text): caught, 6 of 46 rows violate; e.g. #6 null-is-blank: violation:AsText(null) = Error:Hidden; expected Error:Blank…
prober seeds: 4 caught, 313.4 s in all
  L05EqualityProbe/through-every-base (L05-equality): caught, 41 of 309 rows violate; e.g. #0 through-every-base: violation:L05_203_validated_class_flags_enum (validated-class, flags-enum, base-explicit-IEquatable,…
  L05EqualityProbe/equality-is-the-underlying-values (L05-equality): caught, 22 of 309 rows violate; e.g. #2 equality-is-the-underlying-values: violation:L05_74_sealed_record_class_DateTimeOffset (sealed-record-class, DateTimeOffset, base-depth…
  L05EqualityProbe/ordering-follows-the-underlying-type (L05-equality): caught, 238 of 309 rows violate; e.g. #0 ordering-follows-the-underlying-type: violation:L05_203_validated_class_flags_enum (validated-class, flags-enum, base-explicit-IEquatable,…
prober seeds: 3 caught, 198.0 s in all
  C09BaseRecordEqualityProbe/in-a-collection-of-the-base (C09-base-record-equality): caught, 12 of 101 rows violate; e.g. #12 in-a-collection-of-the-base: violation:the base B12_2: IEquatable<B12_2> answers False for equal and False for different values…
  C09BaseRecordEqualityProbe/equal-by-the-wrapped-value (C09-base-record-equality): caught, 21 of 101 rows violate; e.g. #1 equal-by-the-wrapped-value: violation:Equals(object) is false for equal wrapped values; == is false for equal wrapped values; th…
prober seeds: 2 caught, 164.9 s in all
  C20ShouldHoldProbe/fails-unless-it-holds-that-reason (C20-testing-shouldhold): caught, 111 of 261 rows violate; e.g. #0 fails-unless-it-holds-that-reason: violation:ShouldHold(other value of the same type) passed…
  C20ShouldHoldProbe/whichever-of-its-types (C20-testing-shouldhold): caught, 120 of 261 rows violate; e.g. #1 whichever-of-its-types: violation:ShouldHold(a) on a fault holding it threw TypeKitAssertionException: Expected the fault a,…
  C20ShouldHoldProbe/hands-back-detail-and-rule (C20-testing-shouldhold): caught, 111 of 261 rows violate; e.g. #0 hands-back-detail-and-rule: violation:returned fault has detail '' and rule '', expected 'probe-detail' and 'C20RuleKind.Strict'…
  C20ShouldHoldProbe/uninitialized-fails (C20-testing-shouldhold): caught, 28 of 261 rows violate; e.g. #6 uninitialized-fails: violation:default composed fault: ShouldHold gave InvalidOperationException This Fault<C20Alt1, C20A…
  C20ShouldHoldProbe/a-failure-says-the-outcome (C20-testing-shouldhold): caught, 122 of 261 rows violate; e.g. #2 a-failure-says-the-outcome: violation:the failure does not say what the outcome was (Fault { Reason = A, Detail = }): Expected t…
prober seeds: 5 caught, 343.9 s in all
  C11ParseShapesBuildProbe/implicit-result (C11-parse-shapes-build): caught, 28 of 247 rows violate; e.g. #2 implicit-result: violation:the value object over a implicit-result type has parsing: Parse, TryParse, IParsable, a ty…
  C11ParseShapesBuildProbe/ref-text (C11-parse-shapes-build): caught, 22 of 247 rows violate; e.g. #4 ref-text: violation:the value object over a ref-text type has parsing: Parse, TryParse, IParsable, a type conv…
  C11ParseShapesBuildProbe/abstract-string-constructor (C11-parse-shapes-build): caught, 22 of 247 rows violate; e.g. #3 abstract-string-constructor: violation:the value object over a abstract-string-ctor type has parsing: Parse, TryParse, IParsable,…
  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: violation:type-converter: the value object over char, which parses, has no type-converter…
prober seeds: 4 caught, 299.1 s in all

Findings of this step, triaged

The conversion left 17 rows failing in prober try, each triaged with .claude/skills/probe-triage. None is a product defect. No fact, docs sentence or not-promised entry came of them.

  • L01 build: open 2.1.0, and move the package-validation baseline to 2.0.0 #3 and #494 (json.MaxDepth=3): "value object threw JsonException: A possible object cycle was detected … maximum allowed depth of 3". The read now runs where the write was refused by depth, and it is refused by depth too. Triage 1: the write's text-matching site (the stj-maxdepth note) was not applied at the read. It is now, marked as the same site.
  • C10 #267 and #303 (json.MaxDepth=3), harness errors: "UNEXPECTED JsonException … maximum allowed depth of 3" in the read check. Triage 1: the read check re-serialized a document the write had already seen refused by depth. The read now ends on the same site's note.
  • L03, 13 rows of refusals-name-the-fault and json-runs-create:
    • 10 rows (NumberHandling=WriteAsString, Preserve or a renaming modifier): "JsonException does not name the fault: … could not read the JSON value as Int32". Triage 1: under these options the value object cannot read back its own document, and a converter of the application's for the wrapped type cannot either. The old check stopped there (json-half:wrapped-alone-refuses); the converted one went on to the refused value, whose refusal is then the document's, not Create's. The refused-value checks are now inconclusive where the read back had nothing to compare.
    • #461 (StoredJsonConverterFactory under Preserve, a record class): "StoredJsonConverterFactory refused … (NotSupportedException)". Triage 3: the Preserve guard, documented (Generators README#json). Now cited Cite.Docs("src/CodoMetis.TypeKit.Generators/README.md#json").
    • #225 and #243 (an enum dictionary key): "'9' is an invalid start of a property name". Triage 1: the harness put the refused value, a number, unquoted where the key's name stood, so neither side could read it, and refusals-name-the-fault ran although the twin refused too. It now runs only where the twin reads the text.

Nothing reached step 4, so nothing went to the steering session.

Uncited excuses, and how each is converted

Every excuse below had no sentence, record or fact to cite, or was a silent return null with the citation in a comment.

  • Compared, where the check can tell. None of these comparisons failed after the triage above:
    • own-converter-refused-for-wrapped-too and own-vo:read-back-refused (L01): against a converter of the application's for the wrapped type at the same position. Both refuse: inconclusive; otherwise a violation.
    • read-back-refused under UnmappedMemberHandling=Disallow or Preserve (L04): against the twin reading back its own document. Both refuse: inconclusive (was plain-read-back-fails-too); otherwise a violation.
    • L04's JSON round trip that differed under Preserve: against the twin's round trip; the changed input read as a NotSupportedException or an InvalidOperationException: against the twin's read of the same text.
    • L04's text checks, which skipped a closed set (e.Kind != "closed-set") and every missing entry (if (!back.NotApplicable)).
    • L01's read, which exempted a validated dictionary key from "accepted the refused value"; its documented refusals (null for a struct, Create's, a closed set's unknown name), now each held to a JsonException before they are cited.
    • L02's JSON half, which read only a JsonException's message for the input; any exception's message is read now. Where the value object cannot be written, the twin's document places the input, as L01 holds the two the same.
  • Inconclusive, where the row shows there is nothing to compare. The check earns nothing and excuses nothing:
    • both-threw, read-both-threw, ser-both-threw, both-threw-differently (C01, C10, L01, L04, L06): both sides refuse;
    • json:both-directions-left-out-by-options (C01, L06), now each direction the options leave out, the json:member-left-out-by-options note kept;
    • plain-refused, refused-input-not-placeable, ser-refused (L03): the wrapped type is not written, the refused value has no place in the document;
    • L02's rows that refuse nothing (a string read whole, a member the options leave out), which passed silently before;
    • TryFormat over a wrapped type without ISpanFormattable (L04, entry-not-applicable before): no TryFormat is generated, the wrapped type has none either.
  • Cited: byte-array-is-base64, closed-set-key-unpoliced, preserve-documented, own-vo-converter-threw, no-text for a record class, entry-not-applicable (span parsing, a closed set's surface), text:wrapped-round-trip-lossy-too, and the silent excuses whose comment named an entry. Each cites the not-promised entry or docs section in its comment, now as a Cite.
  • C20's entry-not-applicable was unreachable: the operation is substituted where the carrier has none. It is now the harness error it would be.
  • Kept as a note: stj-maxdepth, at the text-matching sites below.

Declared applicability (#50)

Declared on the obligation (claims.json inapplicable), the excuses the axes alone decide:

Claim Obligation Axes Cite
L01 the-underlying-types-json, as-a-dictionary-key-too json.Converters=own-for-value-object docs:src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says ("Your own converter wins")
L01 the-underlying-types-json, read-as-the-underlying-type-reads a value object (not a closed set) over byte at array-element not-promised:a-byte-array-is-base64
L01 as-a-dictionary-key-too, read-as-the-underlying-type-reads a closed set at dictionary-key under a key policy (json.DictionaryKeyPolicy other than the default) not-promised:a-closed-set-key-is-its-name
L02 parsing-never-repeats, no-inner-that-quotes ISpanParsable over Uri, an enum, a flags enum or NodaTime.Instant docs:src/CodoMetis.TypeKit.Generators/README.md#what-is-generated ("A Uri, an enum and a NodaTime type have neither.")
L02 parsing-never-repeats, no-inner-that-quotes ISpanParsable of a closed set docs:src/CodoMetis.TypeKit.Generators/README.md#closed-sets
L02 parsing-never-repeats, no-inner-that-quotes a parsing entry over own-record-class docs:…#what-is-generated (IParsable "when the underlying type can be parsed")
L02 json-never-repeats json.Converters=own-for-wrapped or own-for-value-object not-promised:the-applications-own-converter
L03 tryfrom-runs-create TryFrom of a closed set docs:…#closed-sets
L03 parsing-runs-create the three L02 parsing rows above the same
L04 the-type-converter-writes-the-same ToString-format, TryFormat-short-span of a closed set; the type converter over own-record-class docs:…#closed-sets; docs:…#what-is-generated
L04 text-reads-back TryFrom, ISpanParsable, ToString-format, TryFormat-short-span of a closed set; any text entry over own-record-class; ISpanParsable over Uri, enums, Instant docs:…#closed-sets; docs:…#what-is-generated
L04 text-reads-back the type converter over TimeOnly, DateTime, DateTimeOffset not-promised:dates-and-times-lose-detail-as-text

C01, C02, C09, C10, C20, L05 and L06 declare nothing: no excuse of theirs depends on the axes alone.

The code keeps the same citations, for a row in hand without the claims (the prober's tests), as HttpBaseline does. The declarations leave each claim's pair count as it was (universe): every declared pair is still wanted by another obligation of its claim.

Kept in code, because the shape instance, what the serializer did, or a sibling check of the same obligation decides them:

  • the Preserve guard's refusal, where the value object threw NotSupportedException (L01, L03, L04);
  • the documented read refusals (L01), told by the exception type;
  • "Your own converter wins" for L04's changed input and L03's refused value: each shares its obligation with a round trip that runs under that converter, so a declaration would skip the round trip too;
  • KB-038's base64 read in C10, beside a round trip of the same obligation;
  • L02's span parsing for a closed set and a record class is declared, but L03's refusals-name-the-fault is not: it also judges the JSON refusal in the same rows.

Text-matching sites, kept, each marked with its baseline gap

Site What the text tells Baseline gap
harness/Positions.cs:239 (the old 237) L01's write: the value object's maximum allowed depth under json.MaxDepth=3 (note stj-maxdepth, uncited) no twin in the row shows System.Text.Json charging a converter a level
harness/Positions.cs:268 L01's read, the same refusal: the site of the write, now that the read runs where the write was refused the same
harness/JsonFamily.cs:62 (the old 60) Jx.Depth, the same words, for C10 (PreserveGuard.cs:115, 151, 184, 218) the same
harness/TextRow.cs:285 and 294 (the old 202 and 214) L04's JSON write and read back refused by depth (a silent pass before; now the stj-maxdepth note) the same
harness/TextRow.cs:374 (the old 266, in Quotes) L02: a JsonException is read up to " Path:", since System.Text.Json's Path holds a rejected dictionary key (not-promised:a-rejected-dictionary-key-in-the-path) no twin in the row shows the wrapped type's key in the Path too
harness/OffTheWire.cs:67 (the old 63) L06: the NotSupportedException is TypeKit's by its words, "not a wire type" a serializer's own NotSupportedException is the same type, and no twin refuses as the serializer alone does

The other message reads of these files are comparisons, not excuses: a refusal held to its documented name (Baseline.Refusal: L03's fault Refused, L04's value object named at the root, C10's wrapped type and ReferenceHandler.Preserve, ReadAlike.Named), and two refusals compared message for message with the twin (json:read-refused-by-stj-alone).

Not-promised entries, facts and docs

None added. Every citation is to an entry, a docs section or a fact that exists: KB-038 (C10's base64 read), L01's a-byte-array-is-base64 from #53, and the Generators, AspNetCore and docs sections named above.

Verdicts

Through build-verdict, at a67ac5a:

  • test --project test/CodoMetis.TypeKit.Prober.Tests: TESTS OK: 257 passed, 0 skipped. At 2002740, before the merge brought WS2b: a cited fact is executable, fixtures beside fact tests, seeds by footprint #55's tests, one run failed ToolchainTests.A_dotnet_without_the_pinned_sdk_falls_back_to_the_installers with a NullReferenceException (244 of 245 passed). That test starts processes, and it failed so in test: the http family checks each obligation (WS2 step 4) #53 too, under load; at the merge it passed.
  • test --project test/CodoMetis.TypeKit.Conventions.Tests: TESTS OK: 1484 passed, 0 skipped, ProbeCiteTests and ProbeNoteKeyTests among them (1477 at 2002740, before the merge).
  • docs-query check: 0 problems in 725 links, 512 ids, 5 fact citations (2 not-promised, 3 harness: KB-037 twice, KB-038) and 38 facts. Every cited fact is executable (decision 107).
  • prober universe accepts every declaration and quote.

Noticed, not changed

  • The Generators README's "What is generated" table promises IFormattable, ISpanFormattable and IUtf8SpanFormattable for "every value object". ValueObjectFormattableAspect emits ISpanFormattable only where the wrapped type has it, so a value object over bool, string or NodaTime.Instant has none. L04's TryFormat rows over those end inconclusive, since L04's own sentence ("every generated … TryFormat") promises only what is generated. The table or the aspect should change; that is not this step's to decide.
  • L02 holds 66 fewer pairs current (8281 → 8215). These are the own-converter pairs of its TryFrom and formatting rows, whose only check is now cited inapplicable instead of a silent pass.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 5 commits October 17, 2026 11:16
Every probe of the json and text families (C01, C02, C10, L01 to L04, L06)
and the generation and option-result probes whose harness is theirs (C09,
C20, L05) runs one Row.Check per obligation its row reaches, named by the
claim's obligation id and given its baseline: the wrapped twin, the value
object's own document read back, the documented refusal, or the docs
sentence. Every excuse of outcome inapplicable, expected-refusal or
inconclusive is a cited c.Inapplicable/c.Refused, a comparison, or a
c.Inconclusive with its reason, except the text-matching sites, which stay
and are marked. No NoteKeys member is removed; the pass keys stay as notes.

The excuses that depend on the axes alone are declared on their obligation
in claims.json (#50): L01's own converter for the value object, its byte
array and its closed-set key under a key policy; L02's and L03's entries
that are not generated (span parsing over a type that has none, a closed
set's span parsing, parsing over a record class) and L02's own converters;
L03's TryFrom of a closed set; L04's closed-set text entries, parsing over
a record class, span parsing, and a date's type converter.

A closed set parses by name (Parse(string), TryParse(string, out)), which
TextOps now reaches, so its text entries are checked instead of skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…99's document is

json-half:wrapped-alone-refuses (L03) and json:read-refused-by-stj-alone
(C10, L06) are pass keys: the refusal is shown to be the framework's by the
twin in the same row, as doc:refused-by-aspnet-alone is in the http family
(#53). The first conversion ended those checks inconclusive, which lost 21
rows of L03 and L06 that passed on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… outcome, and every text-matching site is marked

L04's read back no longer notes plain-read-back-fails-too (expected
refusal) before ending inconclusive, nor text:wrapped-round-trip-lossy-too
(inapplicable) before its cited inapplicable: the outcome is the check's
own call. OffTheWire's "not a wire type" and TextRow's Path cut are marked
as the text-matching sites they are, with their baseline gaps.

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