sharp-runtime is a C++23 static library that reimplements a practical subset of the .NET System.* namespace so that ported C#/XNA game code compiles against C++ headers with minimal changes. It is the foundation for CNA (C++ XNA port) and mobile-eggbert.
- Zero errors, zero warnings before any commit.
cmake --build build --parallel 2must be clean. - No test-count regression.
scripts/run_component_tests.sh buildmust show no failures or skips. The current verified baseline is 17,840 tests across 38 executables with THE GATE GREEN — 17,840 run, 17,840 passed, 0 failed, 0 skipped, measured on 2026-08-22 by post-#1941 consumer-audit ticket #2418 after a cache-disabled full repository build at two jobs and the complete local CI gate. It is +59 on #2417's 17,781 final-audit closure: Core.Base +22, Globalization +1, IO +5, Net +6, Net.Http.Headers +2, TimeZone +18, and Xml +5; all other executables are unchanged. Graph 41 / 96, test-only seams 5 / 22, negative fixtures 55 / 284. The Doxygen 1.9.8 no-regression baseline is 2,675 and is enforced both locally and in CI. Ticket #2419 additionally makes the complete production graph a permanent Clang gate: Clang 19.1.7 builds all 219 first-party translation units with-Werror, 0 warnings and 0 errors, from bothlocal_ci_check.shand the GitHub full job.
Historical test-count ledger retained verbatim
- No test-count regression.
scripts/run_component_tests.sh buildmust show no failures. The current verified baseline is 17,738 tests across 38 executables with THE GATE GREEN -- 17,738 run, 17,738 passed, 0 failed, 0 skipped, measured on 2026-08-20 by the release-identity change that gives this repository a version for the first time,v0.1.0-alpha.1. +6 on the 17,732 below, inSharpRuntimeTests_Core_Base(6,148 -> 6,154); no other executable's count moved, and no production statement changed -- the six cases pin that the CMake-side decision and the generatedSharpRuntime/Version.hppagree, and they are deliberately structural rather than an equality check against"0.1.0-alpha.1", so a later bump cannot satisfy them by editing the test. The version is decided in ONE place --project(SHARP_RUNTIME VERSION 0.1.0)plusSHARP_RUNTIME_VERSION_PRERELEASEin the rootCMakeLists.txt-- and everything else derives from it;docs/releasing.mdnames the two copies (CHANGELOG.md,Doxyfile'sPROJECT_NUMBER) that a bump must still update by hand. The pre-release identifier is a normal variable rather than a cache entry on purpose: a cached copy would keep an already-configured build directory reporting the previous release after a bump, which is exactly the stale claim a release must not make. The generated header ridesSharpRuntime::Headers, the interface target every component and consumer already links transitively forSHARP_RUNTIME_HAS_NATIVE_INT128, so a consumer writes#include "SharpRuntime/Version.hpp"without knowing which include root resolves it -- and the cost of that reach is a full rebuild, since a new include root changes every compile command, which is why it was done once and deliberately. The boundary validator had to be taught about it:scripts/validate_module_boundaries.pydeliberately runs without a configured build, so a header that exists only in one is unresolvable to it -- the exception is a set of exact paths (GENERATED_INCLUDE_PATHS) rather than a loosening of the wholeSharpRuntime/prefix, andtest/validate_module_boundaries_test.pypins both halves, that the generated path resolves and that an unknownSharpRuntime/header still fails. Both branches of the version logic are measured, not just the shipped one: emptyingSHARP_RUNTIME_VERSION_PRERELEASEand configuring gives0.1.0with no-, which is the form every final release will take and which no shipped configuration exercises. A pre-existing red gate was found on the way and is reported rather than re-baselined:scripts/check_doxygen_warnings.shemits 2,675 warnings against a maximum of 1,942 measured 2026-07-25, and it is not my change -- measured twice, with and without theDoxyfileedit, the count is identical, and*/tests/*is excluded so the new test file is not even scanned. It runs in.github/workflows/components.ymlbut not inscripts/local_ci_check.sh, which is #2415's shape exactly: a check nothing local runs is the check that rots. It was 17,732 immediately before, measured on 2026-08-20 by ticket #1944, the last open post-audit implementation ticket. +7 on the 17,725 below, inSharpRuntimeTests_Core_Base(6,141 -> 6,148). All five exact-parsing types gain multi-formatParseExact, sharing one ordered first-success loop -- five copies of one taxonomy is how five doors come to disagree, and .NET writes it twice with the two agreeing, so sharing is faithful rather than a shortcut. Ordering is pinned with an input two formats both accept:01/02/2024is 2 January underMM/ddand 1 February underdd/MM, so the two orders give different dates rather than a different code path. An empty ELEMENT aborts the whole loop rather than being skipped -- .NET returns its bad-format-specifier failure immediately, "skip it and carry on" is the plausible wrong implementation, and the pin puts the empty element before a format that would have matched so the wrong rule succeeds where the right one fails. An empty COLLECTION is a format failure, not an argument one, and .NET's null-array arm has no C++ counterpart and is deliberately not reproduced. TWO FAILURE KINDS, BECAUSE .NET GIVES THEM TWO MESSAGES, and this was found by measurement: with the empty-collection guard simply removed the loop body never runs and the fall-through gives the same answer, so the guard would have been a proven equivalence -- carrying .NET's own "No format specifiers were provided." against "was not recognized" makes it load-bearing and gives the right diagnosis, since telling a caller who supplied no formats that their input was bad is the wrong one. THE OVERLOAD HAZARD THE TICKET ANTICIPATED WAS REAL: measured,ParseExact(s, {"a", "b"})and{"one"}were ambiguous while three or more elements were not, because twoconst char*in braces matchbasic_string(InputIt, InputIt)over two unrelated pointers -- and if that candidate had ever won, the result would be undefined behaviour rather than a wrong answer, which is what decided a fix over documenting a papercut.std::initializer_list<std::string>overloads resolve it, a braced list binding by a list-initialization sequence that outranks any user-defined conversion, with a case asserting the unbraced single-format overload is still reachable. Four mutations, three caught; M3 is a proven equivalence recorded at the site -- with the empty-input guard removed every format fails an empty input anyway, and .NET's is an equivalence too, both its arms beingSetBadDateTimeFailure. The span-like shapes are recorded rather than taken: every exact door here takesconst std::string&, and a second text representation beside it repeats the very hazard this ticket closed, in a place where the wrong branch is silent rather than ambiguous. Downstream zero sites. It was 17,725 immediately before, measured on 2026-08-20 by ticket #2416. +5 on the 17,720 below, inSharpRuntimeTests_Core_Base(6,136 -> 6,141); no other executable moved, so nothing that formatted a date changed its answer.DateTime::ToStringhad NO standard-format table at all -- measured,ToString("o")emitted the literal"o",ToString("s")returned"0"by readingsas seconds,"d"returned"15"by reading it as the day, and"%d"rendered"%15". Three distinct defects in one member: an unrecognised specifier emitted as a literal; a one-character custom specifier accepted where .NET requires%d, so"s"and"d"gave a plausible wrong answer rather than a visible failure; and%not being the escape at all. It mattered now because #2414 and #1942 gave the PARSE side a table, so the two halves of one type disagreed about whatomeans -- the #2393 shape, one type over -- and #1939 had already recorded the exact missing rule. THE TABLE ALREADY EXISTED AND WAS SIMPLY NOT CALLED:DateTimeFormatInfo::GetAllDateTimePatterns(char)carries all nineteen specifiers, culture-aware, so this is a wiring repair rather than a new table -- which also means a provider's patterns are honoured for free, #1940's route again. Two exceptions, two contracts:ToStringraisesFormatExceptionwhereGetAllDateTimePatternsraisesArgumentException, and emitting the character as a literal was neither. Two custom tokens came with it -- the formatter had not/ttand noK, both read by the parse side since #1939 and #1942, two more rows where the halves disagreed -- andKfor a LOCAL value emits nothing, stated rather than discovered, because a local marker needs a zoneCore.Basecannot name and there is no parameter here to carry one;Unspecifiedemits nothing too, which is the same rule asKmatching the empty string when parsing. Seven mutations, all caught, with the round trip asserted as a property rather than two tables that happen to match. A first cut of one case wroteToString("K")and threw, a one-character format being the standard reading andKnot one of the nineteen -- the test tripped over its own subject, and the row is kept as evidence the rule bites. Downstream zero sites. It was 17,720 immediately before, measured on 2026-08-20 by tickets #1943 and #1945 (SA-16.2/16.3/16.5/16.6), which closes #1943. +6 on the 17,714 below:SharpRuntimeTests_Core_Base6,132 -> 6,136 andSharpRuntimeTests_Xml524 -> 526 (#1945's declaration pin inverted, three cases in its place). HALF A --DateTimeOffset::ParseExact, and the measurement that made it possible is that AN OFFSET IS NOT A TIME ZONE: a format carrying an explicit offset needs no zone database whatever, the offset being read from the input and stored. The value is CAPTURED, not adjusted -- this is not theDateTimematrix with a different result type, the wall-clock time staying exactly as written -- and only the no-offset case needs a zone, .NET's own comment saying "AssumeLocal causes the offset to default to Local. This flag is on by default for DateTimeOffset." SA-16.6 accepted the cost knowingly and it is larger here than forDateTime, since every offset-less format needs a zone, so the message names all three routes out -- a caller hitting it has three genuinely different fixes and no way to guess them from "zone was null". The parameter isstyleshere whereDateTime's isstyle, .NET varying it by overload, both transcribed rather than harmonised. HALF B --XmlConvertround-trips a kind, and the decision went further than #1945's own sentence: that ticket predicted its pin would fail "the day #1942 teachesParseto read aZ", and the reading half deliberately does NOT go throughDateTime::Parse-- SA-16.4 left it alone, still discarding the zone, so a round trip built on it could never carry a kind however the writer rendered, and .NET does not useDateTime.Parsehere either, building anXsdDateTimethat parses the zone itself. The writing half is the full XsdDateTime form, two changes rather than one -- appending only the marker would have repaired the round trip and left the document wrong, an XSDdateTimeliteral requiring theT-- with the fraction trimmed and anUnspecifiedvalue writing no marker at all. A numeric offset is CONVERTED rather than stamped, because it names an instant and stamping would make+05:00and+02:00give the same wall clock -- the offset read and thrown away again, which is the defect the ticket exists to end -- and the marker is matched as a SHAPE rather than by scanning back for a sign, since2024-06-15ends in06-15. BothToDateTimedoors go through one, the #2393 shape being what a second route would recreate. Fourteen mutations across the two halves, all caught; one was invalid as first written TWICE and is recorded rather than counted -- it leftzone->GetUtcOffsetrunning after the guard and segfaulted, and a segfault is undefined behaviour rather than a verdict, which the harness must not read as a pass. FOUND ON THE WAY AND FILED RATHER THAN BUNDLED: #2416 -- a probe measured thatDateTime::ToStringhas NO standard-format table at all,ToString("o")emitting the literal"o",ToString("s")returning"0"by readingsas seconds, and"%d"rendering"%15"; so after #2414 and #1942 gave the parse side a table, the two halves of one type disagree about whatomeans. Downstream zero sites. It was 17,714 immediately before, measured on 2026-08-20 by ticket #1942. +6 on the 17,708 below, inSharpRuntimeTests_Core_Base(6,126 -> 6,132; two #2414 pins inverted rather than deleted, since they asserted the boundary this ticket moves). #2414 left the style-taking overloads absent and PINNED absent and said why; SA-16.1 took that decision and the zone is a parameter -- #1941 phase 2's own shape, so the port answers the question once rather than twice, with both alternatives declined on the record (accepting only the stamping styles would make a legal .NET style illegal here; a registration hook is the hidden global state #1940 already refused). The grammar gained a zone token --z/zz/zzz/K, rejected in every mode before -- admitted behind a flagDateTime's doors set andDateOnly's andTimeOnly's do not, pinned;gstays rejected everywhere, being a different absence (no era table). Widths differ and collapsing them changes which inputs parse (zzzandKcarry:mm,zandzzdo not), andKalone matches the EMPTY string, which is .NET's rule rather than leniency becauseKrenders empty for anUnspecifiedkind -- soogot itsKback, the row #2414's header said a later ticket must revisit. Two cases, and the second is not the first with a default: with a zone in the input theAssume*styles do not apply at all, .NET saying so in its own comment; with none, four of five outcomes return without converting anything and the fifth is the row a reader most expects to be different --AssumeUniversalALONE comes back LOCAL, because .NET sets the offset to zero and falls through to the local adjustment.RoundtripKindfires only for a literalZ: .NET testsParseFlags.TimeZoneUtc, so+00:00-- the same instant -- is converted rather than stamped, and no assertion about the VALUE can separate the two, only the kind. An offset outside +-14:00 is a failure rather than a clamp. The deviation is one parameter and is stated: a null zone is an error only when a style actually needs one, so the default costs nothing and a converting style raisesArgumentNullExceptionnamingzoneand telling the caller to passCurrentTimeZone()-- a diagnostic where #2414 had a silent mismatch, with a case asserting the same input and door succeed underRoundtripKind, which is what shows the throw is about the conversion rather than the token. Nine mutations, all caught; M7 was NOT CAUGHT at first and found a defect in my TEST rather than the code -- the +-14:00 bound looked pinned by"+15:00", which the scanner's own coarsehours > 14already refuses, so+14:59is the row that separates the two guards and both signs are now asserted. The tests reuse #1941 phase 2'sFixedZonerather than declaring a second, two zone doubles in one file being the shape that lets two suites drift apart. Downstream zero sites. It was 17,708 immediately before, measured on 2026-08-20 by ticket #1943 (TimeSpan half). +8 on the 17,700 below, inSharpRuntimeTests_Core_Base(6,118 -> 6,126).TimeSpan's whole parse surface wasParseandTryParse-- noParseExactin any spelling -- andTimeSpanStylesexisted inmodules/globalizationwith nothing able to consume it, #1997 A-3's shape.TimeSpanStyles.hppmoved intoCore.Base, #1940's shape C for the third time and again with not one#includeline changed; graph unchanged at 41 / 95. THE OBVIOUS IMPLEMENTATION -- REUSE THE DATE/TIME SCANNER -- WOULD BE WRONG IN A WAY THAT PASSES MOST TESTS, and .NET keeps the two apart for reasons visible in the token table: an unquoted literal is an ERROR here,TryParseByFormat's switch ending indefault: SetInvalidStringFailure, so"hh:mm"is not a valid TimeSpan format and the colon must be quoted or escaped -- the date/time scanner matches an unquoted literal instead, so a shared scanner would silently accept a format .NET rejects; there is no sign token at all; each component may appear once;d's digit rule is not the others' (one specifier means 1..8 digits, more than one means exactly that many, so a uniform "exactly tokenLen" gets days wrong and passes every hour/minute/second row); andfrequires its digits whereFdoes not, .NET calling the same reader for both and ignoring the result forF.AssumeNegativeis therefore the ONLY route to a negative result --ParseExact("-01:30", "hh':'mm")fails -- which is what makes the style load-bearing rather than decorative; the standard formats ignore it, as .NET does, becauseccarries its own sign. The bounds are per COMPONENT rather than a total range (days <= 10675199, hours <= 23, minutes <= 59, seconds <= 59), so"25"against"hh"fails rather than carrying into days.c/t/Tare implemented as one format under three names;gandGare pinned ABSENT because they are the localized standard formats, needing a culture decimal separator this port has no database for and optional components the custom scanner cannot express. ATry*method that throws: an illegal style raises where a parse failure returns false, and validation runs before the result is written -- two claims, each asserted. Nine mutations, all caught; two were invalid as first written and reformulated rather than counted, M2's anchor never matching the file (an absent anchor is a harness state, not a finding) and M9's first spelling rejected by-Werror=unused-variable. Downstream zero sites. #1943 still hasDateTimeOffset::ParseExact, which needs a zone for the no-offset case where .NET'sDateTimeStyles.Nonegives the LOCAL offset -- an offset is not a time zone, so a format carrying an explicit one would need no zone at all, and that route is recorded rather than taken because the default still needs #1942's answer. It was 17,700 immediately before, measured on 2026-08-20 by ticket #2415, unchanged from #1945's reading because #2415 touched no production code -- it repairs a GATE, and the build-policy violation that gate was committing on every run.check_selective_components.shhad been RED since 2026-08-19, behind a green test count: #1889 legitimately gaveText.Jsona publicCollections.Coredependency -- fail-fast enumeration needsdetail::MutationCounterand the boundary validator rejected the private declaration outright -- whileforbidden_text_json_collectionsstill assertedList.hppwas unreachable. It went unnoticed because that script is not part of this rule's gate, so every checkpoint since was recorded green while it was red. The fixture is RETARGETED rather than deleted, because this file names it as an invariant:List.hppwas only ever a proxy for "Collections", and the proxy moves toBlockingCollection, the type that sentence is actually about -- strictly stronger, sinceCollections.Blockingpublicly needsThreading, so the include can only compile ifText.Jsonhas acquired aThreadingrequirement, which is what the surroundingassert_target_absent sharp_runtime_threadingexists to prevent, now pinned by compilation rather than by a target name. A SECOND, INDEPENDENT DEFECT in the same script:MATRIX_ROOT="$(mktemp -d)"with nothing in the repository settingTMPDIR, so every run built eight selective component trees into/tmp-- the one place the build-resource policy exists to keep builds out of, andbuild-tmp/is in the closed directory list described as exactly this redirect. The mechanism was designed and never wired up. And the proximate cause of the first defect surviving a day is that NOTHING RAN THE SCRIPT:local_ci_check.shran the boundary validator, the seam checker and the negative-fixture checker but not this one. It now runs last, costs a measured ~10 minutes at two jobs stated in the script rather than left as a surprise, and is deliberately not behind an opt-out -- a check that can be skipped is the check that rotted. It was 17,700 immediately before, measured on 2026-08-20 by ticket #1945. +6 on the 17,694 below, inSharpRuntimeTests_Xml(518 -> 524). #1945 makes fourXmlConvertmembers honour arguments they accepted and discarded -- twoformatparameters and twoXmlDateTimeSerializationModeparameters, spelled/*format*/and/*mode*/in the bodies -- soToDateTime("2024-06-15", "HH:mm:ss")succeeded, the value having been parsed by an entirely different grammar with no diagnostic: the SR-AUD-168 shape four times over. The mode half carried a premise that had stopped being true, its comment sayingSystem::DateTimedoes not track aDateTimeKind-- #1941 phase 1 gave it one and phase 2 made it convert by it. This could land while #1942 stays blocked becausemodules/xmlcan reach a zone whereCore.Basecannot: phase 2 had to take anILocalTimeZoneas a parameter, and hereTimeZonedepends onCore.Basealone so a private dependency is no cycle andSystem::TimeZone::CurrentTimeZone()already is anILocalTimeZone-- so the deviation #1941 recorded is resolved by the module that can actually name the zone, andXmlConvert's signatures stay exactly .NET's with no zone parameter. Only two of the mode matrix's eight cells move the ticks and the rest stamp or pass through, transcribed cell by cell and written once rather than twice, because .NET writes the switch twice and two copies of one matrix is how two doors come to disagree. A limitation was found by a test of mine FAILING and is declared rather than hidden:RoundtripKindexists to carry a kind through a string and here it cannot, sinceDateTime::ToString()emits no kind marker where .NET'sXsdDateTimedoes andDateTime::Parsereads none -- so through the parse doorLocalandUtcalways stamp and never convert, while through the format door they convert, andRoundtripKindandUnspecifiedare observationally identical, measured over every input kind and both doors. That is #2414's no-zone-token boundary one level up, and closing it is #1942's work.ToDateTimeOffset(s, format)is composed rather than duplicated: this port has noDateTimeOffset::ParseExact, and with no zone token in the format .NET'sDateTimeStyles.Nonegives the local offset, so the one exact grammar plus the local zone's offset is what .NET computes rather than an approximation of it. Seven mutations, five caught and two proven equivalences -- M4 and M6 swap theRoundtripKindandUnspecifiedarms, and an assertion catching them would have to distinguish two modes this port cannot; two more were invalid as first written and reformulated rather than counted, M2's first spelling being a no-op (ToUniversalTimeon aUtcvalue returns it) and M7's rejected by-Werror. A mistake of my own is recorded because it is the SECOND occurrence:cat >>to a path that did not exist created a stray untracked test file with no includes, exactly what #2412 recorded -- twice is a pattern, not a slip. Module graph 41 / 94 -> 41 / 95 with the catalogue regenerated; downstream zero sites. Running the selective-component check for this change found it ALREADY RED, and red on a clean tree --forbidden_text_json_collectionscompiles because #1889 legitimately gaveText.Jsona publicCollections.Coredependency, and it has been failing since 2026-08-19 behind a green test count, because that script is not part of this rule's gate; filed as #2415 rather than passed over. It was 17,694 immediately before, measured on 2026-08-20 by ticket #1997 group A-2, which closed #1997. +5 on the 17,689 below, inSharpRuntimeTests_Uri(314 -> 319);SharpRuntimeTests_Netis unchanged at 340, which is the evidence the scanner move below changed no behaviour.Uri::CheckHostNamelands, andUriHostNameTypehad documented it since the enum was ported while nothing in this runtime could produce a value of that type. The recorded cost was understated and the measurement corrects it: #1997 priced A-2 as "a new public module edge to reachSystem::Net::IPAddress, or a second address-literal parser", and the first is impossible rather than expensive --modules/netdeclaresPUBLIC_DEPENDENCIES ... Uri, so that edge is a cycle, the dependency inversionGuid.cpprefused for cryptography, which is why a first cut written againstIPAddresswas rejected by the boundary validator. The route taken is a third one neither option named:modules/net's IPv4 and IPv6 scanners are pure string-to-number scanners with no platform call and no dependency onIPAddressitself, so they moved verbatim intoCore.Base-- both modules already depend on it, so the graph does not change (41 / 94), there is one definition instead of two, andIPAddress.cppis -197 / +11 lines;validatedScopeId,formatIPv4andformatIPv6stayed behind, being about that type rather than about the grammar. IPv4-before-DNS is where .NET's order decides an answer rather than tidying:CheckHostNamepassesallowIPv6=false, unknownScheme=false, which selectsParseNonCanonical, so"1","0x7F.1"and"3232235777"are IPv4 though all three are also good DNS labels, while"1.2.3.4.5"falls through to Dns. Two rules are new with this member: the label rules -- first character an ASCII letter or digit, length 1..63, and a trailing dot accepted -- which this port's constructor never needed, since it only ever asked about characters; and an UNBRACKETED IPv6 literal is still IPv6, which reads like a bug until the reference is read, .NET retryingIsValid($"[{name}]")(Uri.cs:1320-1324), because the question is what a string is rather than whether it may appear in an authority. The constructor's character loop was factored so there is ONE definition -- two grammars for one question is the #2393 shape -- while the two remain different questions, the constructor applying no label rules, soUri("http://-x/")parses whereCheckHostName("-x")isUnknown, asserted together rather than left to be discovered. Seven mutations, all caught; M6 was NOT CAUGHT at first and found a defect in my TEST rather than in the code -- "the entire name must be consumed" was asserted with"[::1]junk", which fails on the front/back guard and so never enters the bracketed branch at all, so a body measuring to the first]passed it; the input that separates them is bracketed at both ends with junk inside,"[::1]]". Downstream zero sites, the member having not existed. It was 17,689 immediately before, measured on 2026-08-20 by ticket #1997 group A-4. +6 on the 17,683 below, inSharpRuntimeTests_Uri(308 -> 314). A-4 is the change SA-3's vtable exclusion was blocking and SA-15.3 released, and it closes SR-AUD-146.UriParseris the extensibility point for custom URI schemes and it could not extend anything:Registerdid not exist at all, so a subclass could be written and then had nowhere to go -- A-3's shape one member over -- and three override hooks werepublicwhere .NET's areprotected, so any caller holding aUriParser&could invoke another parser's hook directly. .NET states that rule in a comment of its own, describing its internal forwarders as existing "to avoidprotected internalsignatures in the public docs" (UriSyntax.cs:245-246), and the migration is that same shape rather than a workaround: a subclass publishes a forwarder, exactly asInternalGetComponentsis. The registration is OBSERVABLE, which is the whole point -- aRegisterthat validated and stored into a table nothing reads would be accepted-and-ignored, the SR-AUD-168 defect, soIsKnownSchemenow consults the registry and mutation M1 breaking that link is caught by four cases. Three rules are transcribed rather than derived: a one-character scheme is refused thoughCheckSchemeNameaccepts it -- two rules disagreeing on exactly one input, with the narrower first; the port range is 0..65535 plus the sentinel -1, because .NET's test casts touintand the cast IS the rule, so a naiveport > 0xFFFFaccepts -2; andOnRegisterruns BEFORE the scheme is stored, so inside the callback the parser does not yet know its own scheme, which is what makes the parameter load-bearing. Two distinctInvalidOperationExceptions are kept distinct because they answer two different questions -- is this parser already registered, is this scheme already taken. A mistake of my own is recorded rather than quietly fixed: .NET keeps built-ins and customs in ONE table, this port MUST split them having no parser objects for the sixteen built-ins, and a first cut checked only the custom map and would have let a caller claimgopher-- the split is permanent, so the same mistake is available to every later change.sizeof(UriParser)8 -> 48, pinned as a relationship as well as a literal; consumers deriving from it rebuild. SA-15.3's fourth condition is discharged as empty and said so: no exception type was introduced, reparented or removed, so nocatchclause changes meaning. Eight mutations, all caught -- M8, republishing a hook, is invisible to gtest because a widened hook behaves identically wherever both compile, and is caught by the negative fixture (52 / 264 -> 53 / 269), verified by running the checker with the mutation applied. One verdict was harness noise and is recorded rather than counted: a re-run of M7 anchored on a substring occurring twice, so the edit never applied and the run reported NOT CAUGHT -- an ambiguous anchor is a harness state, not a finding. Four further .NET hooks stay absent with reasons:OnNewUriwould have no caller, since this port'sUrinever consults a parser, andInitializeAndValidate/Resolvetake anout UriFormatExceptionwith no uninvented C++ counterpart and reachuri._syntax, private state this port has not -- and that nothing calls the hooks at all is declared in the header rather than left to be discovered. Downstream zero sites in both consumers, so no #1773-shaped ticket was owed. Module graph unchanged at 41 / 94. #1997 now has only A-2 left. It was 17,683 immediately before, measured on 2026-08-20 by ticket #2414. +7 on the 17,676 below, inSharpRuntimeTests_Core_Base(6,111 -> 6,118). #2414 givesDateTimeaParseExact, which it did not have in any spelling -- its entire parse surface wasParse(s)andTryParse(s, result), so #1942's style contract had nowhere to land, the same cycle #2412 resolved forDateOnly/TimeOnlyone type over. The obstacle was the scanner rather than the type:MatchExactFormattook abool forDateand ran one of two blocks, the date block rejecting every time token and the time block every date token, so a format naming both families could not be matched by any spelling. Admitting both resolves no ambiguity, which is what makes the widening safe -- the sets are disjoint,Mbeing a month andma minute in two case-sensitive languages -- so the other family's tokens now fall through and each rejection survives guarded on that family not being admitted;DateOnly/TimeOnlyare behaviourally unchanged, pinned, and mutation M3 (admit both forDateOnly) is caught. Two of .NET's standard patterns are transcribed with a NAMED loss:ois...fffffffKthere, and sinceKrenders empty for anUnspecifiedkind the pattern here is .NET'sofor that kind while refusing theZand+hh:mmforms;u'sZis a literal, not a zone token, so it is required and sets no kind. This port's exact grammar carries no zone token at all (z,K,grejected in every mode), which is also why #1942'sRoundtripKindwould have nothing to preserve. The style-taking overloads are deliberately absent and pinned absent, and that is a decision rather than an omission:AssumeLocal/AssumeUniversalonly stamp a kind, butAdjustToUniversalmust convert and conversion needs a local zone -- .NET reachesTimeZoneInfo.Localinternally whereCore.Basecannot, which is exactly what #1941 phase 2 resolved one level down by taking the zone as a parameter -- so the overload needs the same decision made about its signature, and it was not taken unilaterally; .NET's three-rule, three-messageValidateStyles(DateTimeFormatInfo.cs:1720-1743) is transcribed into #2414's record for whoever takes it. A complete date is mandatory while time components default to midnight, which is .NET's own asymmetry:ParseExact("2024-06-15", "yyyy-MM-dd")is a valid call returning 00:00:00, whereas a time-only format is refused becauseNoCurrentDateDefaultdecides that case and is #1942's -- refusing it is what stops an invented default shipping under a ticket that has not been asked about it. Eight valid mutations, all caught; M2 is a proven equivalence recorded at the site, since every date-token arm ends incontinueso a date token cannot reach the time block's rejection while the date block runs, and the condition is kept because it states why the rejection is correct. M7 was invalid as first written (-Werror=unused-parameter) and was reformulated rather than counted. A first run of M4-M8 was invalid and is recorded rather than discarded: the restore after M3 usedgit checkouton a file whose change was still uncommitted, so five consecutive runs reported BUILD FAILED -- the #2374 restore mistake in a new form, caught by the same rule that five identical failures in a row are a harness state, not five findings. Downstream: zero sites in both consumers, the member having not existed. It was 17,676 immediately before, measured on 2026-08-20 by ticket #1980 G-3. +5 on the 17,671 below, inSharpRuntimeTests_Runtime(199 -> 204). G-3 is the first change to land under SA-15.3, which lifted SA-3's exclusion of vtable and base-class changes, and it closes #1980 -- G-1, G-2, G-4 and G-5 landed 2026-08-19.AmbiguousImplementationExceptionis reparented fromSystemExceptiontoException, sealed, and gains .NET's(message, inner)constructor;OSPlatformAttributeis introduced and the five platform attributes derive from it and are sealed. SA-15.3's fourth condition -- enumerate everycatchwhose meaning changes -- is the part no layout assertion can see, and it was discharged by measurement: the clause that moves iscatch (const System::SystemException&), and there are zero such clauses anywhere that can receive this type; the 17 first-party ones are hierarchy tests for other types andcna's single one catches its ownNoAudioHardwareException. The pin asserts all three rows --SystemException,Exceptionand the type itself -- so a later reparenting cannot quietly take the other two. Before G-3, five of .NET's six platform attributes derived fromSystem::Attributedirectly and each carried its own copy ofplatformName_-- five duplicates of one fact, and no type through which a caller could handle any platform attribute, which is SR-AUD-163.protected, notprivate protected: C++ has no "derived classes in the same assembly", and the half that is not expressible is stated rather than pretended.TargetPlatformAttributeis .NET's sixth derived type and is absent, said so five is not mistaken for the set. Nothing grew: the exception stays 168 becauseSystemExceptionadds no members overException, and the attributes stay put becauseplatformName_moved into the base rather than beside it -- the rebuild is required by the vtable, not the size, and the pins assert relationships rather than only literals. Six mutations, all caught, five at compile time, which is the only way C++ reports a shape; reverting the base is caught twice over. One correction of my own is recorded: a first version of the ticket note said G-4 was still outstanding -- copied from the review plan's original table instead of the ticket's own record, and corrected on re-reading it. Downstream zero sites, recorded as #2413. It was 17,671 immediately before, measured on 2026-08-20 by ticket #1941 phase 2. The current verified baseline is 17,671 tests across 38 executables with THE GATE GREEN -- 17,671 run, 17,671 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #1941 phase 2. +6 on the 17,665 below, inSharpRuntimeTests_Core_Base(6,105 -> 6,111). #1941 phase 2 makesDateTimeconvert by itsKind, which unblocks #1942, #1943 and #1944. The recorded blocker looked at the wrong type: it said "a date-sensitive timezone/DST model" and the natural place to look isTimeZoneInfo, whoseGetUtcOffset(const DateTime&)ignores its argument and whoseIsDaylightSavingTime/IsAmbiguousTime/IsInvalidTimeare always false -- all documented limitations of that type.System::TimeZone::CurrentTimeZone()is the per-date one, resolving the offset and the DST flag for the instant given; it describes only the process-local zone, which is precisely the zone these two members convert against, so the model the record wanted was present all along, on the other type. Landed as SA-15.1's abstraction:System::ILocalTimeZoneinCore.Base, two members and deliberately not a zone in miniature, whichSystem::TimeZoneimplements at no cost because it already declared both with the same signatures. The deviation is stated rather than discovered later: .NET'sToLocalTime()takes no argument because it reachesTimeZoneInfo.Localdirectly, andCore.Basehas nothing to ask -- so the zone is a parameter, the alternative (a registration hook with hidden global state and a static-initialisation-order dependency) having been rejected for the reason #1940 gave; the no-argument forms stay absent and phase 1's absence pin, which names exactly those, still holds. TheUnspecifiedasymmetry is .NET's:DateTime.cs:1707tests only theLocalbit and:1772returns early only forUtc, so an unspecified value is read as UTC one way and local the other -- a repair that harmonised them would pass every ordinary case and fail only there, so each direction has its own mutation. Overflow clamps at both ends.LocalAmbiguousDstis still not produced, because it needs an ambiguity answer and this port'sIsAmbiguousTimeis documented as alwaysfalse; producing it would fabricate a distinction the runtime cannot make. Seven mutations, all caught -- and the last only after a one-tick zone was added: a clamp written< -1instead of< 0still clamps every ordinary underflow and differs on exactly one input, so a first run reported it NOT CAUGHT. A boundary one tick wide needs an input one tick wide. Downstream zero sites. It was 17,665 immediately before, measured on 2026-08-20 by ticket #2410. The current verified baseline is 17,665 tests across 38 executables with THE GATE GREEN -- 17,665 run, 17,665 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #2410. +3 on the 17,662 below, inSharpRuntimeTests_Globalization(688 -> 691). #2410 makes both public doors reject a culture name that is not a well-formed BCP 47 tag. What it replaced was worse than "falls back to invariant":CultureInfo("xx-YY")succeeded, reported"xx-YY", and drew its formats from the invariant culture -- an object that lied about what it was. AndCultureNotFoundExceptionalready existed and was already thrown, but only from the LCID path, so one door of one type rejected what the other accepted (#2393's shape). The boundary is deliberately wider than .NET's and the header says so rather than implying it: measured, .NET in this port's own invariant-globalization mode accepts only""and"und"and throws for every other name,"de-DE"included (CultureData.cs:660-675,GlobalizationMode.cs:19); this port has no culture database at all, so that rule would leaveCultureInfounable to represent any named culture. One narrowing past BCP 47 is what makes the check work: RFC 5646 allows a 5-8 letter registered primary subtag, under which"process-default"is well formed -- and a first cut accepted it, missing the exact case the decision names -- so the primary subtag is restricted to the 2-3 letters every real culture name uses, losing the rare registered form, stated in the header. An accepted name still means nothing about available data, which a case asserts:"de-DE"'s month names are still"January". Seven mutations, six caught; two were not caught at first and both were genuine gaps -- the malformed list exercised only the primary subtag, so the over-long and non-alphanumeric checks on later subtags were untested; one is a proven equivalence (breaking"und"'s case-insensitivity changes nothing, because it is a well-formed three-letter primary subtag the general rule accepts anyway); and one was invalid as first written, rejected by-Werrorfor an unused lambda rather than by the tests. Downstream zero sites. It was 17,662 immediately before, measured on 2026-08-20 by ticket #2412. The current verified baseline is 17,662 tests across 38 executables with THE GATE GREEN -- 17,662 run, 17,662 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #2412. +5 on the 17,657 below, inSharpRuntimeTests_Core_Base(6,100 -> 6,105). #2412 exists because #1942 and #1943 listed each other: #1942 waited for "the relevant exact overload" -- one taking aDateTimeStyles, and measured, nothing inmodules/accepted one at all -- while #1943 waited for "#1940-#1942". What separates cleanly needs no approval: #1942's remaining blocker was "a reliable timezone contract", true only of the kind-affecting styles, andDateOnly/TimeOnlyhave noDateTimeKind, so .NET rejects every one of those outright and the whole contract is one line (DateOnly.cs:317-320). The styles that would need a timezone contract are exactly the styles that are illegal there. Both types'ParseExact/TryParseExactnow take a provider and a style, purely additively (both parameters defaulted). ATry*method that throws -- an illegal style raises where a parse failure returns false, which is .NET's (DateOnly.cs:519-522) -- and validation runs before the result is written, asserted because throws and leaves the out-parameter alone are two claims.AllowInnerWhiteskips at a token boundary, not anywhere: a field reader consumes its own digits without consulting the style, so"20 24-06-15"against"yyyy-MM-dd"still fails on the four-digit year -- the row that separates .NET's rule from a blanket skip, and the first cut put the skip in the wrong branch (the%-escape rather than the literal run) where the test caught it. A gap #1939 recorded honestly is now closed: it wrote that longest-first name matching was "defensive rather than load-bearing" because no invariant name is a prefix of another and a first-match mutation went uncaught -- a provider's names carry no such guarantee ("Ma"and"March"are a legal pair), so the rule now decides the answer and M6 is caught.DateTimeStyles.hppmoved intoCore.Base-- #1940's shape-C move again, no include line changed, graph still 41 / 94. Eight mutations: five caught, two proven equivalences (both empty-name guards are defence in depth, becauselen <= bestLenwithbestLenstarting at zero already excludes a zero-length name arithmetically), and three invalid as first written ---Werrorrejected them for an unused variable, so the verdict said nothing about the tests -- reformulated rather than counted. One mistake of my own is recorded: the first append created a newDateOnlyTests.cppthat did not exist, so the block had no includes; the real home isDateOnlyTimeOnlyTests.cppand the stray file was removed. #1942 and #1943 stay blocked on theirDateTimehalves alone, which need #1941 phase 2's unapproved timezone provider. It was 17,657 immediately before, measured on 2026-08-20 by ticket #1940. The current verified baseline is 17,657 tests across 38 executables with THE GATE GREEN -- 17,657 run, 17,657 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #1940. +6 on the 17,651 below, inSharpRuntimeTests_Core_Base(6,094 -> 6,100). #1940 is the root of the remaining date/time chain and it is now closed, which unblocks #1942, #1943 and #1945 directly and #1944 through #1943. The blocker was two things and the ticket's own record named only one: the component cycle and the fact that nothing in this runtime implementedIFormatProviderat all -- notDateTimeFormatInfo, notNumberFormatInfo, notCultureInfo-- soGetFormathad zero implementations and a caller could not build a provider from a culture even in principle; the record's "shape A is feasible today" was true and still not enough, because there was nobody to ask. Shape C's measured claim held exactly:DateTimeFormatInfo.hppandCalendarWeekRule.hppmoved intoCore.Baseandgit diff --statshowed only the two renames -- ownership is by logical path uniqueness, so not one#includeanywhere changed -- graph unchanged at 41 / 94, selectiveCore.Baseconsumer build green.CalendarWeekRulehad to move too, and that is the whole of two files: leaving it behind would have recreated the cycle through the enum. The provider is honoured rather than accepted and ignored, which is #1940's own criterion and exactly why the overload went onToString(format, provider)and not onParse-- the formatter had hard-coded month and day name tables and now reads them from the resolved info, while this port's general date parser reads no culture-driven token at all, so a provider overload there could only accept and ignore one (that is #1942). "Keep all parsers semantically unchanged" is structural rather than asserted:ToString(format)is now literallyToString(format, nullptr), so there is no second formatter left to drift. One index base moved and it is .NET's -- the old tables were 1-based with an empty slot 0,DateTimeFormatInfo's are 0-based with an empty thirteenth slot -- pinned across the full range, because December and January are the two months an off-by-one gets wrong in opposite directions. One deviation is named rather than left to be discovered:GetInstance(nullptr)resolves to the invariant info, not the current culture's, becauseCultureInfostays inGlobalization; this is not new -- the port has answeredCurrentInfothat way since the type was ported, and since #2409CurrentCultureis the invariant culture until something sets it, so the two agree in the default state; a caller who wants the current culture's info passes that culture. Six mutations, five caught; the sixth is a proven equivalence (removing theas DateTimeFormatInfoshortcut changes nothing, becauseGetFormatreturnsthisfor that type) observable only for a derived class -- a shape .NET forbids by sealing the type where this port does not, an asymmetry recorded and deliberately not closed. Two of the six were NOT CAUGHT at first and both were genuine gaps: aGetFormatanswering for any type passed everything, because every case asked only for the one type it should answer for; and no case passed aCultureInfoas a provider at all -- the very route the ticket exists to open. Downstream zero sites. It was 17,651 immediately before, measured on 2026-08-20 by ticket #2409. The current verified baseline is 17,651 tests across 38 executables with THE GATE GREEN -- 17,651 run, 17,651 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #2409. +5 on the 17,646 below, inSharpRuntimeTests_Globalization(683 -> 688). #2409 makesCultureInfo::CurrentCultureper-thread, which its own doc-comment had claimed all along: both current-culture members were process-widestatics, so a set on one thread changed what every other thread read, and a concurrent get/set was an unsynchronised read/write of a non-atomic object. The obvious repair was the wrong one, and that is why this was a decision (SA-14) rather than a transcription: a barethread_localwould have fixed the race and silently removed the process-wide setting this port accidentally had, with no replacement and no diagnostic. .NET's own answer is a second property, so the port grew .NET's three-step chain (CultureInfo.cs:358-366) -- athread_localthat is absent by default, thenDefaultThreadCurrentCulture, then the invariant culture. Absent rather than invariant-valued at step 1 is load-bearing: not chosen and chose the invariant culture are different facts and only the first may fall through. The getter returns a reference, so the fallback could have moved the race one level down -- the next store drops the last owner and frees what the caller holds -- and the default is therefore anatomic<shared_ptr<const CultureInfo>>whose loaded pointer the reader parks in athread_localholder; the held reference may be stale but is never invalid, which a case asserts. Six mutations, all caught, two at compile time. The ownership case had to be made DETERMINISTIC before it caught anything, and that is the result worth carrying forward: run as a race -- a churn thread swapping the default while a reader took and read a reference -- neither gtest nor ASan caught it over 20,000 iterations, the window being nanoseconds wide, with the sanitizer confirmed to build and run clean on the control first; replacing the default from the same thread removes the timing entirely and the mutation is then caught on every run. Two test defects of my own are recorded rather than quietly fixed: the first cut set the culture on the test thread and never restored it, leaking into the binary and making three unrelated pre-existing cases fail under mutation, and a mutation run reported three false NOT CAUGHT results because the harness's filter still named the fixture by its old name -- a harness error, not a result. A consequence found by the first of those deserves its own line: the save/restore idiomprevious = get...(); ...; set...(previous)no longer restores what it used to -- it turns chosen nothing into explicitly chose invariant, and only the former falls through to the default; .NET has the same asymmetry, its setter taking a non-null value, so this is faithful rather than a port artefact. Downstream zero sites, recorded as #2411. It was 17,646 immediately before, measured on 2026-08-20 by ticket #2406. The current verified baseline is 17,646 tests across 38 executables with THE GATE GREEN -- 17,646 run, 17,646 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #2406. +6 on the 17,640 below, inSharpRuntimeIntegrationTests(946 -> 952). #2406 closes thecomponent-modelsweep with two shape repairs and one declaration, which is the larger half. The elevenValidationAttributesubclasses validate nothing -- .NET carriesIsValid(ValidationAttribute.cs:352),Validate(:468,497),FormatErrorMessage(:330) andRequiresValidationContext(:139), and overridesIsValidon every subclass; none of those exist here. The names are what make that worth a@warningrather than a footnote: a caller who writesRequiredAttributeand sets an error message has every reason to believe something checks it, and nothing does -- there is no member to call, so the mistake surfaces as validation that silently never happened, and the stored message is inert, which a case asserts. It is a declaration rather than a repair because implementing it is not one member --RegularExpressionAttributeneeds the regex engine,EmailAddress/Url/Phoneneed .NET's exact grammars, andValidateneedsValidationContextandValidationResult, neither of which exists here -- and the absence is pinned: mutation M7 adds anIsValidand is caught, so the declaration is enforced rather than merely written.DataTypeAttributewas structurally wrong: one public mutablestd::stringwhere .NET has two constructors with different meanings (DataTypeAttribute.cs:20,55) -- a known kind from an enum, or a custom one named by the caller -- so the kind and a custom name were the same field and any string was accepted where only seventeen values are meaningful. TheDataTypeenum is transcribed exactly, the string constructor chains toCustomand stores the name beside the kind, andGetDataTypeName()throws .NET's own text for an unnamed custom kind -- onIsNullOrWhiteSpace, notIsNullOrEmpty, which is the row an.empty()reading gets wrong and which mutation M4 makes. .NET readsEnum.GetNames<DataType>(), which is reflection; the substitute is an exhaustiveswitchwith nodefault:, so a new enumerator is a compile error rather than a silently missing name -- #1980 G-5's idiom, and stronger than a name table, which cannot pin an enum's membership.DisplayFormatis deliberately absent, because the constructor's three-case switch exists only to populate a type this port does not have. What is NOT a divergence is recorded too:RequiredAttribute::AllowEmptyStringsandDisplayAttribute's eight fields stay public data members, because .NET's are{ get; set; }and a public field is observationally identical -- #1969's recorded reasoning -- so SA-8 does not reach them; what does still diverge there is nullability (the #2295 shape, eight signatures) and it is left open rather than half-done. Seven mutations, all caught; fixture set 51 / 260 -> 52 / 264; first-party migration zero sites; downstream zero sites, recorded as #2408. It was 17,640 immediately before, measured on 2026-08-20 by ticket #2405. The current verified baseline is 17,640 tests across 38 executables with THE GATE GREEN -- 17,640 run, 17,640 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #2405. +3 on the 17,637 below, inSharpRuntimeTests_ComponentModel(104 -> 107; one vacuous case replaced by one real one, three added). #2405 has two independent halves. Half A:PropertyChangedEventArgsandPropertyChangingEventArgseach carried two representations of one fact -- a privatestd::optional<std::string>and a public mutablestd::stringsnapshotted from it -- where .NET's whole type is four lines ending inpublic virtual string? PropertyName { get; }. Three defects lived in that one member: it was lossy (value_or("")collapsednulloptand"", the #2295 defect, and the absent state is .NET's documented "all properties may have changed"), it was mutable so a subscriber could retarget the args mid-dispatch, and after such a write the field and the accessor disagreed, so the object contradicted itself -- reachable only because the field was public. The header already documented the first defect and kept the field "for existing sharp-runtime consumers that predate the nullable-property port"; that reason was measured and is empty -- zero downstream sites, one first-party read. Half B:System::ComponentModel::Attributeis removed. There is noSystem.ComponentModel.Attributein .NET -- measured, no such file in the reference tree -- and measured across this module, 20 attributes derive fromSystem::Attribute, 11 fromValidationAttributeand zero from the removed type, which had no members, no derived classes and no callers. Its only appearance outside its own header wasEXPECT_NO_THROW(System::ComponentModel::Attribute{}), the fourth "assertion that cannot fail" this sweep has found; what replaces it dispatches through a base reference, because astatic_assertalone would pass against a base nothing can call through. Three mutations, all caught, and the pin they hang on is the point: with the second representation gone, the property worth asserting is that there is exactly one, so the case comparessizeofagainst a one-optionalshadow struct -- which catches a reinstated field on either type where no value-based assertion would. Half B's removal is caught by the negative fixture rather than gtest, the only instrument that can see a type that no longer exists. Fixture set 50 / 256 -> 51 / 260; downstream zero sites for either half, recorded as #2407.DataAnnotationswas split out as #2406 rather than bundled, becauseDataTypeAttributeholds an untyped mutable string where .NET has an enum, a separate custom-type constructor and two accessors -- closing it means adding public surface, not changing an accessor. It was 17,637 immediately before, measured on 2026-08-20 by ticket #2403. The current verified baseline is 17,637 tests across 38 executables with THE GATE GREEN -- 17,637 run, 17,637 passed, 0 failed, 0 skipped, measured on 2026-08-20 by ticket #2403. +6 on the 17,631 below, inSharpRuntimeTests_ComponentModel(98 -> 104; eight migrated sites rewritten in place, six cases added). #2403 gives sixSystem::ComponentModelattributes .NET's shape, and what makes it more than a style point is that the port was inconsistent with itself inside one file:CategoryAttribute,BrowsableAttribute,DisplayNameAttributeandDescriptionAttributealready had the correct shape in that very header and its neighbour, whileReadOnlyAttribute,ImmutableObjectAttribute,LocalizableAttribute,MergablePropertyAttribute,NotifyParentPropertyAttributeandRefreshPropertiesAttributepublished a bare mutable public data member and most had no statics and no equality members at all. All six are nowfinalforsealed, with a get-only accessor and the fullYes/No/Defaultset.RefreshPropertiesbecomes a top-level enum, where this port nested it asRefreshPropertiesAttribute::Refresh-- differing from .NET in both the name and the scope. The defaults are not uniform and that is .NET's:MergablePropertyAttribute::DefaultisYes(MergablePropertyAttribute.cs:12) where the other four default toNo, so a repair that harmonised the five would be wrong here and nowhere else -- pinned in one case, so it reads as an asymmetry rather than five unrelated literals. One divergence is deliberate and is pinned: .NET'sGetHashCodefor all six isbase.GetHashCode(), identity, while itsEqualsis value-based, so two equal .NET instances can hash differently -- a hash-contract violation in the reference itself. This port does not reproduce it, and the reason is written down rather than chosen:System/Attribute.hpp's own doc-comment states the house rule ("A subclass that needs value equality must override both Equals and GetHashCode") and the four already-correct siblings use a value hash. Mutation M7 adopts .NET's identity hash and is caught, which is the evidence the choice is load-bearing. The module's whole prior coverage for these six was constructor round-trips through the public field -- nothing asserted the statics, the equality members or the defaults, so .NET'sDefaultvalues, which are the actual contract of a metadata attribute, were unpinned. Seven mutations, all caught, three at compile time. No vtable change: the three members are alreadyvirtualonSystem::Attribute, so these are overrides of existing slots. First-party migration was 8 sites, all in this module's own test file, every one named by the compiler; fixture set 49 / 248 -> 50 / 256, its second site being the write, which is the spelling a careless migration would have kept longest because it compiled silently while changing the attribute's meaning. Downstream zero sites, recorded as #2404. A process note worth keeping: an early build reported zero errors and was nearly believed --cmake --build … -k 200is not keep-going, since-kmust follow--, so CMake printed its usage banner and exited. That is this rule's own #2395 trap in a new form, and a mis-passed-kis worse than none, because it looks like success. It was 17,631 immediately before, measured on 2026-08-19 by ticket #2402. The current verified baseline is 17,631 tests across 38 executables with THE GATE GREEN -- 17,631 run, 17,631 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2402. +2 on the 17,629 below, inSharpRuntimeTests_Core_Base(6,092 -> 6,094; one case renamed in place and two added). #2402 closes the entropy sweep by recording what is NOT a defect, which is the half a sweep usually leaves out: the runtime has exactly five sites needing random bytes, two were repaired (#2398, #2401), and two are parity and look exactly like the defect --HashCode::GlobalSeedand unseededRandomboth callstd::random_device, which is precisely what #2401 removed fromClientWebSocket, so a later reader would otherwise "fix" them into a divergence. .NET has TWO entropy entry points and picks between them deliberately (Interop.GetRandomBytes.cs:18-27, reaching differentminipalfunctions perpal_random.c:13-27), andHashCode.cs:58,70-75plus bothRandom.Xoshiro*Impl.cs:38call the non-cryptographic one. There is a structural reason too:HashCodeis inCore.Base, so a cryptographic seed would put a cryptography component under every consumer ofCore.Base-- the inversionGuid.cppalready refused, and #2401's argument (a leaf module, a private edge) does not transfer. One defect did come out of looking:HashCodeTests.Seed_DiffersAcrossProcessesButConsistentWithinOneasserted only the second half of its own name, comparing two accumulators inside one process -- which a constant seed satisfies perfectly -- so the clause the property exists for was untested. That is the third assertion in this sweep that could not fail for the property it claimed, after the twoEXPECT_EQ(buffer.size(), N)cases #2398 and #2399 replaced, and the pattern is now recorded inNEXT.md§4b as a search worth running on its own. A plainfork()cannot test it: the seed is a function-localstaticinitialised on first use, so a forked child inherits it and reports the same hash whatever the source -- the child must re-exec, #1979's idiom for the same class of reason. Two children rather than one, so a constant seed is distinguishable from a 1-in-2^32 coincidence, reporting through a pipe on fd 3 so gtest's own output cannot be mistaken for the payload. The child driver usesSUCCEED()rather thanGTEST_SKIP()deliberately: it runs in every ordinary run, and a skip would move this gate off "0 skipped" permanently, which is a documented property of this floor rather than an incidental one. Three mutations, all caught -- a constant seed and a build-time-fixed seed only by the new case, which is the evidence the repair was needed, and a per-instance seed by 22 pre-existing cases. One was invalid as first written and was reformulated rather than counted: a seed derived from a stack address is not the defect it was meant to be, because ASLR varies it per process. Record:docs/EntropySourceSweep.md. It was 17,629 immediately before, measured on 2026-08-19 by ticket #2401. The current verified baseline is 17,629 tests across 38 executables with THE GATE GREEN -- 17,629 run, 17,629 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2401. +2 on the 17,627 below, inSharpRuntimeTests_Net_WebSockets(105 -> 107). #2401 was found by asking #2398's question once more: #2228 put a real CSPRNG behindGuid::NewGuid, so what else in this runtime needs unpredictable bytes, and where does it get them?ClientWebSocketdrew both itsSec-WebSocket-Keynonce and its per-frame masking key fromstd::random_device. That is a defect rather than a style point, and this repository had already measured why: the standard explicitly permits a deterministicstd::random_device, andRandom.cpp:69-70records in its own comment "on a platform whose random_device is deterministic (MinGW-w64's historically was)" -- MinGW-w64 being a supported compile target here. On such a platform every connection sends the same nonce and every frame's mask is predictable. RFC 6455 does not leave that to taste: §5.3 says the masking key "MUST be derived from a strong source of entropy" and "MUST NOT make it simple for a server/proxy to predict the masking key for a subsequent frame", and masking exists to stop cache-poisoning of intermediaries (§10.3), so a predictable key defeats the one attack it was introduced for. .NET's two routes are different and are transcribed separately rather than harmonised: the nonce isGuid.NewGuid().TryWriteBytesbase64-encoded (WebSocketHandle.Managed.cs:490-494) and the mask isRandomNumberGenerator.Fill(ManagedWebSocket.cs:762-763). The nonce cost no component edge at all, because #2228 already put the CSPRNG behindGuid::NewGuidandCore.Basewas already public here; the mask tookSecurity.Cryptography.Randomas a private dependency, graph 41 / 93 -> 41 / 94, catalogue regenerated, and the selective-component consumer build re-run. Callinggetentropy()directly fromnet-websocketswas rejected on a recorded precedent -- it would be a third copy of the platform entropy call, the duplication #2354 spent a ticket removing. What cannot be observed here is stated rather than implied: on glibcstd::random_devicereads/dev/urandom, so the source change is not behaviourally visible on this platform, and the evidence is the reference, the RFC, this repository's own MinGW-w64 measurement, and symbol inspection, which is precisely discriminating -- after the repairrandom_deviceappears 0 times in that translation unit against 16 for either reversion, and the two reversions are told apart by which ofGuid::NewGuidandRandomNumberGenerator::Filldrops to zero. Five mutations: the two source reversions are NOT caught by tests and are caught by the symbol table, reported as such rather than as passes, and the three RFC-property mutations are all caught. Those three are the plausible optimisations, not contrived defects -- a mask cached per connection still unmasks correctly at the server, because both ends agree on whatever key was sent, so it passes every other assertion in the file. One mutation was invalid as first written and was reformulated rather than counted: zeroing the mask leftrandomMaskingKey()unreferenced and-Werror=unused-functionrejected it. What is newly pinned are the RFC properties themselves, which nothing pinned before: a 16-byte nonce differing between connections (§4.1) and a fresh mask per frame (§5.3), plus a separate assertion that no mask is all zeroes, which the freshness check alone would not catch for a single frame. It was 17,627 immediately before, measured on 2026-08-19 by ticket #2399. The current verified baseline is 17,627 tests across 38 executables with THE GATE GREEN -- 17,627 run, 17,627 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2399. +2 on the 17,625 below, all inSharpRuntimeIntegrationTests(944 -> 946; one vacuous case replaced by two real ones plus one shape pin). #2399 givesRNGCryptoServiceProviderthe shape .NET declares --public sealed classwith[Obsolete], diagnostic id SYSLIB0023 (RNGCryptoServiceProvider.cs:8-10, messageObsoletions.cs:82) -- where this port had a non-final class with no deprecation and only the implicit default constructor. Two spellings are outlawed and they are outlawed for two different reasons, which is why each gets its own fixture site: deriving is rejected by the seal, and naming the type at all is rejected by the deprecation, since under-Wall -Wextra -Werrora deprecation is a hard error -- the boundary #2289 measured before this repository took its first[[deprecated]]. Site 1 suppresses the deprecation on purpose, so that it fails for the seal and only for the seal; without that the two reasons are indistinguishable and the site passes for the wrong one. Two of .NET's three missing constructors were added --(string)and(byte[]), ignoring their argument exactly as .NET's do, andexplicit, which is the faithful translation rather than a narrowing because a C# constructor never participates in an implicit conversion.(CspParameters?)cannot be transcribed and is not:CspParametersdoes not exist in this port, and inventing it to carry a type whose only behaviour isif (cspParams != null) throw new PlatformNotSupportedException()would be inventing public surface -- so what is pinned instead is the shape such an overload would introduce,static_assert(!is_constructible_v<Rng, std::nullptr_t>), which a later ticket adding the type must trip and justify. What is NOT a divergence is recorded too, so a later reader does not complete a complete set: .NET overrides four further members to forward to its inner generator, and this port overrides one and inherits the rest, whose base implementations call that same virtual. Six mutations, all caught -- and one by an instrument the test suite does not have: removing the[[deprecated]]is invisible to gtest, because a scoped suppression compiles perfectly well when there is no diagnostic to suppress, and it is caught by the negative fixture, sites 2 and 3, verified by running the checker with the mutation applied. Three more are caught at compile time bystatic_assert, the only way C++ reports a shape. One mutation was invalid as first written and was reformulated rather than counted: storing thebyte[]argument in a member without reading it is a no-op, not a seed. The shipped case could not have caught any of it --EXPECT_EQ(buffer.size(), 24u)on a buffer whose size was fixed before the call, which passes against a generator that writes nothing. Fixture set 48 / 245 -> 49 / 248; downstream zero sites in both consumers, recorded as #2400. It was 17,625 immediately before, measured on 2026-08-19 by ticket #2398. The current verified baseline is 17,625 tests across 38 executables with THE GATE GREEN -- 17,625 run, 17,625 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2398. +13 on the 17,612 below, all inSharpRuntimeIntegrationTests(931 -> 944). #2398 stopsRandomNumberGeneratorthrowingPlatformNotSupportedExceptionon Emscripten -- and the premise it threw on had already been measured false inside this repository. Its comment read "No secure random source wired up under Emscripten yet", whileGuid.cpp:377-388records #2228's measurement that Emscripten's libc declaresgetentropy()in<unistd.h>and implements it as__wasi_random_get(), backed by the host'scrypto.getRandomValues. SoGuid::NewGuid()has been drawing real entropy on Emscripten through exactly the call this file refused to make -- two answers to one question in one runtime, and the type whose entire purpose is cryptographic randomness was the one refusing. .NET does not refuse either:RandomNumberGeneratorImplementation.Browser.csforwards toInterop.GetCryptographicallySecureRandomBytes, whose__EMSCRIPTEN__arm isSystemJS_RandomBytes(src/native/minipal/random.c:83-93). Four platform arms became two, and that is what makes the repair verifiable rather than merely tidy:getrandom()is Linux-only, so the old file had one arm per platform and the Linux gate compiled exactly one of them; with a single non-Windowsgetentropy()loop, the code Emscripten takes is the code Linux takes, so the gate executes it on every run. The limitation was also undeclared --CLAUDE.md's platform-limitation table never listedRandomNumberGenerator, so a caller reading it would have concluded the type worked everywhere; after this change that conclusion is correct and no row is added. Failure still throws, and differs fromGuiddeliberately:NewGuid()retries because callers treat it as infallible, while .NET throws here too (Interop.GetRandomBytes.cs:22-26), and neither ever falls back to a weaker source. Two messages moved onto .NET's exact text --GetInt32's empty range (RandomNumberGenerator.cs:105-106,SR.Argument_InvalidRandomRange) andverifyGetBytes's truncatedSR.Argument_InvalidOffLen(:438-446), the latter also ending an inconsistency inside this port, sinceConsole.hppalready spelled .NET's full sentence. The exception types were already right and did not move: .NET usesArgumentExceptionwith no parameter name for the empty range, because the fault is the relationship between two arguments rather than either one's range, and a case asserts that shape beside theArgumentOutOfRangeExceptionguards so a later repair cannot conflate them. The shipped coverage could not have caught any of this: two cases assertingbuffer.size()after filling a buffer sized before the call, both of which pass against a generator that writes nothing. Thirteen cases replace them, including #2228'sfork()distinctness idiom (a userspace PRNG has state andfork()duplicates it) and the 256-byte chunk boundary, which the Linux gate could not reach before this change becausegetrandom()has no such limit. Eight mutations, six caught and two not caught with stated reasons: restoring the Emscripten throw cannot be caught here because that arm is not compiled and there is no Emscripten toolchain in this container, and ignoring agetentropy()failure is unobservable in isolation because with chunking intact the call does not fail. One mutation was invalid as first written and was reformulated rather than counted -- removing the chunk cap leftmaxChunkunused, so-Werror=unused-variablerejected it and the verdict said nothing about the tests. Downstream: zero sites in both consumers.RNGCryptoServiceProviderwas split out as #2399 rather than bundled, because every part of it --sealed,[Obsolete], and three missing constructors -- is a public source break or new public surface, where #2398 outlawed no spelling at all. It was 17,612 immediately before, measured on 2026-08-19 by ticket #2397. The current verified baseline is 17,612 tests across 38 executables with THE GATE GREEN -- 17,612 run, 17,612 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2397. +15 on the 17,597 below, all inSharpRuntimeIntegrationTests(916 -> 931; one shipped pin inverted in place, fifteen cases added). #2397 came from a queue-empty parity sweep: both work queues were empty, so a module with no dedicated test executable was measured against the reference --System::Text::RegularExpressions, whose only coverage sat inside one integration file. Four divergences, two of them silent data loss through a public member.Regex::Splitdiscarded every matched capture group's value --Split("a1b2c", "(\\d)")returned three elements where .NET returns five (Regex.Split.cs:304-311, which appends each matched group after the segment preceding its match and skips one that did not participate) -- and it dropped a trailing empty segment,Split("abc", "c")returning{"ab"}where .NET returns{"ab", ""}(Regex.Split.cs:321). Both losses had one cause: the body was a singlestd::sregex_token_iterator(-1), which yields only the non-matching segments and suppresses a trailing empty token, so no care at the call site could recover the data.Regex::Escapeused its own metacharacter set, differing fromRegexParser.cs:2135-2136in both directions -- six missing (TAB, LF, FF, CR, SPACE and#) and two extra (],}) -- and .NET additionally spells the four whitespace metacharacters as a backslash plus a letter (RegexParser.cs:180-199), a rule this port had no need of because it had none of those characters in its set. The direction that could have broken something was measured rather than reasoned: the newly emitted forms (\,\#,\n,\r,\t,\f) had to be accepted by libstdc++'s ECMAScript grammar and the newly bare]/}had to be accepted as literals -- probed over all 127 single ASCII bytes plus 18 composites, .NET's output round-trips as an exact literal with zeroregex_errorand zero wrong matches. One narrowing is real and is .NET's own: splicingEscapeoutput into a character class --"[" + Escape(x) + "]"-- is no longer protected by an escaped], which is precisely why .NET specifiesEscapefor a literal in a pattern rather than in a class. Fourth,Match::getIndexProperty()answered -1 for an unsuccessful match, a sentinel .NET never produces:Match.cs:75buildsMatch.Emptywithcapcount == 1, reachingGroup.cs:27-28andCapture.cs:27-32, so .NET reports 0. A shipped test pinned the -1 and was inverted rather than deleted, because it was asserting the divergence; the case that replaces the sentinel pins what follows from removing it -- a successful match at position 0 and a failed match now report the same index, so onlygetSuccessProperty()separates them, which is whatMatch.cs:72-74says in terms. Landed under SA-5, with no layout, vtable, signature ornoexceptchange and no outlawed spelling, so no negative fixture was owed -- the set stays 48 / 245 and the graph 41 / 93. Nine mutations, eight caught and M7 an equivalence proven rather than asserted: dropping the no-match early return changes nothing, because every loop iteration appends at least the segment before its match, so an empty result set meansprevatis still 0 and the fall-throughsubstr(prevat)already yields{input}-- measured over 288 (pattern, input) pairs with both forms side by side, zero differences -- and the line is kept for being .NET's, with the site saying so. Downstream: zero sites in both consumers, the onlyRegexhits incnabeing insidevendor/googletest. What #2397 did NOT close is recorded rather than left silent:Regex::Unescapeis absent, thecount/startatSplitoverloads are absent, andMatch::Empty().Groups().Countis 0 here against .NET's 1 (GroupCollection.cs:67), because this port'sMatchdoes not derive fromGroup/Captureas .NET's does. It was 17,597 immediately before, measured on 2026-08-19 by ticket #2395. It was 17,597 tests across 38 executables with THE GATE GREEN -- 17,597 run, 17,597 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2395. +3 on the 17,594 below, inSharpRuntimeTests_Core_Base(6,089 -> 6,092; one gated pin replaced by four cases). #2395 gives a bit pattern and a number different spellings: the raw-bits constructor becomes the named staticFromBitson both 16-bit floats, which frees the constructor signature for .NET's value conversions, and #2384 unit 3'stodirection lands on both types in step (#2340). The collision was structural:Half(uint16_t)was the raw bit pattern and .NET spends exactly that signature onexplicit operator Half(ushort), whose body is(Half)(float)value-- and it was worse than one overload, because an exactintmatch beatsint -> uint16_t, so adding any value-taking integer constructor madeHalf(0x7BFF)silently mean the number 31743; #2384 measured it by building the constructors and watching 44 shipped tests turn red,Half::MaxValueamong them. The dangerous half is that the compiler cannot find the sites:Half(0x3C00)is valid under both readings, so an unmigrated site keeps compiling and changes meaning silently. The work was therefore sequenced so that it could not: the rename landed first, with no value-taking constructor present, so every site had to be named by the compiler; then all sites were migrated; then the conversions were added. Two process lessons came out of that step and both cost real sites -- truncating the compiler's error list (| head) hid sites that then compiled silently, and the build stops at the first failing translation unit unless-kis passed. The migration is 67 sites, not the ticket's 66: a fourth site was found only by the full gate, intests/integration/Task40Tests.cpp, a tree thatmodules/andtest/do not cover -- the same gap #1958/SR-AUD-196 recorded when its own grep missed it. Three others were found only by an exhaustive re-grep, among themBitConverter::ToBFloat16andUInt16BitsToBFloat16, both bit functions that would have begun returning a number. The conversion bodies are NOT all the same, and that is .NET's doing: Half gets(Half)(float)valuefor every integer, while BFloat16'sint/uint/long/ulonggo throughRoundFromSigned/RoundFromUnsigned(BFloat16.cs:560-646), because 8 significand bits mean a 32- or 64-bit integer routed through a 24-bit float rounds twice -- probed over 400,000 random values of each width, the two differ on 4 int32 and 2 int64 inputs, the first being1119879149, direct0x4E85against the float route's0x4E86, verified by hand.byte/sbyteare explicit here where .NET makes them implicit, because C++ permits a standard conversion before a user-defined one and C# does not, so the implicit form makes every integer argument ambiguous; what is lost is the implicitness, never the conversion.nint,nuint,decimal,Int128andUInt128are absent in both directions and pinned, so a later unit completing one has to justify the asymmetry. Six mutations, all caught -- but three only after work that is recorded: M2 and M3 were first NOT CAUGHT because the 64-bit path is a separate template instantiation with no witness and no case exercised an exact tie, and both now have one (3485786034122340516and65792, which is1.0000000_1 x 2^16-- an exact midpoint above an even significand, so ties-to-even holds0x4780where ties-away gives0x4781); and M6 is caught only under UBSan, because removing the zero short-circuit leavesabs << 32, which x86 masks in hardware so the answer still comes out0x0000-- undefined behaviour that happens to work, reported as "shift exponent 32 is too large for 32-bit type" with the guard removed and silent with it. Negative fixture set 47/240 -> 48/245, and its header states plainly what it cannot do: reject the old spelling. Downstream: zero sites in both consumers. It was 17,594 immediately before, measured on 2026-08-19 by ticket #1958 (SR-AUD-209). It was 17,594 tests across 38 executables with THE GATE GREEN -- 17,594 run, 17,594 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1958 (SR-AUD-209), which closes #1958. +4 on the 17,590 below, inSharpRuntimeTests_Threading(523 -> 527; one #1956 layout pin split in two and inverted, three cases added, one case deliberately removed). SR-AUD-209 makesAutoResetEventandManualResetEventwhat .NET declares them to be:sealed class ... : EventWaitHandlewhose entire body is one constructor (AutoResetEvent.cs:6-9,ManualResetEvent.cs:6-9) -- neither declares a member of its own, andSet,Reset,WaitOne,CloseandDisposeare all inherited. This port had them with no base and no vtable, each carrying its own mutex, condition variable and signalled flag, soWaitHandle::WaitAll/WaitAny-- repaired by #1952 and documented ever since -- could not accept them at all: not a wrong answer, the code did not compile. That is what made SR-AUD-209 the one divergence in the namespace leaving a documented API unusable. Two things the finding did not name were required, and either omitted would have made the repair a regression. (1)EventWaitHandlehad no closed state: #1956 gaveMutex,AutoResetEventandManualResetEventaclosed_flag and this was its fourth case, missed, soClose()here reachedWaitHandle's emptyDispose()and did nothing -- deriving without fixing it would have silently reverted #1956 for both events. (2)EventWaitHandle::Set()lost wakeups, storing and notifying without holdingmtx_, so a waiter that had evaluated the predicate as false but not yet slept missed the notification;AutoResetEvent::Set()took the lock, so deriving would have introduced the race into a type that did not have it. Measured over 900 single-waiter rounds:EventWaitHandlelost 2,AutoResetEvent0 -- and this is the typecnaholds by value in six places, all for async completion, exactly the shape a lost wakeup hangs. Layout: both events 96 -> 112 andEventWaitHandle104 -> 112, so every consumer rebuilds; 112 was measured after 104 was asserted and the build rejected it, #1956's flags having fitted existing padding on three other types and that expectation being carried over rather than measured. The pin asserts the relationship -- both events must be exactlysizeof(EventWaitHandle)-- as well as the figures, so it says these declare no members of their own rather than these are 112 bytes today.WaitOne()returnsboolrather thanvoid, a widening at every call site since ignoring a return value is legal, and theinitialStateparameter loses its default because .NET has none and it had zero call sites anywhere. Seven mutations, six caught and M6 honestly not caught: revertingSet()to the unlocked form is a race window, detectable at roughly 0.2% per round, so a bounded test would catch it about a third of the time -- and a first cut of the suite carried exactly such a 200-round case and it was removed rather than kept, on the reasoning #1957/SR-AUD-201 and #2031 recorded for their own window-closing mutations. A multi-waiter amplification was tried and is invalid, reporting 100% "loss" for the locked form too, because repeatedSet()calls on an AutoReset event coalesce into one signal -- it measures AutoReset semantics, not the race. Downstream: zeroAutoResetEvent/ManualResetEventsites in both consumers; all 14 hits areEventWaitHandle, and measured against its usage (EventWaitHandle(true, ManualReset)returned as aWaitHandle&)cnaneeds a rebuild, not an edit -- and it builds against the sibling checkout ondevelop, so both repairs reach it at the merge rather than now. It was 17,590 immediately before, measured on 2026-08-19 by ticket #2208. It was 17,590 tests across 38 executables with THE GATE GREEN — 17,590 run, 17,590 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2208. +1 on the 17,589 below, inSharpRuntimeTests_IO_IsolatedStorage(62 -> 63; one residual pin removed, two cases in its place). #2208 confinesIsolatedStorageFileStream: its constructor took astd::filesystem::pathand checked nothing, opening whatever it was handed anywhere on the filesystem and creating that path's missing parents on the way. That is a wider hole than the TOCTOU #2207 declared and accepted -- that race needs an attacker who can already write inside the store root plus a window between check and use; this needed neither a race nor a privilege, only the call -- and it was the one door on a type that exists to confine file access which confined nothing. The reference corrected the ticket's proposed shape, and the correction removed the source break it was priced around: #2208 said remove the path constructor and take the owning store instead, while .NET publishes eight constructors all beginning(string path, FileMode mode, ...)with the store an optional trailing parameter (IsolatedStorageFileStream.cs:21-56), and its storeless form is not unconfined -- a null store meansGetUserStoreForDomain(), resolved throughisf.GetFullPath(path)exactly as the store-taking form is (:82-118). So the confinement lands without removing an overload .NET publishes and without inventing a leading-store parameter order; an earlier cut used(store, path, mode)and that order is now pinned as not compiling, because the wrong order compiles perfectly well as an addition and no behavioural test can see it, both orders confining correctly. On POSIX there is no source break at all --std::filesystem::pathconverts tostd::stringimplicitly there, so the old call still compiles and only its meaning moves, from this filesystem path to this path inside the store; on Windowsvalue_typeiswchar_tand the spelling breaks. An absolute path is contained, not refused, becausefullPath()strips leading separators at every door -- the rule #2209 recorded -- and refusing it at this one door would make the type inconsistent with itself. Each door reports its own parameter name: routing every door through one resolver made it possible for one door's diagnostic to name another's parameter, which is #2323's rule, so the public constructors namepathwhileOpenFile/CreateFilestill namerelativePath, through a private four-argument constructor. Two behaviours came with it -- the mode is validated with .NET's own text (Invalid mode, see System.IO.FileMode.) before the file is created, reachable only by a value cast in from outside the enumeration; andIsolatedStorageFile::GetUserStoreForDomain()was added purely additively with .NET's exact scope combination, because it is the store the storeless form defaults to. .NET'spath == "\\"check is deliberately not reproduced, because on POSIX a backslash is an ordinary file-name character and this module's ownisDirectorySeparator()says so -- the outcome is identical anyway, sincefullPath()strips separators and then rejects what is left when it is empty. Eight mutations, seven caught, one a proven equivalence: swapping the default store's scope is unobservable, because both factories call literally the sameGetIsolatedStorageRoot()and the stream never retains the store, soForDomainis written for fidelity rather than for effect. Three mutations were invalid as first written (-Werror=unused-parameter,-Werror=unused-function, and a[[nodiscard]]onfullPath()) and were reformulated rather than counted. Seven of the eight are caught by pre-existing confinement tests, and that is the point --OpenFilenow routes through the constructor, so the whole shipped suite covers it; the mutation only the new cases catch is the half-repair,OpenFilepre-resolving while the constructor ignores its store. Negative fixture set 46/236 -> 47/240, measured by the checker rather than derived from this file (§22.4's running total was already stale). Downstream: zero constructions in both consumers (mobile-eggberthas one#includeand no call). It was 17,589 immediately before, measured on 2026-08-19 by tickets #1888 and #1889. It was 17,589 tests across 38 executables with THE GATE GREEN — 17,589 run, 17,589 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1889. +4 on the 17,585 below, inSharpRuntimeTests_Text_Json(302 → 306). #1889 makesJsonArray/JsonObjectenumeration fail-fast, closing two measured defects: an iterator held across a reallocatingAddwas an ASan-confirmed heap-use-after-free — a SIGSEGV without a sanitizer (J11) — and one held acrossClear()silently returned a value from destroyed storage with no diagnostic in any build (J12), which is the worse of the two precisely because nothing traps. It is the repository's standard idiom, the oneList<T>andBitArrayalready use, and CLAUDE.md's counter invariant was followed exactly:detail::MutationCounter, never a bare integer, because++on a signed counter is undefined atINTCS_MAXand an implicit assignment would transplant the source's counter into the destination.JsonArrayandJsonObjectgo 48 → 56;JsonNodeandJsonValueare unchanged, which is what shows the counter went where the enumerators are and nowhere else. Zero consumer sites. The module edge was made explicit by the validator rather than by me.MutationCounterlives inmodules/collectionsand the two headers are public, soText.JsonneededCollections.Coreas a public dependency where it had been private — and the boundary validator rejected the private declaration outright. A local copy was never an option; CLAUDE.md forbids it in terms. The graph is unchanged at 41/93: the edge already existed and only its kind moved, which is a smaller change than #1814's. Two of my own measurements were wrong and the compiler corrected both. "Zero first-partybegin()/end()sites" missedJsonNodeParseDepthTestsiterating withit->second, so the enumerator needsoperator->— guarded there too. And a shipped #1886 layoutstatic_assertfired at compile time with a runtime one failing beside it; both are updated, the two figures that did not move are left as #1886 wrote them, and the growth is additionally asserted as a relationship so a later member cannot hide behind a hand-updated literal. Six mutations, all caught — but M6 only after the test was strengthened, and the first result is recorded rather than quietly fixed. Abegin()that bumps the counter leaves a single range-for working perfectly, because the one enumerator snapshots the versionbegin()just produced; it is observable only with two enumerators over the same unmutated container.SetItemearns its own row for the mirror reason — it changes no element count, so only the counter can notice it at all. It was 17,585 immediately before, measured on 2026-08-19 by ticket #1888. ±0, and deliberately so: three shipped pins were replaced by two and one case was added. #1888 deletesJsonNode's four copy/move members and movesDetachParentto protected withJsonArray/JsonObjectas friends, closing three measured defects — a copy gave a second container sharing the same children, each still reporting the original as its parent (J08); assignment sliced, rewritingparent_on a node still stored in a container (J09); and publicDetachParentlet one node sit in two containers (J13). A .NETJsonNodeis a reference type, so there was never an object copy to translate, andXObjectalready deleted all four — this ends an asymmetry inside the port rather than inventing a restriction. Measured impact: zero first-party copy/assign sites and zero in both consumers. The header's own note aboutDetachParentwas wrong and the reference corrects it: it claimed to mirror "JsonNode.cs's internalDetachParent", and there is noDetachParentonJsonNode.csat all — .NET puts it on the containers,privateon each (JsonObject.cs:316,JsonArray.IList.cs:231), bodyitem?.Parent = null, withParent's setterinternal. Protected-plus-friends is that reachability in C++. Four pins were inverted and the fourth was not where the measurement said to look:JsonNodeTeardownTestsbuilt its second container withmake_shared<JsonArray>(realOwner), a copy commented "shares children" — a grep forX = *ymissed it and the compiler found it; its real subject is #1886's== thisguard, so it is rewritten to reach that guard without a copy. Three mutations; M1 and M3 caught at compile time, and M2 is NOT caught and is reported as such. Restoring the move members is a proven equivalence, measured with a probe for two independent reasons:JsonNodeis abstract, sois_move_constructible_vis false whatever the declaration says, andJsonArray/JsonObjecteach have a user-declared destructor (#1895's iterative teardown) that suppresses their implicit moves, so move-constructibility falls back to the already-deleted copy. The deletion is kept for intent and becomes load-bearing the day a container drops that destructor; the header and the fixture's own site say so, so nobody is misled about what rejects the spelling today. And it finally wrote the fixture #1894 could not.test/consumer/text_json_node_lifetime_negative.cppis named in #1894's acceptance criteria, and #1894 recorded — correctly — that it "cannot be started, not merely should not be", because no CCF-019 repair had outlawed any spelling. #1888 is the one that did. Fixture set 45 / 231 → 46 / 236. It was 17,585 immediately before, re-measured on 2026-08-19 by ticket #1894, which closes it. What that number does and does not depend on, audited 2026-08-19 and measured rather than reasoned. There are zeroDISABLED_tests in the repository — worth checking because a disabled test is invisible to both halves of this rule: gtest omits it from the run total and it never appears as skipped, so it could hide from the count and from the "0 skipped" claim at the same time. There are 56GTEST_SKIPsites, none of which fires here, and all of which are environment-conditional — chiefly tzdata zone availability (~21),/procdescriptor counts (8), signal delivery (4), locale availability (3) and DNS (2). The run total is therefore environment-independent with respect to those skips, and only the pass/skip split moves. That was proved with a three-test probe rather than assumed: a conditionalGTEST_SKIPleavesN tests … ranunchanged and dropsPASSEDby one. So a container withoutAmerica/New_Yorktzdata still readsrun=17,585, with ~21 skipped and ~17,564 passed — the floor this rule protects is intact there, which is exactly what a no-regression rule needs and what the existing qualification below (about failures differing by environment) does not by itself say. ±0, and deliberately so: #1894 repaired a test rather than adding one. It closed on a rule-14 sweep — its gate had gone stale that same day. Its own note set the condition, "close the negative-fixture half as not applicable if #1899 is declined/wontfix", and #1899 was declined on 2026-08-19 while #1888 remains declined, so both became determinate. The negative-fixture half is not applicable, and that is verified rather than trusted: a probe against the shipped headers confirms all four spellings #1888/#1899 would have outlawed are still legal —JsonArraycopy- and move-constructible,JsonNode::DetachParent()callable, andExtensions::Ancestorsreturningstd::vector<XElement*>. A negative fixture asserts a spelling is rejected by the compiler; with nothing rejected, writing one would mean inventing an outlawed spelling to pin. This is not a claim that CCF-019 produced no fixtures at all — its async members did, in #1959 — only that the two owned-tree modules #1894 scoped outlawed nothing. The sanitizer half was re-measured because the recorded figures were stale, and the re-measurement found a real defect. The note cited a clean run over 218/218Text_Jsonand 184/184Xml_Linqfrom 2026-07-31; both suites have since grown to 302 and 349, so those figures describe a tree that no longer exists. Re-run: ASan+LSan clean on both, and UBSan abortedText_Jsonon a misaligned reference binding inchar_traits<char16_t>::length. It is a defect in a test, not in production code, and finding which construction caused it needed measurement rather than reading — the array copied verbatim into a standalone probe did not reproduce, nor did any literal alone. The manifestation is translation-unit-layout dependent: the emptyu""literal is merged into a 1-byte-aligned mergeable section beside narrow literals, solength()binds aconst char16_t&to an odd address. One line —u""becomesstd::u16string(), the same empty string with no literal to misalign — and the mutation that restores it reproduces the report. The instrumentation was shown able to report before any of this was believed (the #1957/SR-AUD-204 lesson): deliberate defects built with the same flags gave one report each for ASan, LSan and UBSan, and the no-defect control gave none. The write-up also corrects that document's running total, stale since 2026-08-04 at 11 fixtures / 94 sites, to the measured 45 / 231. It was 17,585 immediately before, measured on 2026-08-19 by ticket #2384 (unit 3), which closes that ticket. +3 on the 17,582 below, inSharpRuntimeTests_Core_Base(6,084 → 6,087). Purely additive: ninefromconversions on each 16-bit float, truncating toward zero — where truncate and floor part company for negatives — and allexplicit, so a 16-bit float can never silently become an integer. Only thefromdirection landed, and the reason the other half did not is the unit's real finding. .NET declares 43 conversions onHalfand 47 onBFloat16; four groups cannot be transcribed. The 13operator checkedvariants have no C++ counterpart at all — C# selects them inside acheckedcontext and there is simply nothing to write.nint/nuintare measured to be the same C++ type aslongcs/ulongcshere (std::intptr_tislong), so a separate overload is a redefinition, not an addition — the conversions exist, through those.ushort→Halffinds its signature already taken by the opposite meaning:Half(uint16_t)is the raw bit pattern where .NET's is(Half)(float)value. And the fourth group was found by building it rather than by predicting it. Addingexplicit Half(intcs)makes C++ overload resolution prefer it for an int literal — an exact match beats anint → uint16_tconversion — soHalf(0x7BFF)silently stops meaning "these bits" and starts meaning "the number 31743". This type's own constants are written that way, and landing the constructors turned 44 shipped tests red,Half::MaxValueandHalf::NegativeInfinityamong them; a minimal probe isolates it. Resolving it means renaming the raw-bits constructor — a public source break across 66 first-party sites with a silent meaning change for any site not migrated, which is the dangerous class — so it is #2395, a decision rather than a transcription. Two further measurements came from writing the test rather than reading the type: the raw-bits constructor already accepts any integer convertible touint16_t, soHalf(someSByte)compiles today and means bits — "is it constructible" therefore cannot be the pin, only the meaning can be; and the two types have different shapes of the same hazard, becauseBFloat16's extrafloatconstructor makes an int literal ambiguous there whereHalfsilently picks raw bits. One decision, two migrations. One divergence is already decidable and is recorded with #2395: .NET makes thebyte/sbyteconversions implicit, and reproduced as implicit converting constructors they make everyintargument ambiguous — measured, because C++ permits a standard conversion before a user-defined one and C# does not. Whatever #2395 decides, those two must beexplicit; what is lost is the implicitness, never the conversion. Four mutations, all caught. It was 17,582 immediately before, measured on 2026-08-19 by ticket #2384 (unit 2b). +3 on the 17,579 below, inSharpRuntimeTests_Core_Base(6,081 → 6,084). Purely additive: 39 members on each 16-bit float, the transcendental, root, power and angular families. Every one is a float round-trip, and that is .NET's own shape — verified member by member againstHalf.csandBFloat16.cs— so unlike units 1 and 2a there is no bit-level body to get wrong, which changes what is worth testing. The risk in a forwarding table is not arithmetic but mis-wiring:SincallingCos, orAtan2's arguments swapped, and every compile-only test passes against both. So each case asserts that a name reaches its own float function, comparing againstFromSingle(expected(in))rather than a literal — a test of the forwarding rather than ofMathF's accuracy — plus explicit cross-checks that three members disagree on the same input, plus operands chosen so a swapped argument order gives a different answer. Seven mutations, all caught. Unit 2a's own estimate was wrong and the measurement corrects it. It put unit 2b at "~45 members per type, mostly one-line forwards"; only 39 could be forwarded, split 25 viaMathF, 13 viaSingle, and 10 via neither. Those ten —Compound,ExpM1,Exp2M1,Exp10M1,LogP1,Log2P1,Log10P1,Lerp,MultiplyAddEstimateandClampNative— have no counterpart in this port'sSystem::MathForSystem::Single, so adding them to the 16-bit floats would mean wideningfloat's own public surface first: a different type's API, and a different question from the one this ticket asks. They are pinned absent on both types, so whichever ticket widensSystem::Singletrips that pin and can complete the 16-bit types in the same change;MultiplyAddEstimateandClampNativeare doubly absent, sincefloatlacks them here and .NET declares them onHalfonly.Ieee754Remainderis the one member whose .NET name and this port'sMathFname differ (IEEERemainder), so its forward is spelled out and pinned — a mis-wiring there would be invisible to a name-based reading. Only unit 3 remains: 43 conversion operators onHalfand 47 onBFloat16. It was 17,579 immediately before, measured on 2026-08-19 by ticket #2384 (unit 2a). +5 on the 17,574 below, inSharpRuntimeTests_Core_Base(6,076 → 6,081). Purely additive: rounding (Ceiling,Floor,Round×2,Truncate),Sign, and the IEEE 754:2019*Numberfamily on both 16-bit floats, plusMaxNative/MinNativeonHalf. The*Numberfamily is notMax/Min, and two rules separate them — both pinned, because a forward toMaxsatisfies every ordinary row and fails exactly these: it does not propagate NaN (Max(NaN, 2)is NaN,MaxNumber(NaN, 2)is2, from either side, and .NET says so in its own comment), and+0is treated as larger than-0, which no comparison can see since+0.0 == -0.0, so the pin asserts the bits — a naive(x > y) ? x : yreturns the wrong zero and passes everything else in the file.Signhas two transcribed edges: it throwsArithmeticExceptionon NaN rather than returning a sentinel, and it testsIsZerobeforeIsNegative, soSign(-0.0)is0, not-1.Roundis ties-to-even throughMathF::Roundrather thanstd::round, and the swap is caught. The unit's most useful finding is a limit on #2340's own rule. Measured by diffing the two reference surfaces,MaxNative,MinNative,ClampNativeandMultiplyAddEstimateare declared onHalfonly — so in step means each type gets what .NET gives it, not that the two surfaces are identical, a distinction that only becomes visible once the surface is large enough to differ. Their absence onBFloat16is pinned, so a later unit that "completes the symmetry" has to justify inventing them. Six mutations, all caught. The remainder of #2384 is measured rather than estimated: unit 2b is ~45 members per type, mostly one-line forwards, and unit 3 is 43 conversion operators onHalfand 47 onBFloat16; unit 2b's absence is pinned on both types viaSqrt. It was 17,574 immediately before, measured on 2026-08-19 by tickets #2387, #2207, #2364, #2384 (unit 1) and #2338. #2364 is the only one that changed a value a caller gets, and it changed eight of them: the POSIXGetFolderPathtable is now .NET's. #2320's recorded reason for stopping was measured false — it said the rest "has no .NET mapping, so it would cross from parity into invention", and every remaining row maps ontoReadXdgDirectory(GetFolderPathCore.Unix.cs:220-249) or onto a static path, so these are alignments, not widenings. The sharpest row isPersonal/MyDocuments, which returned the home directory itself — an application writing "the user's documents" wrote into$HOME— and now returns aDocumentssubdirectory;UserProfileis asserted not to have moved with it, because the two used to be the same answer and a repair that moved both would look correct without that row. Two rows are the reverse shape:ProgramFilesandSystemwere invented here as/usrand/usr/libwhere .NET maps them only underTARGET_OSX, so the alignment removes a mapping.ReadXdgDirectoryis transcribed including every rejection in its line grammar — a value must be$HOME/-prefixed or absolute and anything else is skipped rather than accepted — and it readsuser-dirs.dirsout of the XDG config directory, so it honoursXDG_CONFIG_HOMErather than a hard-coded~/.config. The eight rows were never covered, which is why the divergence survived: the full suite passed unchanged before and after. Every new case setsHOMEand the XDG variables explicitly underDoNotVerify, so what is asserted is the mapping rather than what this machine happens to have — the #2320/SA-6 lesson turned into a rule. Six mutations, all caught. #2387, #2207, #2338 and #2384 unit 1 are the batch's other half. #2387 leavesExternalException::ToString()absent and pins it: a virtual would be the #2374 shape, but where that slot bought an overridable lease policy nothing else could provide, this one would only relocate the naming problem into each ofcna's three derived types — and a stored name growssizeofand breaks all three. #2323's rule decides it: a message naming the wrong type is a lie, where an absence is merely an absence. #2207 declares theIsolatedStorageTOCTOU with its threat boundary, which is the part that makes it acceptable rather than tolerated: it does stop an absolute path, a..climb and a pre-existing symlink — the case people assume the race defeats, and it does not — and it does not stop an attacker who can already write inside the store root, who already reads and writes every file there directly, so the race widens their reach beyond the store rather than granting access to it. Both halves are asserted rather than argued. #2338 keeps Unicode normalization invariant, and its premise was stale twice: the ticket says/rvis absent (it is present) and #2386 has since measured that .NET returnstrueand the argument unchanged in invariant globalization mode, saying so in its own comment — so this port was never diverging, and the question was never when do we fix this but do we add a capability .NET itself only has through ICU or NLS. Own UCD tables plus UAX #15 and an ICU dependency were both declined; measured zero call sites in both consumers. #2384 unit 1 gives both 16-bit floats nine members, and #2340's in-step rule worked as designed — adding them toBFloat16alone broke the build, because #2382's pin asserted their absence with the message "it must move System::Half too". Four of the nine are bit operations, not float round-trips, and .NET says so itself:CopySigncarries the comment "required to work for all inputs, including NaN, so we operate on the raw bits". Three edges are transcribed rather than derived —-Infinityincrements toMinValue,+Infinitydecrements toMaxValue, and-0.0increments toEpsilonwhile+0.0decrements to-Epsilon. Every body was derived per type, which is #2382's lesson applied rather than quoted; for these nine the two references agree, and that was checked. Six mutations, all caught — but M6 only after a measurement corrected the test:BFloat16::Absvia a float round-trip went uncaught because the case used0x7FC1, a NaN that is already quiet; probed over all 65,536 patterns the two forms differ on exactly 126, every one a signalling NaN, and the case now uses0x7F81and asserts exact bits. Units 2 and 3 remain and their absence is pinned on both types. It was 17,561 immediately before, measured on 2026-08-19 by tickets #2374, #2392, #2058 and #2381. +2 on the 17,559 below:SharpRuntimeTests_Core_Base→ 6068 andSharpRuntimeTests_Numerics→ 344;SharpRuntimeTests_Buffersis unchanged at 630, because #2058 widened an existing case rather than adding one. #2374 givesMarshalByRefObjectthevirtualInitializeLifetimeService().NET has (MarshalByRefObject.cs:22-26); its absence turned an observable runtime diagnostic into a compile error at an unrelated place. The class already has a vtable, so this inserts a slot here and in both derived classes,AppDomainandContextBoundObject— a silent binary break, granted per action on the measurement that all three types have zero sites in both consumers.virtualis the point rather than a detail: a non-virtual member of the same name satisfies a presence check, compiles at every call site, and silently defeats the override the .NET member exists for — which is why #2297 left it absent rather than add it that way, and why the new case calls through a base reference and asserts dispatch, not presence. #2297's absence pin is inverted and renamed, and its detection idiom is kept rather than replaced by a direct call, because that is what makes presence and absence expressible in the same form. Three mutations, all caught; a first run of the set was invalid and is recorded rather than counted — the restore step copied a pre-change backup, so two mutations reported ANCHOR MISSING against a file that no longer had the member. #2392, #2058 and #2381 changed no production statement. #2392 declares thatTotalOrderIeee754Comparer::GetHashCodehashes the bit pattern where .NET hashes the value (TotalOrderIeee754Comparer.cs:198-202, whoseDouble.GetHashCodecollapses all NaNs and both zeros); adopting .NET's coarser hash was declined, and the contract holds either way — equality here is bit-pattern identity, so equal never hashes differently and the choice is about distribution, not correctness. The pin states the two rows as a difference from .NET rather than only as a property of this port, and additionally pins thatDouble::GetHashCodealready matches .NET, so a future reader does not fix the wrong one. #2058 declaresReadOnlySequence<T>single-segment only: .NET hasReadOnlySequence(startSegment, startIndex, endSegment, endIndex)(ReadOnlySequence.cs:94) and computesIsSingleSegmentas_startObject == _endObject(:41-45) where this port hard-codestrue; its premise was partly stale — the false class comment had already been removed by an earlier ticket, so what remained was the ticket's own framing as blocked. The existingstatic_asserts become the declaration, and the single-segment case was widened from two constructors to all five, because an answer that is constant is only demonstrably constant if every door is tried. #2381 is the one instruction that was granted and could not be carried out, and it was measured rather than assumed. The user approved editingcna's XNBDateTimeReaderto stop discarding theDateTimeKind, conditioned on the promise to check both sharp-runtime versions first. Measured:develop— whichcnabuilds against — hasexplicit DateTime(longcs ticks)and noDateTimeKindat all, whilenexthasDateTime(ticks, kind)andSpecifyKind. This is #2366's trap without #2366's escape: there,{}meant remove under both versions, so one spelling served both; here no expression means preserve the kind in a version that has no kind, and arequires-guarded form does not help because the unqualified name lookup is a hard error rather than a substitution failure. Editing anyway would leavecnaunbuildable, which is worse than the documented deviation it carries, so the ticket records the exact merge-time diff and isblockedon the merge rather than on a decision. It also records what the repair will not buy: #1941 landed phase 1 only, so a preservedKindis truthful and round-trippable but changes no conversion. It was 17,559 immediately before, measured on 2026-08-19 by ticket #2393. +1 on the 17,558 below, inSharpRuntimeTests_Uri(307 → 308; 51 constructions across 24 tests updated in place, two assertions inverted, one case renamed, one added). #2393 makesUri(std::string)require an absolute URI. The port had TWO absolute-URI grammars in one type: the one-argument constructor calledparse()and never checkedisAbsoluteUri_, while the(string, UriKind)overload did — andTryCreategoes through that overload — soUri("://example.com/")succeeded whileTryCreate("://example.com/", UriKind::Absolute, u)returned false, letting a caller construct aUrithis port's ownTryCreatesays is not absolute. Reachable from ordinary code, since aUriBuilderwith an empty scheme renders exactly that andgetUriProperty()isUri(ToString()). The direction was derived rather than chosen: .NET has one grammar by construction —new Uri(s)isCreateThis(s, false, UriKind.Absolute)(Uri.cs:424-429),TryCreate(s, Absolute, out u)isCreateHelper(s, false, UriKind.Absolute)(UriExt.cs:223-227), andCreateThisthrows exactly whatCreateHelperreturnsnullfor. The repair is one line. The narrowing is wider than the ticket described and is stated plainly rather than discovered later: the reported symptom was one odd string, but the constructor no longer accepts any relative reference. Relative-URI support is untouched — every accessor, the query/fragment split and base resolution are unchanged; only the default kind moved, onto .NET's, and the migration is to name it. Measured: zero real consumer sites (cna's singleSystem::Urimatch is inside a comment) and zero in mobile-eggbert; first-party, 51 constructions across 24 tests, each keeping its coverage through the two-argument constructor because they were testing relative-URI behaviour, not the constructor's default. Two shipped assertions were inverted because they asserted the defect, one of them ending withEXPECT_NO_THROW(Uri("/relative/path"))and calling it "what makes the distinction observable rather than theoretical" — it was observable, and it was the defect. One consequence is worth following, because it moved three times in one day: #2391's mutation M1 was recorded as an unobservable equivalence, then measured wrong — because of this very defect — and is now an equivalence again, sinceselfcan no longer be aUrithe strict comparand parse would refuse. The original reasoning was right and was defeated only by a bug one layer down; the case is renamedDecl2391_…IsNowAnEquivalenceand records all three steps, so the line staysRelativeOrAbsolutebecause .NET writes it, not because it is load-bearing. The new pin asserts the equivalence over an 11-row corpus rather than the one reported string, because a repair that fixed only the reported example would pass a single-row test. Four mutations, all caught. It was 17,558 immediately before, measured on 2026-08-19 by tickets #2155, #2199, #1896, #2366 and #2377. +15 on the 17,543 below:SharpRuntimeTests_Timers36 → 38,SharpRuntimeTests_Text_Json301 → 302,SharpRuntimeTests_Xml_Linq337 → 349. #2155 givesTimers::TimertheSystem::Objectbase soElapsedreports the raising timer, as .NET does (Timer.cs:313); it had reportednullptrfor a structural reason —EventHandler<T>::Raisetypes its sender asObject*andTimerhad no such base, sonullptrwas the only value that compiled. The obvious alternative does not exist here: .NET derives fromComponent(Timer.cs:15) and this port has noComponentModelComponentat all, so the divergence is in the base, not the sender.sizeof104 → 112 and a new vtable; two shipped pins were inverted and both failed the build, each having said in terms "#2155 landed without updating its pin". Four mutations, all caught — M2 passes some otherObject*, which is why the pin asserts identity rather than non-nullness. #2199 implementsXObject's Changed/Changing, inert since it was ported. Both gates opened the same day. Layout:sizeof(XObject)16 → 24 and every derived type by exactly one pointer — aunique_ptrallocated only on first registration, so an unobserved tree pays a null pointer and no allocation, the closest analogue of .NET's annotation slot; all ten figures match the approval exactly and three shipped layout pins failed the build. Removal is a registration token, becausestd::functionhas nooperator==and a handler therefore cannot name its own registration — not a cost question but an impossibility, soadd_*returns a token andremove_*takes it; removing all registrations and throwing were both offered and declined. Ids are process-wide, so a token from one object can never match another's. The semantics are transcribed: bubbling innermost-first, the sender being the object changed (the child for Add/Remove, .NET's own asymmetry atXLinq.cs:156,177),Changingbefore andChangedafter, and — the subtle one —notifymeans "any object on the chain carries registrations", not "a changing handler ran", which is what keepsChanged-only subscriptions alive.XElement::setValuePropertydeliberately raises nothing of its own, because .NET's setter isRemoveNodes(); Add(value);and aValueevent there would be invented. Nine mutations, all caught, four only after test repairs, and three share one root cause: the recorder captured only theChangedhalf's kinds and senders, so a mutation corrupting theChanginghalf alone passed — half a pair is still a wrong notification. The fourth was a vacuous assertion: the foreign-token case removed A's registration before trying B's token, so it returned false for the wrong reason and a per-object counter went undetected. #1896 takes deep tree construction from quadratic to linear — 100,000 levels go 42.583s → 0.045s for JSON and 120.028s → 0.051s for XML, measured on the same binary and flags. Its approved layout growth was NOT needed and was NOT taken: the node being attached is parentless (already checked), so it can only be an ancestor ofparentif it has a subtree — a childless node cannot contain anything — soparent == thisis checked directly and the walk is skipped otherwise.sizeofis unchanged everywhere and no virtual was added. The trap is that an empty container short-circuits, so the self-check must sit OUTSIDE the emptiness guard; moving it inside makes self-attachment legal again, and both mutations that do so are caught. There were TWO quadratic sources and only one was in the ticket: fixing the guard left XML at 89.3s, still quadratic, because #2199's own ancestor walk — added earlier the same day, and .NET has exactly that shape (XObject.cs:424-427) — visits every ancestor even with nothing subscribed. A process-wide count of live registrations fixes it exactly rather than approximately: zero means no walk can find anything. Eight mutations, all caught, but M7 and M8 were first reported NOT CAUGHT and that was the honest result — a leaked count corrupts nothing, every assertion still passes, and the suite merely got slower until one run hit the 240-second mutation timeout; a mutation caught only as a timeout is not caught by name. The two cases now time the build: bound 2 s, linear ~40 ms (50× headroom), M7 13,870 ms and M8 14,098 ms (7× over). #2366 and #2377 are the twocnarepairs, landed in that repository's working tree under a per-action instruction with no commit authorised. Both carry a premise correction. #2366's is thatcnabuilds against the sibling checkout, which is ondevelopand does not contain #2313 — so cna is not broken today and both the defect and its repair arrive with the merge; that madestd::nulloptthe wrong spelling, since it does not compile againstdevelop, and the repair uses{}, which means remove under both versions. #2377's is that the ticket named five types and there are seven:NetworkNotAvailableExceptionandNetworkSessionJoinExceptionchain to a base default constructor and would have inherited a message naming their base, which is #2323's own rule one level down. Both repairs are version-independent andCnaTestspasses 68/68. Module graph unchanged at 41/93; negative fixture set 44/226 → 45/231. It was 17,543 immediately before, measured on 2026-08-19 by tickets #2170, #2390, #2391, #2118, #2185 and #1899. +10 on the 17,533 below:SharpRuntimeTests_Numerics341 → 343,SharpRuntimeTests_Threading522 → 523,SharpRuntimeTests_Uri305 → 307,SharpRuntimeTests_Text_Json300 → 301,SharpRuntimeTests_TimeZone188 → 190,SharpRuntimeTests_Xml_Linq335 → 337. All six wereneeds_userand all six were answered on the same day, which is what this checkpoint really records: the queue's remaining blockers were decisions, not evidence. They are written up as SA-12 and SA-13 indocs/StandingApprovals.md, and two of the six were decided against the recommendation on the record — that is the user's call and the reasoning is preserved so the trade stays visible rather than relitigated. #2170 givesTotalOrderIeee754ComparertheIEqualityComparer<T>base it has advertised in its own doc-comment since #2169, buying polymorphic binding and nothing else — before it, passing one where the interface is required was a compile error, so no call site can have depended on the old behaviour.sizeof8 → 16, the subobject at offset 8, pinned as an offset rather than only a size because a single base that merely grew would give the same 16. The growth has no .NET counterpart and that is stated rather than implied: .NET's comparer is areadonly structand a C# struct pays nothing to implement an interface, so the second vptr is a C++ artifact.IEquatable<TotalOrderIeee754Comparer<T>>is deliberately not reproduced — .NET's body is=> true, which a defaultedoperator==on an empty type already says, and a third base would cost a third vptr to express nothing. A divergence the ticket did not name was found and deliberately NOT bundled: .NET'sGetHashCodeisobj.GetHashCode()— the value's hash, whichDouble.GetHashCodenormalizes so "all NaNs and both zeros have the same hash code" — while this port hashes the bit pattern, so-0.0and+0.0hash differently. Both satisfy the contract and .NET's is merely coarser, but the difference is directly observable through a public member, and closing it inverts five shipped pins, so it is #2392. Mutation M4 (adopt .NET's hash) is caught byFloat_SignedZerosAreDistinct, which is the evidence those pins are load-bearing. Module graph unchanged at 41/93 —IEqualityComparer<T>is inCore.Base, already a dependency. Five mutations, all caught. #2390 is a general rule rather than a decision about one type, recorded as SA-12: where this port has a real creator,internalmembers becomeprivatewith that creator afriend(the #2298 shape); where it has none, they stay public and the accessibility divergence is recorded in the header. Both mechanical alternatives were declined for stated reasons —privatewith no friend leaves the type impossible to instantiate, costing the tests that verify its fixed message and HResult, which is the only observable content such a type has;privateplus a friend that never constructs it is a dead friend declaration that looks faithful while granting access to a class that will never use it.ThreadStartExceptionis the second case, becausestd::threadeither constructs or throws and .NET's window does not exist here; the divergence is in accessibility alone and the pin asserts the shape has not widened with it. #2391 adopts .NET'sUriBuilderdelegation and WITHDRAWS the non-throwing guarantee #2004 measured and chose — both members now go through the builtUri(UriBuilder.cs:277-279), so identity becomes canonical (host case folded, an explicit default port resolved away) and a builder whose rendering does not parse throws from both,b.Equals(b)included. It is a convergence rather than a reversal, and #2004's own justification had already become false: #2004 argued the string hash was value-identical becauseUri::GetHashCodehashedabsoluteUri_verbatim, and #1995 made it hash a canonical identity key earlier the same session — keeping #2004 would have meant a builder and theUriit builds hashing differently, which is the defect #2004 existed to prevent, one level up. The asymmetry is .NET's:selfgoes through theUriproperty and throws, the comparand is handed over as a string and merely returns false. Four pins inverted, each having been written anticipating exactly this. Five mutations, all caught — but M1 only after the test meant to catch it was rewritten, and the first record of it was wrong: it was filed as an unobservable equivalence on the reasoning thatgetUriProperty()is always absolute, and probing the premise instead of trusting it showed this port'sUri(std::string)accepts"://example.com/"while its ownTryCreate(s, Absolute)rejects it — two absolute-URI grammars in one type, filed as #2393. #2118, #2185 and #1899 changed no production statement: they are decisions and their evidence, the shape of #2202, #2015 and #2324. #2118 declares thatJsonElement::GetRawTextre-renders — .NET slices the original document bytes (JsonElement.cs:1196-1201→JsonDocument.cs:700-704) and nlohmann's DOM retains no source spans at all; retaining them was declined because it is paid at parse time and in memory by every caller whether or not the member is called, and #2117 had already grownJsonElement48 → 56 the same session. What is lost is the representation, never the value, and re-rendering is idempotent, which is asserted rather than asserted-about. Its gated pin was renamed and re-roled as a declaration, each row now also recording what .NET would have returned. #2185 makes SR-AUD-228 a permanent deviation in the reflection deviations' shape, on two measurements: it is not closable by sampling libc at any granularity — two zones can agree on every sampled instant and still differ in rule, so no finer sampling turns offsets into rules, and closing it needs tzdata's own TZif structures, which are out of scope — and the failure is one-directional, this port can only ever be too permissive, so a caller usingHasSameRulesas a necessary condition is correct and one using it as sufficient is not. The layout cost an earlier design measured (160 → 184) is real, is not what blocks it, and is not paid. #1899 closes carrying its own impossibility proof, recorded beside the contract it justifies rather than only in the ticket: an owning handle to an object noshared_ptrowns cannot be manufactured (the topmost ancestor has no parent), andXElement/XDocument/XContainerare routinely automatic-storage — 51 declarations in this repository's own tests — soenable_shared_from_thiswould throwbad_weak_ptrat a correct call site rather than rescue anyone. Its two premises are pinned, because a proof whose premises silently stop being true is not a proof. It was 17,533 immediately before, measured on the same date by ticket #1996 (groups G-3 and G-4), which closes that ticket. +5 on the 17,528 below, inSharpRuntimeTests_Uri(300 → 305; two pins inverted in place). G-3 makessetSchemePropertyreject text that is not a scheme — a narrowing, which is precisely what SA-5 grants — and the truncate-at-colon retry is the half the ticket's summary omitted: §14.2 says only "throw for an invalid one", which would reject"http:", while .NET accepts it and storeshttp, retrying after cutting at the first:(UriBuilder.cs:108-134); an empty scheme is accepted too, the whole block being guarded onvalue.Length != 0. One pre-existing row's subject moved and the guarantee got stronger: G-2's pin asserted a non-ASCII scheme is folded invariantly, and G-3 makes such a scheme unstorable, closing the Turkish-locale hazard by construction rather than by the fold's implementation. G-4 promotes a relative constructor string, which used to render the unparseable:///www.example.com/path. The summary is wrong about the host: it says "andlocalhosthost", while .NET prefixeshttp://and reparses, so the host comes from the string — a mutation usinghttp://localhost/is caught. And one of my own expectations was wrong, corrected by the reference: I asserted the promoted render washttp://www.example.com/path, it is…:80/path, and .NET renders the default port too (_port = _uri.Port, appended whenever!= -1) — the test now asserts the stronger property that the promoted and explicit routes are identical. Six mutations, all caught. Downstream: zero sites. One boundary stays deliberately untaken and pinned — .NET'sHostsetter refuses"contoso.com/path"and this port stores it, which belongs to G-1's block. It was 17,528 immediately before, measured on the same date by ticket #1995 — 17,528 run, 17,528 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1995. +6 on the 17,522 below, inSharpRuntimeTests_Uri(294 → 300; two pins inverted in place, one obsolete pin removed).Uri's identity was the raw input string, soHTTP://EXAMPLE.COM:80/Pathandhttp://example.com/Pathwere unequal with different hashes. Both members now feed from one canonical key, which is how .NET keeps them consistent — it renders both fromUriComponents.HttpRequestUrl=Scheme | Host | Port | Path | Query. The design record proposed only folding and the default port; the reference also excludes the fragment and the user-info, saying so in a comment — "Fragment AND UserInfo (for non-mailto URIs) are ignored" — sohttp://a/p#one == http://a/p#twoandhttp://u@a/p == http://a/p, which §14.1's wording would never have predicted. A trap measurement found, not the record:defaultPortForSchemematches lower-case names only, so this port parsesHTTP://example.com/with port −1 and the lower-case form with 80 — comparing stored numbers would have left them unequal even after folding the scheme, defeating the repair on its own motivating input; the key resolves the default from the folded scheme, for comparison only, and the parse is untouched. Not reproduced and stated: .NET compares UNC/DOS paths case-insensitively and hashesfile:URIs likewise; this port models neither, so equality is narrower than .NET's — never equal where .NET is unequal.UriBuilderis deliberately not delegated, though §14.1 and .NET both say it should: delegating reintroduces the throw #2004 measured and removed, so the two landed decisions conflict and the reference cannot settle it for this port — filed as #2391. Six mutations, all caught; M2/M3/M6 were invalid as first written (anchors spelling\x01as an escape rather than literal text) and were re-run rather than counted. Downstream: zero sites — the one grep hit is a comment sayingcnadeliberately avoidsSystem::Uri. It was 17,522 immediately before, measured on the same date by ticket #1997 (group A-3) — 17,522 run, 17,522 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1997 (group A-3). +5 on the 17,517 below, inSharpRuntimeTests_Uri(289 → 294). A-3 givesUrithe two overloads that let aUriCreationOptionsreach a Uri operation at all — before this, a value could be constructed and read and then had nowhere to go, which is SR-AUD-149's consumer half. Purely additive, under SA-5. The plan's wording implied the option type had to be written too, and the compiler corrected that: it already existed, and a first cut declaring it alongside the overloads failed with "redefinition of class System::UriCreationOptions" — only the overloads were missing, and the existing type is left untouched, including its public data member, because .NET's is an unvalidated settable auto-property that a public field is observationally identical to (#1969's reasoning forChannelOptions). Both overloads resolve againstAbsolute, and that is the reference's choice (Uri.cs:476-480,UriExt.cs:236-240), so a relative string throws through the constructor and fails throughTryCreateeven though the sibling one-argument constructor accepts one — all three asserted together so the asymmetry is observable rather than theoretical. The option itself stays inert and that is disclosed rather than implied: .NET's flag disables path/query canonicalisation and this port performs none, so aUribuilt with it set and one built with it clear are byte-for-byte identical — asserted across three inputs chosen for dot segments, percent sequences and a default port, and if that test ever fails the port has grown canonicalisation and the disclosure must be revisited. The header's obsolete half-warning is replaced accordingly; leaving it would be the SR-AUD-168 defect. Three mutations, all caught, two run once per overload because they are independent bodies. Downstream: zero sites. A-2 and A-4 remain — A-2 on a measured module-boundary cost, A-4 on a vtable and access-level change. It was 17,517 immediately before, measured on the same date by ticket #1999 — 17,517 run, 17,517 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1999. +2 on the 17,515 below, inSharpRuntimeTests_Uri(287 → 289; one pin inverted in place).UriTypeConverter::ConvertFromreturned a by-valueUri, which cannot express .NET'snull, so an empty string was forwarded straight to the constructor and threw where .NET returns null (UriTypeConverter.cs:40-51). It now returnsstd::optional<Uri>. The widening is exactly one input wide, and that is .NET's own boundary: its comment — "Let the Uri constructor throw any informative exceptions" — says the empty case is the only short-circuit, so a malformed string still throws; that has its own pin, because "return the empty state on any failure" is the plausible over-correction. Landed under SA-10 rather than as an approval, and the recorded cost was wrong: §14.5 called it "a vtable-slot signature change plus mandatory migration for every override", but SA-10 names return type explicitly and routes it through SA-2 — SA-3's exclusion is a change to the vtable's shape, not a signature within an existing slot — and there are zero overrides and zero derivations anywhere, in-tree or downstream, so the migration was four test call sites. Four mutations, all caught; M4 only after a case was added — every test converted an absolute URI, so requiringUriKind::Absolutenever failed, and a relative input is the only one the kind discriminates, which is exactly why .NET passesRelativeOrAbsolute. M2 was invalid as first written (-Werror=unused-parameter) and was reformulated rather than counted. Fixture set 44 fixtures / 226 sites; site 3's diagnostic is "invalid covariant return type", more precise than the "does not override" predicted — gcc reads the old signature as an attempted covariant override. Downstream: zero sites. It was 17,515 immediately before, measured on the same date by ticket #1980 (group G-5) — 17,515 run, 17,515 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1980 (group G-5). +9 on the 17,506 below, inSharpRuntimeTests_Runtime(189 → 198; one pin migrated in place). G-5 givesMarshalAsAttribute.NET's field types —Valueget-only (SA-8),ArraySubTypeanUnmanagedTyperather than a looseintcs,SizeParamIndexashort— adds the two absent fieldsSafeArraySubType/IidParameterIndex, seals the class, and addsVarEnum,ComInterfaceTypeandClassInterfaceType. Field types are the contract for the same reason G-2's values were: a loose integer where .NET has an enum lets any number be stored where only a marshalling kind is meaningful. One member stays absent deliberately: .NET typesSafeArrayUserDefinedSubTypeasType?, which is reflection, and inventing a second string would look like parity while storing something else —MarshalTypeRefsurvives as a name-carrying string only because it already did. Six mutations, all caught, four at compile time. M4 is the one worth keeping: .NET'sVarEnumjumps fromVT_DECIMAL = 14toVT_I1 = 16, and asserting those neighbours catches a renumbering but not an insertion — insertingVT_UNUSED15 = 15went uncaught, because C++ cannot enumerate an enum's members. The fix is an exhaustiveswitchwith nodefault:: the build runs-Wall -Wextra -Werror, so-Wswitchturns any unhandled enumerator into error: enumeration value 'VT_UNUSED15' not handled in switch, pinning the enum's membership — and it fails the other way too, since removing an enumerator breaks the census's own reference. Fixture set 43 fixtures / 223 sites, site 2 being the assignment toValuethat let a caller retarget an attribute after construction. Downstream: zero sites. #1980 now has only G-3 left (a vtable and layout change SA-3 excludes). It was 17,506 immediately before, measured on the same date by ticket #1980 (group G-4) — 17,506 run, 17,506 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1980 (group G-4). +3 on the 17,503 below, inSharpRuntimeTests_Runtime(186 → 189; two pins migrated in place). G-4's point is that the port was wrong in BOTH directions, which is why one rule could not fix it:CompilerFeatureRequiredAttributepublished a fullIsOptionalsetter where .NET's is{ get; init; }— too permissive — whileObsoletedOSPlatformAttributeandRequiresPreviewFeaturesAttributetook aurlconstructor parameter .NET does not declare and fed it into a read-only accessor where .NET's is{ get; set; }— too inventive on the constructor and too restrictive on the property.initis two facts, not one: the value can be supplied at construction and cannot be assigned afterwards, so removing the setter alone would have been a narrowing rather than a translation — the two-argument constructor is what makes the removal faithful, and one test asserts both halves together. Landed under SA-8 (the mutability half) and SA-5 (theUrlhalf). Five mutations, all caught, two at compile time; M4 and M5 are run once per type deliberately, because they are two separate declarations and fixing one is the easy half-repair, and M4 was invalid as first written (its anchor spanned doc-comments that differ between the types) and was reformulated rather than counted. The absence pin uses a dependent parameter (the #2299 gcc trap). Fixture set 42 fixtures / 219 sites, site 4 being the trait query that breaks a consumer silently. First-party migration was two sites, both tests, both found by the compiler; downstream zero sites in all three types. G-1 and G-2 landed the same day; G-3 (vtable) and G-5 remain. It was 17,503 immediately before, measured on the same date by ticket #1980 (group G-2) — 17,503 run, 17,503 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1980 (group G-2). +5 on the 17,498 below, inSharpRuntimeTests_Runtime(181 → 186; one pin inverted in place). G-2 corrects the interop metadata values, and getting the numbers right is the whole contract of these types: P/Invoke is a declared permanent deviation, so they exist to preserve managed metadata rather than to produce an effect. TheLPStructfinding was understated: the plan recorded "48 → 43", a wrong number, and 48 isLPUTF8Str's value — so the two enumerators were indistinguishable,LPStruct == LPUTF8Strwas true, and aswitchoverUnmanagedTypecould not carry both arms. Two more divergences the plan never named were found by measuring the reference alongside the four it did: bothCharSetfields default to a named value here while .NET's are plain fields defaulting to 0, which is not a declared enumerator (Noneis 1) — reproducing an unnamed default is deliberate and follows from the types' purpose.Currency/IDispatchwere absent,Packwas 8 where .NET's plainpublic int Pack;gives 0, andPreserveSig/BestFitMappingweretruewhere .NET's plainpublic boolgives false — the port had already got the other three booleans right, which is what makes those two a divergence rather than a policy. Every otherUnmanagedTypevalue already matched, asserted across a spread so three repairs cannot be mistaken for a renumbering. Five mutations, all caught, M2 at compile time — removing an enumerator breaks the test naming it, the only way C++ reports that. The plan predicted the one pin that had to be inverted (DefaultPack_IsEight) and it was the only pre-existing test the group touched. Downstream: zero sites in both consumers. G-1 landed the same day; G-3 (vtable), G-4 (mandatory migration) and G-5 remain. It was 17,498 immediately before, measured on the same date by ticket #1958 (SR-AUD-196) — 17,498 run, 17,498 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1958 (SR-AUD-196). +1 on the 17,497 below, inSharpRuntimeTests_Threading(521 → 522; two message-taking cases replaced by three, and the integration case rewritten in place).ThreadStartExceptionpublished three constructors where .NET has two, and .NET has no message-taking one at all — both of its own pass the fixedSR.Arg_ThreadStartException(ThreadStartException.cs:11-24), soThreadStartException("anything")produced an exception .NET can never produce while still claimingCOR_E_THREADSTART. The port's own doc-comment already said the constructors are internal in .NET, so the documentation and the code disagreed. The surviving overload takes the reason alone, which is why it replaces the old(message, inner)pair rather than joining it; the class is nowfinal, matchingsealed. Landed under SA-8 with SA-2's five conditions. Five mutations, all caught, two at compile time. Fixture set 41 fixtures / 215 sites; site 3 is the string-literal spelling, the one most likely to survive a careless migration because a literal is not astd::string. The first-party count was wrong and the compiler corrected it: four sites, not two — a grep overmodules/andtest/missedtests/integration/, a separate tree. Downstream: zero sites in both consumers. The accessibility half is filed, not guessed — .NET'sinternalhas no C++ equivalent, and the two mechanical translations differ from each other and from .NET (a friendlessprivatemakes the type uninstantiable; afriend Threadwould be dead, because this port has no window in which to throw it), so #2390 asks it as a general rule. This closes #1958's seventh finding; only SR-AUD-209 remains. It was 17,497 immediately before, measured on the same date by ticket #1958 (SR-AUD-194) — 17,497 run, 17,497 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1958 (SR-AUD-194). +7 on the 17,490 below, inSharpRuntimeTests_Threading(514 → 521).Thread::Start(void*)captured its argument and then discarded it with a literal(void)parameter;while its own doc-comment said the value was "forwarded to the thread function" — there was no way to receive it and no diagnostic, because the only accepted callback shape had no parameter slot. The recorded cost estimate was wrong and is corrected by measurement: #1958 listed this as "a signature change", routing it through SA-10 and SA-2, and no existing signature changes — the repair is an additive second constructor (.NET'sThread(ParameterizedThreadStart),Thread.cs:152) plus an SA-5 behaviour change, so it landed as ordinary work with a pinned layout. The one spelling that would become ambiguous,Thread(nullptr), exists in zero places acrossmodules/,test/and both consumers. No shape flag was needed: exactly one of the two callables is ever set, and which one is .NET'sstartHelper._start is ThreadStarttest.sizeof(Thread)104 → 136; consumers rebuild. Two asymmetries are .NET's and both are pinned —Start()does not reject a parameterized thread (it passes null,Thread.cs:239-253), and the shape check applies only before the first start, because .NET wraps it inif (startHelper != null)and says so in a comment. That second pin was written asserting the opposite and failed, and the reference showed the TEST was wrong rather than the code — this port gets the same rule fromfn_being moved from on the first start, exactly asstartHelperis nulled. Five mutations, all caught; two only after repair — M1 was a SEGV because the test dereferenced a null the mutation supplies, and M3 was a SIGABRT because my mutation threw from a thread body, which is not a realistic regression. M2 is caught only as a crash and that is inherent: removing the guard hands an emptystd::functionto a new thread, reachingstd::terminatewith no handler — the #2215 limit. Downstream: zero sites in both consumers. #1958 now has two findings left, SR-AUD-209 and SR-AUD-196. It was 17,490 immediately before, measured on the same date by ticket #1958 (SR-AUD-220) — 17,490 run, 17,490 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1958 (SR-AUD-220). +8 on the 17,482 below, inSharpRuntimeTests_Threading(506 → 514).ThreadLocal'strackAllValueswas accepted and never read, and the type had noValuesproperty at all — two halves of one finding, inseparable because the flag is only observable through the property it gates, so a caller who asked for tracking got a silent no-op with no way to notice.Valuesis transcribed fromThreadLocal.cs:421-434, message included, and the tracking check precedes the disposed check, so a disposed untracked instance reportsInvalidOperationException. The lifetime question decided the design: .NET'sGetValuesAsListwalks the ThreadLocal's ownLinkedSlotlist, so a value survives its thread exiting — the registry therefore holds strong references and per-thread storage movedunique_ptr→shared_ptr, since aweak_ptrregistry would silently drop a dead thread's value. Co-ownership rather than copying is also what makes an update reflected rather than duplicated.sizeof(ThreadLocal<int>)56 → 128; consumers rebuild. Six mutations, all caught — two only after the tests meant to catch them were found vacuous, and both are recorded: the check-order mutation cannot be asserted withEXPECT_THROW, becauseObjectDisposedExceptionderives fromInvalidOperationException(the #2152 trap), so the derived type must be caught first; andDispose's registry release is unreachable through the public surface — afterDispose,Valuesthrows either way — so its only observable is when values are destroyed, tested by letting the owning thread exit and counting destructor calls. Downstream: zero sites in both consumers. #1958 stays open for SR-AUD-209 (vtable), SR-AUD-194 (signature) and SR-AUD-196. It was 17,482 immediately before, measured on the same date by ticket #2389 — 17,482 run, 17,482 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2389. +6 on the 17,476 below, inSharpRuntimeTests_Threading(500 → 506). #2389 completes what #1956 half-landed: .NET'sReaderWriterLockSlim.Disposeperforms two checks (ReaderWriterLockSlim.cs:1250-1258) and #1956's design record named only the held-mode one, soDisposeaccepted a lock other threads were waiting for.waitingReaders_andwaitingUpgraders_join SR-AUD-204'swaitingWriters_, and the two new counters feed no admission predicate — only writer-waiting does — so their guards takenotifyOnLast = falseand cannot perturb wake-up ordering.sizeof120 → 128: SR-AUD-204's single counter fit existing padding and these two did not, so consumers rebuild. Four mutations, all caught. Three process notes, and they are the substance of this ticket. (1) The first mutation run reported all four "caught" — by a pre-existing layout gate asserting 120, which was failing on the unmutated build too, so no verdict meant anything; a mutation verdict is only evidence against a baseline that passes. (2) Re-run green, M1 and M2 came back NOT CAUGHT — a real defect in my tests, which disposed from a thread that also held a mode, so the held-mode check fired and the waiter check was never exercised; the disposer must hold nothing. (3) The corrected tests then flaked inside the gate: a fixed 150 ms settle does not guarantee the waiter has been counted. They now rebuild the scenario over up to six attempts and pass the momentDisposerefuses — sound rather than tolerant, since only a genuinely firing check can pass it, and every mutation still fails all six. That layout gate doing its job is also the evidence 128 is a real change rather than drift. Downstream: zero sites in both consumers. It was 17,476 immediately before, measured on the same date by ticket #1956 — 17,476 run, 17,476 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1956. +9 on the 17,467 below, inSharpRuntimeTests_Threading(491 → 500). #1956 makes disposal a real state acrossSystem::Threading. Its recorded gate was "the repair turns previously-succeeding public calls into throws … needs approval question 1", and SA-5 grants exactly that, verbatim — "including where a call that succeeds today starts throwing" — granted 2026-08-17, two weeks after the design record was written. Four findings: the three wait handles'Close()were empty bodies, soClose()thenWaitOne(0)returned success while the headers already claimed Close "closes the handle" — documentation and behaviour disagreed;ThreadLocal::IsValueCreatedansweredfalsewhen disposed, indistinguishable from alive, no value yet;ReaderWriterLockSlim::Dispose()succeeded with a lock held. One member is deliberately excluded and that is pinned, not glossed:ITimer::Changemust returnfalse, not throw — .NET'sTimer.Changeopensif (_canceled) { return false; }(Timer.cs:539-542), so making it throw for symmetry would contradict the interface; mutation M6 is the one that pin exists to stop, since "make disposal consistent" would otherwise look like an improvement.MutexoverridesDispose()rather than shadowingClose(), which is .NET's own arrangement (Close() => Dispose()), so all three routes reach the guard. Every affectedsizeofis unchanged — the flags land in existing padding. Six mutations, all caught; M6 was invalid as first written (a missing include) and was reformulated rather than counted. One deliberate narrowing is recorded: .NET'sDisposeperforms two checks and this port implements the second — the waiters half needs per-mode waiter counts this port lacks — so the behaviour is a strict subset of .NET's, never refusing a disposal .NET would accept; filed as #2389, and the design record's own gap rather than an implementation shortcut. Downstream: zero sites in all six affected types, measured separately. It was 17,467 immediately before, measured on the same date by ticket #1957 (SR-AUD-204) — 17,467 run, 17,467 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1957 (SR-AUD-204). +5 on the 17,462 below, inSharpRuntimeTests_Threading(486 → 491), and it closes #1957's fourth and last member. SR-AUD-204 givesReaderWriterLockSlimwriter preference: the read predicate was!writerActive_alone — it asked whether a writer held the lock, never whether one was waiting — so a steady arrival of readers starved a blocked writer indefinitely. .NET packs the same signal into its_ownersword, and its own comment is the derivation: "Setting these bits will prevent new readers from getting in" (ReaderWriterLockSlim.cs:1005-1010) —WAITING_WRITERSandWAITING_UPGRADERboth sit aboveMAX_READER, so the single test_owners < MAX_READERrefuses a reader when a writer holds or awaits the lock. Both kinds count, so an upgrade-to-write blocks readers too — the easy half to miss, pinned separately — and a timed-out writer stops blocking, via an RAII guard mirroring .NET'sfinally, because that failure mode would be permanent rather than transient.sizeofis 120 before and after. Writer preference cannot deadlock a recursive reader: every thread already holding a read, write or upgrade lock returns before the predicate. Two mutations, both caught. Three honest results on the sanitizer, two of them limits: the full target cannot build under TSan —Thread::MemoryBarrier()isstd::atomic_thread_fence, which gcc rejects under-Werror=tsan, a pre-existing incompatibility (#2298's) reached from an unrelated file — so the evidence is a focused probe, clean over 1,020,489 read acquisitions; the probe was shown able to report (a deliberate unsynchronised counter yields 2 race warnings), per the plan's rule that a silent sanitizer is evidence about the probe; and the probe does not reproduce the starvation, reportingwrites=6089even pre-repair, because its readers do not overlap continuously — the deterministic gtest does that instead. Downstream: zero sites in both consumers. It was 17,462 immediately before, measured on the same date by ticket #1957 (SR-AUD-210) — 17,462 run, 17,462 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1957 (SR-AUD-210). +6 on the 17,456 below, inSharpRuntimeTests_Threading(480 → 486). #1957/SR-AUD-210 lets aBarrier's post-phase action read the phase number:FinishPhase()runs that action while holdingmutex_, andgetCurrentPhaseNumberProperty()took the same non-recursive mutex, so a legal call deadlocked. This is not a new discovery — #1955 fixed the sibling property exactly this way and its comment names this one as the remaining case. .NET'sCurrentPhaseNumberisVolatile.Read(ref _currentPhase)(Barrier.cs:184-188), a lock-free read, so the reference settles the design andphaseCount_becomes astd::atomic<longcs>— layout-neutral,sizeof(Barrier)160 before and after. The design record named only the deadlock, and the reference exposes a second half: .NET increments the phase inSetResetEvents, called fromFinishPhase'sfinallyafter the action and on the throwing path too, while this port incremented first — unobservable only because the property that would have seen it hung. Fixing the deadlock alone would have shipped a newly reachable wrong answer in place of a hang, so both land together and the action now reads the phase that is ending, as .NET does; nothing outside the action moves, sincemutex_is held throughout. Three mutations, all caught. M2 was invalid as first written — adding the increment without removing the later one is a double increment, not a move — and was reformulated rather than counted. M3 was first reported "not caught" and that was a defect in my mutation harness, found by checking rather than believing it: M3 hangs a pre-existing multi-participant case, so the whole-suite run hit its timeout and emitted noFAILEDlines at all, which the harness read as a pass — run alone, the new throwing-action pin fails in 0 ms. SR-AUD-204 remains and #1957 stays open for it (SR-AUD-202 landed as #2341, SR-AUD-201 earlier the same day). Downstream: zero sites in both consumers. It was 17,456 immediately before, measured on the same date by ticket #1957 (SR-AUD-201) — 17,456 run, 17,456 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1957 (SR-AUD-201). +5 on the 17,451 below, inSharpRuntimeTests_Threading(475 → 480). #1957/SR-AUD-201 makesPeriodicTimer::WaitForNextTicksingle-consumer: two concurrent waiters used to both returntruefor one tick (the audit measuredconcurrent=1,1), so a caller that accidentally shared a timer got twice the intended work rate with no diagnostic — the worst shape of concurrency bug, where the wrong answer is a plausible one. #1957's design predates SA-3 (2026-08-17), which answers its whole approval question, and the reference resolves its one recorded[unverified]flag: .NET carriesprivate bool _activeWaitand throws (PeriodicTimer.cs:192,199-203), commenting "Failing to do so is an error." The guard runs first and that is transcribed: .NET tests it before the cancellation short-circuit and the signalled fast path, so a second consumer is refused even on a disposed timer — it is the concurrent use that is the error. The flag clears on every exit via RAII, which is the mistake in the other direction: a flag never cleared makes the first wait lock the timer out for ever.sizeofis 128 before and after — the bool fits existing padding — so SA-3's pin records no growth at all. Three mutations, two caught; M3 is uncaught and cannot be caught deterministically, because moving the guard below the disposed check differs only in the race window betweenDispose()releasing the mutex and the parked consumer reacquiring it — a test for it would be flaky, and this session has twice repaired flaky tests rather than written one; the ordering is kept because it is .NET's and the site says so. SR-AUD-204 and SR-AUD-210 are untouched and #1957 stays open for them (SR-AUD-202 landed earlier as #2341). Downstream: zero sites in both consumers. It was 17,451 immediately before, measured on the same date by ticket #1981 — 17,451 run, 17,451 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1981. +7 on the 17,444 below, inSharpRuntimeTests_Runtime(174 → 181). #1981 stopsConditionalWeakTable's enumerator keeping every snapshotted value alive — for a type whose whole purpose is not to retain values, that is a defect of the primary contract. Measured before and after:table.Remove(key)left the value alive until the enumerator was deleted, and now releases it. The snapshot demotes each value to aweak_ptrandMoveNextlocks both halves, so onlyCurrentis retained and an entry released after the snapshot is skipped rather than yielded stale.Reset()becomes empty, transcribed fromConditionalWeakTable.cs:492— so a caller can no longer re-enumerate, and because the body really is empty it does not clearCurrenteither, which is the easy mistake in the other direction and is pinned separately. .NET's own design is deliberately not reproduced and the reason is recorded: its enumerator holds no snapshot at all but a borrowed reference to the table plus an index range, andGetEnumerator()here returns a raw pointer whose lifetime the table does not control — reproducing it would reintroduce CCF-019. The recorded cost was overstated:Enumeratoris a private nested class the table heap-allocates, so no consumer can name, size or hold one, and the table's ownsizeofis 72 before and after — SA-3's pin is on the type that can actually be sized. Three mutations, all caught; M3 only after a case was added, because no test enumerated after aRemove, which is the sole situation where the key is alive and the value is not — testing the key alone yields a pair with a null value and counts it as live. M1 was invalid as first written and was reformulated rather than counted. No existing test relied on the oldReset(), all 174 passing unchanged. Downstream: zero sites in both consumers. It was 17,444 immediately before, measured on the same date by ticket #2150 — 17,444 run, 17,444 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2150. +10 on the 17,434 below, inSharpRuntimeTests_IO_Compression(103 → 113; one absence pin inverted, ten cases added). #2150 givesDeflateStream,GZipStreamandZLibStream.NET's(Stream, ZLibCompressionOptions, bool)constructor. Its recorded gate was a classification, and this repository's own design record already contradicted it: the ticket said "public surface addition — approval required", whiledocs/SystemIOCompressionNamespaceReviewPlan.md§7 lists #2150 as signature new ctors, vtable —, layout —,noexcept—, result additive — the #1980-G-1 and #1997-A-1 shape, which is ordinary SA-5 work. The one substantive claim is false and is now asserted rather than argued: a new overload can only change an existing call if some argument binds to both, andCompressionModeis a scoped enum whileZLibCompressionOptionshas no converting constructor, so nothing converts to both — pinned, so the claim must be re-made if either fact changes. The format arithmetic went from three copies to one, not four: each encoder carried its own in an anonymous namespace and the streams had none, soDetail::ResolveWindowBitsis shared and the encoders now delegate to it — a net removal, proven behaviour-preserving by all 103 pre-existing cases passing unchanged. Two asymmetries are transcribed rather than smoothed: Deflate and GZip clamp the window log to 9 and ZLib deliberately does not (.NET's comment explains that classic zlib silently upgrades 8 to 9 while zlib-ng rejects it), and the sign and offset are the container. Six mutations, all caught — M3 only after a pin was added, because every end-to-end case used a window log of 9 or 15 and never exercised the one input that separates clamping from not clamping; asserting the resolver directly was necessary rather than merely convenient, since classic zlib upgrades 8 to 9 internally and no end-to-end test could discriminate it. M6 was invalid as first written (-Werroron an orphanedconstexpr) and was reformulated rather than counted. Downstream: zero sites in both consumers. It was 17,434 immediately before, measured on the same date by ticket #2388 — 17,434 run, 17,434 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2388. +4 on the 17,430 below, inSharpRuntimeTests_Threading_Tasks(222 → 226; one pin rewritten in place, four cases added). #2388 is #1969's other half, filed and closed the same day:ParallelOptions::MaxDegreeOfParallelismis private behind the rule-5 pair and validated in its setter, where .NET validates it (Parallel.cs:85-90). #1966 had already landed the same rule at the entry of everyParallelmethod, because a public data member has nowhere to put a check — and the difference is observable, not cosmetic: an invalid degree used to be stored and survive until a loop ran, so a caller that assigned and never looped got no diagnostic at all, and one that read the value back read a number .NET can never hold. The parameter name stays"MaxDegreeOfParallelism"and that is confirmed rather than assumed: .NET writesnameof(MaxDegreeOfParallelism)here andnameof(value)inBoundedChannelOptions— the reference is inconsistent between its two option types, both are transcribed as they are, and harmonising this onto #1969's"value"would be inventing a reference. One #1966 test moved rather than being left to pass for the wrong reason: it asserted the degree error beats the empty-body error, and with the guard in the setter there is no ordering left to assert — the throw now comes from the assignment, outside theEXPECT_THROW, which is the #2359 trap; it is replaced by the stronger property that an invalid degree cannot reachParallel::Forat all. Five mutations caught — M4 by the new pin and four pre-existing #1966 cases — and one equivalence recorded rather than counted: removing the now-unreachable use-site guard changes nothing, because the field is private, its only mutator validates, and the private member makesParallelOptionsa non-aggregate, so brace initialisation cannot reach it either; it is kept as defence in depth and the site says so. Fixture set 40 fixtures / 211 sites, site 4 being the aggregate-ness a consumer loses silently. Downstream zero sites in both consumers, measured separately from #1969's rather than assumed to match. It was 17,430 immediately before, measured on the same date by ticket #1969 — 17,430 run, 17,430 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1969. +6 on the 17,424 below, inSharpRuntimeTests_Threading_Channels(64 → 70). #1969's recorded blocker was an approval request, and SA-8 grants it verbatim — "making a public data member private and adding the rule-5 accessor pair" is the question this ticket asked on 2026-08-03, and SA-8 adds that the decision "is not to be re-litigated ticket by ticket".BoundedChannelOptions::FullModewas a bare public data member, and the shape was the whole obstacle: a data member has nowhere to put a check, sostatic_cast<BoundedChannelFullMode>(99)could be stored — and then the writer took the drop path (mode ≠Wait) and matched no arm of the drop switch, so nothing was dropped and Count reached 2 on a channel bounded at 1. The reference corrects the ticket twice: .NET validates two members, not one — and this port already hadCapacityright, private withThrowIfNegative(…, "value")at both doors, so nothing there needed changing; and the capacity bound is< 0, not< 1, so a zero-capacity channel is legal in both and the intuitive "at least one slot" repair would be a divergence. The three base flags are deliberately left public and the boundary is pinned: .NET'sSingleWriter/SingleReader/AllowSynchronousContinuationsare auto-properties with no validation, so a public field is observationally identical and SA-8 does not reach them — converting them would be a source break buying no behaviour, the exact inverse ofFullMode. Five mutations, all caught, and M3 only after a vacuous assertion was repaired: it searchedwhat()for"value", and the default message is "…out of the range of valid values.", which contains the substring whatever the parameter is named — it now assertsgetParamNameProperty(). M5 additionally hangs a pre-existing case, and a mutation caught only as a hang is not caught by name. Fixture set 39 fixtures / 207 sites; six first-party sites migrated; downstream zero sites in both consumers. #2388 is filed rather than bundled for the identical shape one module over, with the correction thatParallelOptions's parameter name is already right — .NET writesnameof(MaxDegreeOfParallelism)there andnameof(value)in Channels, and both are transcribed as they are. It was 17,424 immediately before, measured on the same date by ticket #2202 — 17,424 run, 17,424 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2202. +3 on the 17,421 below, inSharpRuntimeTests_Xml(515 → 518). #2202 changed no production statement: it is a declaration and its evidence, the shape of #2015 and #2324. This runtime writes a processing instruction correctly in any position and can read one back only before every other node — an XML declaration may precede it and nothing else may, not even a comment — so SR-AUD-349's closure property does not hold for this one node kind. The ticket's own acceptance criteria prescribe this outcome ("if the substrate cannot express it, the correct outcome is an explicit documented limitation … plus a pin, not silence"); the substrate had already been confirmed unable, and the documentation and the second pin were simply never written. The cause is measured, not asserted: called directly with no port code involved,vendor/tinyxml2produces exactly the port's five verdicts, and the leading<?p d?>comes back as a Declaration node valuedp d— tinyxml2 has no processing-instruction node type at all, so every<?inherits the rule that a declaration may appear only before anything else. .NET differs by exactly that model distinction:XmlLoader.cs:203-209switches onXmlDeclarationandProcessingInstructionas separate cases in the same general node loop, which runs for element content too. Two routes were refused — patchingvendor/(third-party, never edited) and pre-rewriting non-leading<?…?>around the substrate, which forks its semantics for every document; mutation M3 shows what that costs, a naive shim breaking the shippedXPathSelectTests.ProcessingInstructionNodeTest_SelectsPI. Three mutations, all caught, two of them also by pre-existing tests. Pinned at both layers as the criteria require, and the asymmetry pin is written to fail the moment the limitation lifts. It was 17,421 immediately before, measured on the same date by ticket #2059 — 17,421 run, 17,421 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #2059. +1 on the 17,420 below, inSharpRuntimeTests_Buffers(629 → 630). #2059's premise does not survive the reference, and the ticket is resolved by declining its own proposed repair. SR-AUD-088 is titled "MemoryHandle documents RAII cleanup but never unpins at scope exit" and #2059 existed to add~MemoryHandle(){ Dispose(); }— but .NET's ispublic unsafe struct MemoryHandle : IDisposable(MemoryHandle.cs:12), a struct with no finalizer, so scope exit does not unpin there either;using var h = memory.Pin()is a language construct, not something the type does. What the finding actually found was a doc-comment promising what the type never did, and that promise had already been removed — so adding the destructor would be a divergence, not a repair, and the ticket's own copy hazard (one unpin per copy for a single pin) is the second, independent reason.Dispose()needed no change either: it already matchesMemoryHandle.cs:41-53statement for statement, idempotence included. What did land is the divergence the ticket never named: both data members were public here and areprivatein .NET, which publishes exactly one of them —Pointer, as a getter — so the port let a caller retarget a live handle, or detach itsIPinnableand make the subsequentDispose()a silent no-op that leaks the pin. Landed under SA-8 with SA-2's five conditions discharged; zero first-party sites needed migrating, measured across all ofmodules/andtest/. It is an access change and not a layout change —sizeofis 24 before and after, so no consumer rebuilds — the same shape as #2332'sSequencePosition. .NET's third field,GCHandle _handle, stays deliberately absent: no moving collector, no handle to free. Five mutations, all caught, two of them at compile time through absence pins that use a dependent parameter (the #2299 gcc trap). Fixture set grows to 38 fixtures / 204 sites, its third site being the detach that breaks silently rather than loudly. Downstream: zero sites in both consumers. It was 17,420 immediately before, measured on the same date by ticket #1958 (SR-AUD-193) — 17,420 run, 17,420 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1958 (SR-AUD-193). +4 on the 17,416 below, inSharpRuntimeTests_Threading(471 → 475). #1958 gives every thread a distinctManagedThreadId:Thread::CurrentThread().getManagedThreadIdProperty()returned 1 from every thread not created through aSystem::Threading::Thread, so the main thread, every rawstd::threadand every pool worker all reported the same number — erasing the uniqueness .NET's contract states unconditionally, and silently breaking anything keyed on it. The naive repair is wrong and intermittently so: handing1to whoever asks first gives it to a worker whenever one asks beforemaindoes, so the main thread keeps1by identity, from an OS id captured during static initialisation — which runs on it. One counter, not two, and that is the load-bearing choice: uniqueness is across all threads, not within each kind, so external threads draw from the same counter the wrapper's constructor uses. The obvious test for that does not work and the honest record is the point: collecting four ids of each kind and asserting eight distinct values passes against a second-counter mutation, because two counters only collide where their ranges overlap and a fresh one sits far below the shared one — the assertion that discriminates is ordering, an external id taken after aThreadobject's id must exceed it. Four mutations, all caught, the fourth only after that rewrite. The id is assigned on first use, so a thread that never asks costs nothing. No signature, layout, vtable ornoexceptchange — the id lives in athread_local, so no consumer rebuilds. #1958's own description is stale and is corrected rather than copied: SR-AUD-214 and SR-AUD-189 were landed by #1971 on 2026-08-03, and the ticket stays blocked only for members needing approvals this one did not (SR-AUD-209 a vtable change, SR-AUD-194 a signature change, SR-AUD-196 removing public constructors, SR-AUD-220 storage on a public template). Downstream: zero sites in both consumers. It was 17,416 immediately before, measured on the same date by ticket #1996 — 17,416 run, 17,416 passed, 0 failed, 0 skipped, measured on 2026-08-19 by ticket #1996 (groups G-1 and G-2 only). +4 on the 17,412 below, inSharpRuntimeTests_Uri(283 → 287; two pins updated, four cases added). #1996 names G-1 + G-2 as the recommended minimum and both are alignments to the reference, so SA-5 covers them:setHostProperty("::1")now rendershttp://[::1]/instead of the unparseablehttp://::1/, andsetSchemeProperty("HTTP")lower-cases. G-3 — which #1996 calls “the only narrowing” — and G-4 are not taken and are pinned absent. One deliberate divergence is forced by that split and is recorded at the site: .NET’s host setter wraps first and then throws, so"[::1"becomes"[[::1]"and is refused; without the rejection the wrap would store that nonsense and turn a value #1991 already refuses into one it might not — so a value already carrying a bracket is left exactly as given.h:abc→[h:abc]is not a divergence but .NET’s own “probable ipv6 address” rule, and it made two existing pins’ unparseable examples parseable, so their subject moved to"[::1"and the empty host. Five mutations, four caught; the uncaught one is an equivalence in the “C” locale —std::toloweragrees with the explicit ASCII fold on every byte there, and distinguishing them needs the global locale changed inside a shared binary, which #2174 considered and declined. Downstream: zero sites. It was 17,412 immediately before, measured on the same date by ticket #1997. +4 on the 17,408 below, inSharpRuntimeTests_Uri(279 → 283). #1997’s acceptance criterion calls A-1 “strictly additive and touches no existing declaration”, soUri::GetLeftPart(UriPartial)lands under SA-5 —System::UriPartialhad documented it since it was ported and the member did not exist. Two details are only visible in the reference: the scheme delimiter is a substring of the source, not a rule —GetParts(Scheme | KeepDelimiter)is_string.Substring(Offset.Scheme, Offset.User − Offset.Scheme)— somailto:keeps its bare colon; andAuthorityis the empty string when there is none, where .NET’s comment three lines abovereturn string.Empty;says the opposite of it, and the code is what runs. A-2’s cost estimate was wrong and is corrected rather than paid: the same criterion callsCheckHostName“strictly additive”, and it classifies throughIPv6AddressHelper/IPv4AddressHelper, whichmodules/urihas not — it depends onCore.Basealone and IPv6 content validation is a declared out-of-scope boundary there. A first cut wrote it againstIPAddressand the module graph rejected it, so the cost is measured rather than guessed; A-2 stays with #1997 and the reason sits inUri.hpp. Six mutations, all caught. Module graph unchanged at 41/93. Downstream: zero sites. It was 17,408 immediately before, measured on the same date by ticket #1980. +4 on the 17,404 below, inSharpRuntimeTests_Runtime(170 → 174). #1980’s own acceptance criterion calls G-1 “purely ADDITIVE … cannot break a consumer”, which makes it ordinary SA-5 work rather than an approval; G-2..G-5 are untouched and the ticket stays blocked for them.OSPlatformgains the default .NET’sreadonly structhas always had,RuntimeInformationgainsRuntimeIdentifier, andExternalExceptiongains(message, errorCode)andErrorCode. Two premise corrections:ErrorCodeneeds no data member — .NET’s isErrorCode => HResult, an alias, sosizeofis unchanged; and the header’s blanket note thatRuntimeIdentifier/FrameworkDescriptioncannot be described is right for one of them —FrameworkDescriptionis a build-generated .NET product version, whileRuntimeIdentifierisAppContext.GetData(...) as string ?? "unknown", reproduced exactly,as stringbeing a type test rather than a coercion.ToString()was implemented and then removed by the downstream measurement:cnaderives fromExternalExceptionin three types, so the statically resolved name #2323 used would have misnamed every one — #2323’s own rule, that naming the wrong type is a lie where an absence is not. Split out as #2387 (needs_user), because a virtual is a vtable change and a stored name breaks those three. Five mutations, all caught; two were invalid as first written, one of them caught by compilation, which is the only way C++ reports a missing constructor. It was 17,404 immediately before, measured on the same date by ticket #1983. +2 on the 17,402 below, inSharpRuntimeTests_Runtime(168 → 170). #1983 gives WindowsOSArchitecture.NET’s two-step probe —IsWow64Process2resolved at run time, elseGetNativeSystemInfo— so a WOW64 process stops reporting X86 as the operating system’s architecture, and makes an unrecognised compilation target a build error. Two of the ticket’s three “absences, ALL of which must be resolved” are gone: a MinGW-w64 cross-compiler is installed and/rvis present; only the mixed-bitness Windows host remains, and that gates observation rather than implementation — #2378’s position exactly. The fabricatedX64fallback is replaced by#error, which is all .NET offers, because the property states the compilation target and an unknown target means the build is wrong. Two mapping tables, not one:IMAGE_FILE_MACHINE_*andPROCESSOR_ARCHITECTURE_*are different enumerations with different defaults, and both asymmetries are transcribed. Four mutation attempts, two valid and both caught — symbol inspection shows the Windows imports drop 2 → 0, and a lifted preprocessor chain shows the baseline rejected where the mutation compiles. One mutation is uncaught and cannot be caught here: changing a mapping arm alters no symbol, so its behaviour is unverifiable without a Windows host — which is the third absence, restated rather than waved away. Downstream: zero code sites. It was 17,402 immediately before, measured on the same date by ticket #2194. +4 on the 17,398 below, inSharpRuntimeTests_Net_NetworkInformation(63 → 67). #2194 makesPingcorrelate its reply to its request and report a refused socket option instead of discarding the result. Its blocker was environmental and is gone —ping_group_rangewas1 0and is now0 2147483647, so a real round trip runs. The acceptance criterion is wrong on two of its three fields, both measured: the identifier cannot be matched, because a Linux ping socket has the kernel rewrite it (probed,0x1234out and0x94d4back) — .NET checks it only because its raw path writes an id the kernel leaves alone; and the source must not be matched, because an ICMP error legitimately comes from an intermediate router, which is what a lowTtlasks for, and .NET does not check it either. The sequence survives and is what correlates. Four mutations, two caught — and the two that were not are the informative ones. Computing the correlation and ignoring it changes nothing here, which exposed a vacuous test of mine: a ping socket is demultiplexed by the kernel-assigned id, so a foreign datagram never reaches our queue. The check is therefore defensive on this platform and is kept because #1962 would add a raw socket, which receives every ICMP datagram on the host. The restarted-deadline mutation would hang rather than fail, so no non-flaky test can catch it.Ttl = 256is the reachable option door:PingOptionsrejects onlyttl <= 0, which is .NET’s own bound. Downstream: zero code sites. It was 17,398 immediately before, measured on the same date by ticket #2042. +7 on the 17,391 below, inSharpRuntimeTests_Net(333 → 340; two pins inverted, nine cases in their place). #2042 boundsCookieContainer, which was unbounded in every direction — 10,000 cookies from one origin were all retained, and an expired cookie was kept for ever and merely hidden from emission. Its gate was “every bound is a number somebody must choose” AND “.NET’s exact default capacities cannot be established here”, which are the same claim and it is false:CookieContainer.cs:69-71defines 300, 20 and 4096. The per-domain limit binds at 19, not 20, and that is .NET’s arithmetic — aging must free a slot for the cookie being added.MaxCookieSizeis the one limit that reports rather than evicting, and it bounds the Value alone; a capacity that cannot be satisfied silently rejects, as .NET does. One structural difference is stated: .NET evicts from the least-recently-used path collection, and this flat list has no collections to time-stamp, so it drops the oldest stored cookie — within a collection .NET does the same thing. No extra state was needed: insertion order is vector order. Six mutations, all caught, three only after the case meant to catch them was fixed — the domain must be at its limit for the ordering to be observable, and the expired cookie must not also be the oldest or plain eviction removes it anyway. A bug in one new test (iterators into two different temporaries) hung the suite once and is named in the note. Downstream: zero code sites, and all six cross-module consumer suites green. It was 17,391 immediately before, measured on the same date by ticket #2031. +3 on the 17,388 below, inSharpRuntimeTests_Diagnostics(229 → 232; one pin inverted, four cases in its place). #2031 makesKill(entireProcessTree)walk the tree instead ofkillpg-ing one process group, which asetsid()descendant had left. The reference corrects the ticket’s own proposed design three times:SIGSTOPcomes before the children are enumerated — .NET’s comment says why, “so it won’t start additional children” — where option A enumerated then killed and so reintroduced the defect one level down; there is a self-guard refusing a tree that contains the caller; and failures are collected whileESRCHis ignored. The guard’s placement is load-bearing: it runs before the current-process no-op because that is where .NET has it, and a first cut that put it after left it unreachable through any ordinaryProcessobject. Six mutations, five caught. The “no recursion” mutation went uncaught at first for an instructive reason —setsid()changes the session, not the parent, so the original pin’s grandchild is an immediate child and a one-level walk kills it; a three-deep case was added. TheSIGSTOPmutation is uncaught and cannot be caught deterministically: it removes a race window, and the only test that would see it is a flaky one, which this session has twice repaired rather than written.Kill(false)is deliberately untouched. Downstream: zero code sites. It was 17,388 immediately before, measured on the same date by ticket #2117. +2 on the 17,386 below, inSharpRuntimeTests_Text_Json(298 → 300; two pins inverted, four cases in their place). #2117 makes aJsonElementcaptured beforeJsonDocument::Dispose()raiseObjectDisposedExceptioninstead of answering. Its gate was SA-3 all along — the ticket said it is “gated exactly asmodules/io’s #2098 is”, and #2098 landed under SA-3 the day before. The design is .NET’s rather than a workaround: .NET’sJsonElementholds_parentplus_idxand every accessor delegates throughCheckNotDisposed(), so the flag lives with the document; this port’s element now points at a sharedJsonDocumentStateand carries the node as a raw pointer into it.sizeof(JsonElement)48 → 56, pinned by shadow structs asserting a difference of exactly one pointer; consumers rebuild. The separatedisposed_bool is deleted rather than mirrored — two flags for one fact is what let a document and its elements disagree — and deriving a child no longer builds an aliasingshared_ptrat all. Two boundaries are .NET’s and both are pinned: a default element staysUndefined(CheckValidInstanceraisesInvalidOperationException, a different exception fromCheckNotDisposed), and aClone()taken beforehand survives, because .NET’sCloneroots the copy in a new document.ValueKindthrows too, which is easy to omit. Five mutations, all caught. Downstream: zero code sites. It was 17,386 immediately before, measured on the same date by ticket #2003. +1 on the 17,385 below, inSharpRuntimeTests_Uri(278 → 279). #2003 changed no production statement: it asked for approval to makeUrireject an embedded NUL, “accepting that the .NET behaviour it matches could not be re-measured in this environment” — and with the reference present it measures the other way, so the approval must not be sought.UriHelper.s_notSafeForUnescapeCharslists U+0000–U+001F explicitly,Uri.TryCreateThishas no whole-string character precheck at all, and a control character in the path is percent-escaped rather than refused. This port already matches, and the one difference — raw rather than%00— is the declared no-percent-encoding boundary, not a second divergence. Two premise corrections: the title’s “every component” stopped being true when #2359 gave the host .NET’s DNS character set four days later; and “no truncation” had been asserted of the string but never of the accessors, which a first probe appeared to contradict only because%sstops at a NUL — every row now asserts a length. The decision is enforced rather than written down: truncating the input is caught by four tests and the repair the ticket proposed is caught by three, both built and run before being reverted. It was 17,385 immediately before, measured on the same date by ticket #2086. +3 on the 17,382 below, inSharpRuntimeTests_Xml(512 → 515; one pin inverted, four cases in its place). #2086 makesRemoveAlldispatch the node-change pair per child and leaves those children alive. The ticket recorded two candidates with neither selected and (b) marked “NOT compatible without approval”; the reference selects (b) —XmlNode.RemoveAllis literally a loop overRemoveChild— so it is derived, not chosen. The lifetime hazard #2079 refused to introduce does not arise on that route: this port’sRemoveChildalready detaches throughDetachNode, so the wrapper a handler receives names a live object. The sibling is captured before the removal, as .NET does, because detaching re-parents the child and itsNextSibling()then walks the holder’s list. ASan discharges the acceptance criterion and is shown to discriminate: clean across all 29 cases on the repair, andheap-use-after-freeat the handler’s own dereference on the destroy-then-raise shape. Four mutations, all caught; a fifth was a no-op rather than a mutation — guarding the loop onchild == FirstChild()is always true, since removing the head promotes the next — and was reformulated rather than counted. The cost is stated: detached children live until the document does, which is not a new policy but is newly reached by theInnerText/InnerXmlsetters. Downstream: zero code sites. It was 17,382 immediately before, measured on the same date by ticket #2032. +2 on the 17,380 below, inSharpRuntimeTests_Diagnostics(227 → 229; three pins inverted, two cases added). #2032 makesWaitForExit(milliseconds)honour its own bound — it returned true after 29,951 ms against a declared 5,000, because reaping the child joined the pipe readers and a reader cannot return until every holder of the write end closes it, an inherited grandchild included. Its blocker was #2029, and the choice #2029 gated was a false trichotomy: the ticket said the repair must join, detach or abandon the reader, and the reference says reaping the CHILD and waiting for its OUTPUT are two different things —WaitForExitCorewaits for_output.EOFonly whenmilliseconds == Timeout.Infinite, commented “if we have a hard timeout, we cannot wait for the streams”. So the join moves rather than going away, and four doors are repaired by deleting one statement because all of them reached it throughreapIfNeeded. The restartStart()keeps its join and stays pinned, because assigning to a joinablestd::threadcallsstd::terminate— required by the language, not chosen as policy. Four mutations, all caught, and two of them only after work that is recorded: joining after an expired deadline sat on a statement no case reached, sinceWaitForExit(ms)returns early when the child has already exited; and the case written for the unbounded overload does not discriminate its mutation — seven pre-existing tests do, which is the evidence that join is load-bearing. Downstream: zero code sites. It was 17,380 immediately before, measured on the same date by ticket #2046. +1 on the 17,379 below, inSharpRuntimeTests_Net(332 → 333; five tests rewritten, one deleted as superseded, four added). #2046 makesDnsapply the requestedAddressFamilyto a resolved name, which it never did —GetHostAddresses("localhost", Unix)returned127.0.0.1while the same request against the literal was refused. The ticket asks “which way should the two agree?” and the reference says they do not:Dns.cs:213returns an empty array for a mismatched literal, the name path returnsEAI_FAMILY→AddressFamilyNotSupported, andGetHostEntrydoes not short-circuit a literal at all. Three answers, all cited, so no decision was needed. It reverses #2039 deliberately, and #2039 recorded the reason it might be wrong: it choseHostNotFoundover an empty vector because an empty vector is “indistinguishable from ‘checked and found nothing’”, and noted the plan had predicted empty — that prediction was right and its “correction” was the error, made with the reference absent, which is the condition SA-5 exists to end.GetHostEntrykeeps throwing because it returns one entry, not a list, so the empty answer is not available to it. One residual difference is pinned rather than glossed: a mismatched IP family on a literal keepsHostNotFound, because .NET would reverse- then forward-resolve and could find an address this port never looks for.EAI_FAMILYwas measured directly here —AF_UNIX,AF_PACKETand99all return −6. Five mutations, all caught. Downstream: zero code sites. It was 17,379 immediately before, measured on the same date by ticket #2045. ±0 on the #2385 reading, and deliberately so: #2045 rewrote its pin in place, one case replacing one. #2045 makesIPEndPoint::TryParsereject a trailing:with no port —"1.2.3.4:"used to parse as1.2.3.4:0, a port the caller never wrote. Its recorded gate was “the .NET reference tree is absent here so the intended behaviour cannot be established”, and the reference is present:IPEndPoint.cs:99-152locates the port field structurally and runsuint.TryParse— notint.TryParse, the ticket’s one wrong detail — over the remainder underNumberStyles.None, which rejects an empty span. The guard is a flag rather than!portPart.empty()and that is the whole design: “no colon at all” and “a colon with nothing after it” both leave the port text empty and are not the same input, so a mutation reverting it is caught. Five mutations, four caught; the uncaught one is proven equivalent —std::stoul("")throws and the existingcatch (...)already rejects — and the line is kept because .NET’suint.TryParsereturns false rather than throwing. One mutation was invalid as first written (-Werroron an unused variable) and was reformulated rather than counted. Downstream: zero code sites. It was 17,379 immediately before, measured on the same date by ticket #2385. +3 on the 17,376 below, inSharpRuntimeTests_Core_Base(6,064 → 6,067; one pin’s two rows inverted, three cases added). #2385 makes the four(string, index)overloads read a code point, combining a surrogate pair asCharUnicodeInfo.GetCodePointdoes. It was exposed by #2315 and #2336 rather than introduced by them: while every non-ASCII code point answeredOtherNotAssignedand every one answered −1, a supplementary character and a lone surrogate were indistinguishable, so the divergence could not be observed — with the tables in, 80 supplementary code points carry a decimal digit value and 1,177 a numeric value. Three conditions, and the easy mistake is the third: a low surrogate at the index is never combined, including the low half of a valid pair, because .NET only ever looks forward. Five mutations, four caught and one equivalence. The fourth was caught only after a row was added, and the reason is recorded: with a low surrogate accepted at the index the only successor that would combine is another low one, a shape no existing case had. The equivalence is proven rather than assumed —CheckIndexgivesi + 1 <= size()andoperator[](size())is specified to return a null character, so the dropped bound is defined behaviour and the check is kept for being .NET’s rather than for being load-bearing. Downstream: zero sites. It was 17,376 immediately before, measured on the same date by ticket #2386. +1 on the 17,375 below, inSharpRuntimeTests_Core_Base(6,063 → 6,064; one #2337 pin inverted, one case added). #2386 makes an undefinedNormalizationFormraiseArgumentExceptionwith .NET’s verbatim text andnameof(normalizationForm), becauseCheckNormalizationFormruns before the invariant shortcut and so holds in every mode. The four values are enumerated rather than range-checked, and that is load-bearing: 3 and 4 are holes (FormC=1,FormD=2,FormKC=5,FormKD=6), so a bounds check accepts two undefined values. The larger half is a premise correction that closes a question rather than opening one: #2337 pinned three behaviours as “statements about the CURRENT stub” that “must be inverted by the repair”, andNormalization.cs:11-40returnstrueand the argument unchanged in invariant globalization mode, saying so in a comment — so this port was never diverging, it was matching .NET under its own conditions, and the header now declares that with the citation. SA-4 does not reach #2338 and the measurement says why:CharUnicodeInfoData.cscontains zero normalization data, because .NET has no such tables and dispatches to ICU or NLS — so #2338 isneeds_userwith three recorded options rather than blocked on an approval that was already granted. Five mutations, all caught; the identity mutation is caught by three pre-existing pins. It was 17,375 immediately before, measured on the same date by ticket #2018. ±0 on the #2336 reading, and deliberately so: #2018 rewrote the two gatedRunepins rather than adding to them, two replacing two. #2018 is SA-4’s third unlock —Rune’s six classification and casing members answer from the UCD 16.0 tables, ending the self-contradiction SR-AUD-294 rests on (IsWhiteSpacewas Unicode-aware while its siblings were ASCII-only). The case tables share the category trie — .NET indexes all three with one offset function — and are 16-bit signed deltas, so the plane is preserved by masking rather than added to, a delta being unable to cross one. The round trip is not a bijection, and the exact set is asserted rather than a count — five uppercase BMP scalars do not return to themselves, and the count would have hidden that four of the five are Unicode’s own duplicate letters (U+03F4,U+2126,U+212A,U+212B), which lowercase into the canonical letter whose uppercase is the canonical capital; the fifth isU+1E9ESHARP S. A repair that “fixed” it would be disagreeing with the UCD. Five mutations, four caught; the uncaught one is a proven equivalence — the ASCII fast path agrees with the table on all 128 ASCII code points for all six members, and is kept because .NET has two statements there and collapsing them would simplify the reference rather than port it. Downstream: zero sites. It was 17,375 immediately before, measured on the same date by ticket #2336. +4 on the 17,371 below, inSharpRuntimeTests_Core_Base(6,059 → 6,063). #2336 is SA-4’s second unlock:GetDecimalDigitValue,GetDigitValueandGetNumericValueread a generated UCD 16.0 numeric table, so the sixteen-code-point reduction is gone and the BMP figures go 10/13/16 → 370/465/742. Decimal and Digit are two properties, not one built on the other — .NET reads a different nibble of the same byte, and the old “decimal value, else three hard-coded superscripts” agreed only over the thirteen it covered. What did NOT change is the load-bearing half: a CJK ideograph still answers −1, because Python answers 5.0 from Unihan, whichUnicodeData.txtfield 8 does not carry and .NET therefore does not either — measured, all 83 such code points are CJK, and there is no code point where the table and Python both claim a value and disagree. The widening creates two sentinel traps and both are asserted: U+0F33 really is −0.5 (the BMP’s only negative), sovalue < 0is now wrong, and U+2189 is exactly 0.0, sovalue != 0is wrong the other way. Five mutations, all caught — one by five pre-existingChartests, which is what shows the table is load-bearing for the shipped API. A gap was exposed and deliberately not bundled: the four(string, index)overloads index a code unit where .NET reads a code point, invisible until now because every non-ASCII answer used to be −1 — filed as #2385. Downstream: zero sites. It was 17,371 immediately before, measured on the same date by ticket #2315. +5 on the 17,366 below, inSharpRuntimeTests_Core_Base(6,054 → 6,059; six #2316 pins inverted, five cases added). #2315 givesCharUnicodeInfo::GetUnicodeCategorya generated UCD 16.0 table, so the declared reduction is gone and 292,420 of the 1,114,112 code points changed answer. The gate was already lifted and the ticket did not know it: its recorded blocker was “Approval F”, and SA-4 is Approval F, granted two days earlier — so #2336, #2018 and #2338 were unblocked with it, in SA-4’s stated order. The ASCII ladder is deleted, not kept as a fast path: two sources of truth for 128 code points, and it was wrong on 17 of them — exactly the punctuation/symbol split #2316 had documented as part of the reduction. The census is decomposed so it is external evidence rather than the table agreeing with itself: subtracting private use, surrogates and controls leaves 154,998, which is Unicode 16.0’s own published character count. SA-4’s cross-check is discharged and the answer is clean — against Python 15.1.0 and Perl 15.0.0, exactly one genuine reclassification, the same one in both (U+1171E,Mn→Mc); every other difference is a later assignment, and the 627-code-point gap between the two counts is Unicode 15.1’s own additions, so the corpora corroborate each other. A first Perl parse read inversion lists as ranges and reported 0 disagreements — what a silently wrong oracle looks like. Five mutations on the trie arithmetic, all caught. One inverted pin was kept: U+D7FF really is unassigned, so #2316 was right for a reason it could not have known, and the first cut’s “update” was the error. Generator and table both committed with a--checkmode. Downstream: zero sites. It was 17,366 immediately before, measured on the same date by ticket #2382. +7 on the 17,359 below, inSharpRuntimeTests_Core_Base(6,047 → 6,054). #2382 bringsBFloat16to the line this port already decided forSystem::Half— sixteen members — and gives itHalf’s@note Statusblock, since the omissions being undeclared is what let SR-AUD-176 be raised as a defect. “Half’s line” is not “Half’s bodies”, and for one member that distinction is load-bearing: .NET’sBFloat16delegates its identity trio tofloat, andHalf::GetHashCodemasks to 16 bits, so copying it would have compiled, satisfied the hash contract and returned the wrong number. A defect the ticket did not name was found by a mutation:ToString()was a barestd::to_chars, a second formatter besideSingle::ToString, and exhaustively over all 65,536 patterns 256 disagreed — the two infinities and 254 NaNs, C’s"inf"/"nan"against .NET’s"Infinity"/"NaN"— so the type was inconsistent with itself,ToString("F2")having been right all along. It surfaced because an uncaught mutation was investigated instead of excused: dropping the empty-format branch is only uncatchable if the two formatters agree. Eleven mutations, nine caught, two equivalences deleted rather than defended. The declined surface is pinnedstatic_assert-absent with a dependent parameter, the #2299 gcc trap having been hit again. Downstream: zero sites. It was 17,359 immediately before, measured on the same date by ticket #2363. +5 on the 17,354 below, inSharpRuntimeTests_Net_Sockets(127 → 132; three #2138 pins inverted, eight cases in their place). #2363 givesTcpClient,TcpListenerandUdpClientIPv6, so #2138’s refusal is removed rather than left unreachable. The ticket’s premise is corrected three times. .NET does not merely resolve withAF_UNSPEC—Socket.Connect(string, int)callsIPAddress.TryParsefirst (Socket.cs:919-923), which settles the question #2138 deferred and settles it away from #2359: a URI authority has bracket syntax and a socket hostname parameter does not, and .NET answers the socket question in the socket code. And not everyAF_INETconstant should have gone —UdpClient()andUdpClient(port)areInterNetworkin .NET (UDPClient.cs:24,47), unlikeTcpClient(), so a mutation that “finishes the job” by making them dual-stack is caught. #2138’s own measurement had one gap and this ticket found it: it concluded nothing was ever silently narrowed, which held for every path it probed and not forUdpClient::Receive, which read an IPv6 sender through asockaddr_inand handed back a fabricated IPv4 address. The layout claim was wrong in the first cut and the pin caught it: anAddressFamilymember growsTcpClient24 → 32, so the family is stored as theboolthis port’s ownIPAddressuses — everysizeofunchanged, no consumer rebuild. Nine mutations, eight caught; the uncaught one (dropping the IPv6 scope id) is unobservable without a link-local interface, which SA-6 makes a test defect rather than evidence. Three cases were restructured mid-pass because a thread parked inAcceptTcpClient()turns a failed connect into a hang, and a mutation caught only as a timeout is not caught by name. Downstream: zero code sites in either consumer. It was 17,354 immediately before, measured on the same date by ticket #1939. +6 on the 17,348 below, inSharpRuntimeTests_Core_Base(6,041 → 6,047). #1939 lands #1929 row 4A:DateOnlyandTimeOnlygain invariant single-formatParseExact/TryParseExact, purely additively — no existing result, message or failure moved. The digit-width rule is .NET’s, not the specifier count:ParseDigits(str, 1)reads one or two digits whileParseDigits(str, n>1)reads exactlyn, soyyyy-M-daccepts both paddings andyyyy-MM-ddaccepts only one — a scanner that counts specifiers is wrong in both directions, and that mutation is caught. The approval’s own worked example is stale and is corrected rather than copied: it asserts the unpadded text still fails generalParse, which #1929 made false the day before, so the test asserts the separation with text the general parser really does reject.:and/are matched as literals, because rejecting the characters would fail the approval’s own example on the wrong token — what is out of scope is a provider’s power to change them, which is 4B. One mutation is not caught and is an equivalence: no invariant month or day name is a prefix of another, so longest-first name matching is defensive rather than load-bearing, and the comment at the site says so. It was 17,348 immediately before, measured on the same date by ticket #1941. +5 on the 17,343 below, inSharpRuntimeTests_Core_Base(6,036 → 6,041). #1941 lands phase 1 only of #1929 row 4D:DateTimestores and reports aDateTimeKind, and nothing converts by it —ToLocalTime,ToUniversalTimeand offset/Zparse conversion stay absent and are pinned absent, so phase 2 is a deliberate act rather than a drift. The layout does not move:MaxTicksneeds 62 bits, so the kind packs into the two that are free, exactly as .NET does —sizeof(DateTime)stays 16 andDateTimeOffset48, so no consumer rebuilds. The member is renamed, and that is the safety property:ticks_becamedateData_behind a maskingticks(), because a bareticks_would silently have changed meaning and all thirty reads inside the type would have had to be remembered — renaming makes the compiler visit each one. Nothing that existed changed its answer: .NET'soperator ==shifts the flag bits away (DateTime.cs:1862) andGetHashCodeusesTicks, so comparison, equality, hashing, arithmetic and formatting are unmoved, each asserted. The reserved fourth encoding is transcribed and its mutation is honestly uncaught:LocalAmbiguousDstis unreachable in phase 1 because the constructor refuses kind 3, and the fold is kept anyway so phase 2 need not change a shipped accessor. Downstream is an opportunity, not a break:cna's XNBDateTimeReaderrecords a documented deviation — it masks the kind out and discards it, explicitly because this gap existed — and that gap is now closed (#2381). Five mutations, four caught, one uncaught for a stated reason; the range-check mutation was caught only after a row was added for it. It was 17,343 immediately before, measured on the same date by ticket #2348. +4 on the 17,339 below:SharpRuntimeTests_Xml509 → 512 andSharpRuntimeTests_Xml_Linq334 → 335. #2348 rejects a DOCTYPE internal subset that closes its own declaration — a subset of]><evil/><!--emitted<!DOCTYPE r []><evil/><!--]>, which is malformed and injects an element. The ticket's premise was corrected twice, in opposite directions: #2084 had already corrected the finding's claim that]is the terminator (it is>), and the reference now corrects the ticket's own implication that .NET does better —XmlWellFormedWriter.WriteDocTypevalidates the subset withXmlCharType.IsOnlyCharDataalone, a character check this port also performs since #2349, and then writes it withRawText, so .NET emits the same malformed document. This is therefore a deliberate narrowing past the reference, and it is the rule #2084 already applied in this same function to theExternalIDliterals, where .NET likewise checks only characters. The rule is not "reject any>" — #2084 considered and rejected that, because<!ENTITY a "b">is legitimate and only this runtime's>-terminated DOCTYPE node mis-reads it, a reader limitation a writer must not be narrowed to fit. It is a]followed by>, and it is quote-aware, so<!ENTITY a "]>">still passes. Four mutations caught, the most useful being "make it any>", which is caught by pre-existing tests — the evidence that the narrowing stopped where #2084 said it must. It was 17,339 immediately before, measured on the same date by ticket #2355. +3 on the 17,336 below, inSharpRuntimeTests_Text(314 → 317). #2355 widens the encoder fallback from acharto achar32_t, so a custom fallback can see which scalar was unencodable — the narrowing wasstatic_cast<char>(scalar & 0x7F), soU+1F600arrived as the byte0x00andU+00E9as the letteri. The repair is not "add .NET's second overload": .NET's parameter is acharwith a second overload taking a surrogate pair, and it reassembles that pair into one integer before formatting its message (EncoderExceptionFallback.cs:53-59) — which is itself the evidence that the scalar is the value a caller wants. This port has no pair to reassemble, so reproducing the overload would reproduce a limitation it does not have, the same argument #2299 made aboutFunc<void>. A behaviour repair came with it:ASCII()->GetBytes("\U0001F600")produced two?and now produces one, because .NET's replacement fallback runs once for a pair (EncoderReplacementFallback.cs:117-138) — the doubling existed only to mimic the pair through a narrow parameter, and two pins asserted the old answer with the wrong inference, "matching the two UTF-16 code units .NET would encode it from": the premise is right and the conclusion does not follow. A public source break under SA-2, all five conditions discharged — the negative fixture's third site is theoverride-less spelling that stays silently abstract and is reported only at instantiation (37 fixtures / 200 sites), downstream ticket #2380, and zero sites in both consumers. Three mutations, all caught. It was 17,336 immediately before, measured on the same date by ticket #2378. +2 on the 17,334 below, inSharpRuntimeTests_Core_Base(6,034 → 6,036). #2378 makes an undefinedSpecialFolderthrow on Windows, whereEnvironment.Windows.cs:768-770ends its switch in athrowabove aDebug.Assert(!Enum.IsDefined(folder)), while POSIX keeps returning""becauseGetFolderPathCore.Unix.cs:22says outright "No need to validate if 'folder' is defined". The table is transcribed rather than reflected: 47 enumerators over 46 distinct values (PersonalandMyDocumentsshare0x05), 14 holes inside0x00–0x3B, and those counts are asserted rather than trusted. Its placement is the point: it lives in adetailheader rather than inside the#ifdef _WIN32arm, because the behaviour is Windows-only but the data is not, and behind the#ifdefit would have been unreachable from the only platform this gate runs on. The Windows arm is verified rather than asserted: a MinGW cross-compiler is present, soEnvironment.cppcompiles for Windows,nmshows the guard and its table in that object and neither in the POSIX one — evidence that the rejection is confined by construction — and removing the guard drops the symbol, so the check discriminates. What is still not verified is runtime behaviour on a real Windows host; the CI is Ubuntu-only and that limit is stated. Four mutations caught, one reformulated after-Werrorrejected the first spelling. It was 17,334 immediately before, measured on 2026-08-18 by ticket #2376. ±0 on the #2359 reading, and deliberately so: #2376 inverted its pin in place, one case replacing one. #2376 makes the three strict HTTP-date arms match the format strings they transcribe —sscanf's%dand%[A-Za-z]bound no field, soSun, 06 Nov 199 08:49:37 GMTwas accepted and read as the year 199 AD, a wrong instant off by more than eighteen centuries rather than a rejected format. .NET'syyyy/yyareParseDigitsat an exact width and itsddd/ddddare name tables, not widths. The ticket named two sites and there are three: asctime carries the same unbounded year and the same unvalidated weekday, and repairing two thirds of one rule is not repairing it. And the lenient arm needed the same rule — the first cut leftXyz, 06 Nov 1994 …parsing, because #2360's arm checked the weekday's length rather than its name, so a value the strict arm had just refused was accepted one arm later; that is the hazard of two parsers for one grammar. Six mutations caught, two of them reformulated after the first spelling was invalid — removing an entry from a fixed-sizestd::arrayleaves a null the comparison dereferences. It was 17,334 immediately before, measured on the same date by ticket #2359. +1 on the 17,333 below, inSharpRuntimeTests_Uri(277 → 278; two pins inverted, three cases replacing two). #2359 makes aUrihost obey .NET's DNS character set —Uri("http://exa mple.com/")was accepted, reporting the host"exa mple.com". #2005 stopped here deliberately, because this half "needs a trace through .NET's host parser, not a line to quote". The trace is four steps and it corrects the framing: which of the six namedBadHostNamesites a space reaches does not matter, becauseDomainNameHelper.IsValidtestsIndexOfAnyExceptover exactly-0-9A-Z_a-z., the IRI path lists the space explicitly, and — the step that makes it uniform — no built-in scheme setsAllowAnyOtherHost, measured across all fourteenUriSyntaxFlagsconstants. Non-ASCII stays accepted because .NET's IRI path takes it, except U+0080–U+009F; decoding the scalar rather than waving bytes through is what makes that reachable, and it costs nothing because #2354 put the decoder inCore.Baseearlier the same day (graph unchanged at 41/93). The path, query and fragment are untouched. One tightening arrives with it: a WebSocket test asserted that CR/LF in a host is refused atConnectAsync, and that URI can no longer be built, so the assertion moved to the constructor — leaving it where it was would have passed for the wrong reason, since the constructor now throws outside theEXPECT_THROW. Four mutations, all caught. It was 17,333 immediately before, measured on the same date by ticket #2349. ±0 on the #2309 reading, and deliberately so: #2349 inverted its two scope pins in place, two cases replacing two. #2349 makes both writer door families reject a code point outside XML 1.0'sCharproduction — 28 of the 29 non-Charbytes in0x00–0x1Fused to be emitted raw, so the document was not well-formed XML. The ticket recorded five priced options and called it a user decision; the reference collapses them to one:XmlWriterSettings.CheckCharactersdefaults totrue(XmlWriterSettings.cs:513) and is enforced —InvalidXmlCharthrows when it is set and entitizes when it is not — so .NET rejects by default and the flag is what turns that off. Both of the ticket's pricing complications are dissolved rather than accepted. It priced enforcement onXmlConvert::VerifyXmlChars, which iteratescharand so checks bytes, acceptingU+FFFE,U+FFFFand a lone surrogate — but #2354 put a code-point decoder inCore.Baseearlier the same day, and both modules already depend on it, so the correct check costs one call and no new edge (graph unchanged at 41/93). And it priced option B as "not a same-shaped change on both sides" because the Xml.Linq doors take no settings — .NET's do not either:XNode.GetXmlWriterSettingsbuilds a defaultXmlWriterSettings(XNode.cs:681-687) and inheritsCheckCharacters = true, so the two door families agree by construction rather than by coordination. Two exception types, deliberately:NULkeepsXmlExceptionbecause it is this port's own truncation guard, every other non-Charcode point raises .NET'sArgumentException. What is still open is stated rather than left implicit: this port's reader accepts these characters, so writing and reading now disagree — .NET has no such asymmetry because its reader enforces too, and closing it here means driving tinyxml2 character by character. Four mutations, all caught. It was 17,333 immediately before, measured on the same date by ticket #2309 and confirmed by two consecutive full runs. +4 on the 17,329 below, inSharpRuntimeTests_Core_Base(6,030 → 6,034; three pins inverted, four cases added). #2309 closes SR-AUD-098's C2/C3/C4:AggregateException("outer", {a, b})reported"outer"and discarded the leaves, and .NET composes unconditionally whenever there are inner exceptions (AggregateException.cs:339-360). The ticket's two blocking grounds answer each other: composition and preservation cannot both hold under one stored string — measured, closing both yields"custom outer (a) (b) (a) (b)"and grows without bound under repeatedFlatten()— and the escape is a secondstd::string, which is exactly SA-3's case.sizeof(AggregateException)192 → 224 andsizeof(UnobservedTaskExceptionEventArgs)208 → 240, pinned; consumers rebuild.FlattenandHandlediffer and that is .NET's doing:Flattenpasses the raw message for a plain aggregate and the composed one for a derived type (:335), which is precisely what stops the accretion, whileHandlepasses the composed one (:281) — so a rethrown aggregate legitimately lists its leaves twice. The composition runs in the constructor rather than the getter, which is observationally identical becauseinnerExceptions_is fixed at construction, and avoids a public signature change. Five mutations, all caught. The gate run also caught a pre-existing flake from #2326: a resolution test that took 200 back-to-back timestamp pairs measured the machine, not the code — eight passes in isolation, one failure under gate load — and now comparesGetTimestamp()againststeady_clockread directly, a comparison of values immune to scheduling that still catches the mutation restoring the division. It was 17,329 immediately before, measured on the same date by ticket #2186. +1 on the 17,328 below, inSharpRuntimeTests_TimeZone(187 → 188; two gated pins inverted, three cases replacing two). #2186 asked five parity questions, each "requiring the .NET reference or a managed runtime that this container does not have", and/rvanswers all five: three repairs, two already correct. The conversions clamp rather than throw (TimeZoneInfo.Cache.cs:340-342), andConvertTimeclamps once, at the end, because .NET computes from raw ticks "to avoid precision loss from double-clamping" — a mutation that clamps twice is caught. The reference corrects the ticket's own statement of two of the three validations: thedaylightDeltarange is not ±14 hours but-23.0 .. 14.0, and .NET explains why in a comment — Samoa moved across the International Date Line — while the message still says ±14, an inconsistency that is .NET's and is transcribed rather than tidied; and the seconds check is not sub-minute but not a whole number of minutes. Question 5 was already right, and the reference contains a trap: reading onlyCalculateUtcOffset's window test puts an ambiguous 01:30 inside daylight, and the line after it overrides that from a flag only a prior UTC→local conversion sets — so a freshly builtDateTimegets the standard reading, which is what #2182 chose. Non-zone-data now raisesInvalidTimeZoneExceptionwhere an absent id still raisesTimeZoneNotFoundException: different answers to different questions, and a caller catching only the first used to swallow the second. Five mutations, all caught. It was 17,328 immediately before, measured on the same date by ticket #2174. +5 on the 17,323 below, inSharpRuntimeTests_Numerics(336 → 341). #2174 asked four parity questions that were blocked on evidence, not approval, and/rvanswers all four: two were already matches, one is a match whose explanation was wrong, and one was a real divergence in a place the ticket's own probe could not see.Complex::ToStringwas right all along — the audit's two citations disagreed and the source wins, since the(26.1, 18.06)doc example is stale prose about a constructor.BigInteger::Parsethrew on" 1"and"1 ", whereNumberStyles::Integer— the default style — isAllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign, so the port was stricter than the style it claims to implement. The vector half is the substantive one: the ticket measured NaN propagating fromMin/Maxwith the NaN in the first operand; with it in the second the port discarded it, becausestd::max(a, b)isa < b ? b : aand is asymmetric under NaN. .NET's expression propagates from either side — and nothing in it mentionsIsNaN(y), which is exactly why that row is easy to miss. A signed-zero rule came along with it, because .NET's third disjunct is not symmetric (MaxtestsIsNegative(y),MintestsIsNegative(x)). Two of my own expectations were wrong and the port was right:BigInteger(-1) << INTCS_MINis −1, not 0, since>>is arithmetic, and<< INTCS_MAX's unbounded work is what .NET does too. One mutation is an equivalence in the "C" locale and is recorded as one rather than counted — a test could only distinguish it by changing the global locale inside a shared binary. It was 17,323 immediately before, measured on the same date by ticket #2166. ±0 on the #2105 reading, and deliberately so: #2166 inverted three pins in place, three cases replacing three. #2166 makes sevenConsoledoors reject arguments they accepted silently. The cursor bound is not a belief about a platform layer, which is how #2165 recorded it: .NET validates inConsole.csitself, before dispatch (Console.cs:550-559), so it applies on every platform — and the comparison is>=, so 32766 is the last accepted column and 32767 the first rejected one, an off-by-one pinned on both sides. The other six are a different case and the difference is stated rather than glossed: .NET's Unix pal throwsPlatformNotSupportedExceptionfor every one of them, so on this port's runtime platform .NET states no range at all; the ranges adopted are its Windows pal's, because they are the only ones .NET defines, and copying the Unix answer would remove a feature this port offers rather than repair one. The buffer-relative halves of those checks are deliberately not reproduced — this port has no buffer geometry — so the rejection is a subset of .NET's and never a superset. This also lands the exact message texts #2163 and #2164 could not: their types were probe-verified and their texts were not, and the ticket recorded that the texts rode along on this one. The previous checkpoint's #2105 rename test flaked in this run and was repaired first: it assumed two events share oneinotifybatch, passed three times in isolation and failed inside the full gate, so it now counts only deliveries that arrive after the stop — a test that is intermittently green is not evidence (#2352). Six mutations caught; two were invalid as first written and one was masked by its sibling check, all reformulated rather than counted. It was 17,323 immediately before, measured on the same date by ticket #2105. +3 on the 17,320 below, inSharpRuntimeTests_IO(693 → 696). #2105 was a deferred verification whose review said the measurement "needs TSan plus a deterministic harness". It needs no TSan, and the batch is what makes the harness deterministic: inotify delivers many events in oneread()and the watch loop dispatched the whole batch before it next reachedpoll(), so creating 24 files and stopping the watcher from the first handler answers the question with no timing race. Thirteen further handlers ran after the setter returned. Only the self-stop path was wrong — an external stop joins the watch thread, a stronger guarantee than .NET's, and that half is now pinned too. The repair is .NET's: gate per event, at the point of dispatch, and drop (FileSystemWatcher.Linux.cs:1071-1093, 1211-1217). The gate has two sites and the second is easy to miss — an unpairedIN_MOVED_FROMis reported as aDeletedfrom a loop that runs after the batch — found by mutation, since removing it passed every other test in the repository. And its test is easy to write vacuously: the move-out must come first in the batch, or the per-event gate drops it before it is ever recorded and the second gate is unreachable, which is exactly what the first cut measured. One mutation is an equivalence and the code says so at the site rather than claiming a rationale it does not have. It was 17,320 immediately before, measured on the same date by ticket #2070. +1 on the 17,319 below, inSharpRuntimeTests_Net_Http(200 → 201; one gated pin inverted, two cases added). #2070 makesStringContenttake anEncodingrather than a charset string, and serialise through it. The old signature let the label and the bytes contradict each other:StringContent("é", "utf-16")announcedcharset=utf-16and emitted the two UTF-8 bytesc3 a9, which a conforming server reads as one UTF-16 code unit,U+A9C3— a different character, silently. .NET does not validate against that; it makes it unrepresentable, and that is why the repair is a signature change rather than a check: oneEncodingis both the serialiser and the label (StringContent.cs:48-73,90-98), so there is no second source of truth to disagree with. A check would still let a caller name a charset the body was not encoded in. A public source break under SA-2, with all five conditions discharged — including a negative fixture whose third site is the two-argumentStringContent(body, "utf-8")most likely to survive a careless migration (36 fixtures / 197 sites), downstream ticket #2379, and the measurement that closes it: zeroStringContentsites in both consumers. One check became unnecessary and is kept anyway — #2063's CR/LF rejection on the charset, which can no longer carry one. The module graph goes 41/92 → 41/93 (Net.Http→Text) with the catalogue regenerated; a private copy of one encoder would have been the duplication #2354 just removed six of. Three mutations, all caught. It was 17,319 immediately before, measured on the same date by ticket #2321. +1 on the 17,318 below, inSharpRuntimeTests_Core_Base(6,029 → 6,030). #2321's blocker was the exception identity, and/rvsettles it: #2320's transcription had the type, the parameter name and the format string right and used the two-argumentArgumentOutOfRangeExceptionconstructor where .NET uses the three-argument one, so theActual value was N.clause was being dropped. That is the whole repair. The folder half asked for a table the reference says is not needed: C4 wanted a definedness table separating a defined-but-unmappedSpecialFolderfrom an undefined one, andGetFolderPathCore.Unix.cs:22answers in a comment of its own — "No need to validate if 'folder' is defined" — becauseGetSpecialFolderreturnsnullfor anything unhandled andnullbecomes"". So on POSIX the two categories are deliberately indistinguishable, which is what this port already did; the new pin probes both, since one that probed only undefined values would pass against a table that does not exist. The answer is platform-dependent, which the ticket did not anticipate: .NET's Windows core ends its switch in athrow, and matching it needs the table after all — but only on Windows, where this Ubuntu-only gate could never run it, so that is #2378 rather than an untestable table smuggled in beside verified work. Three mutations, all caught. It was 17,318 immediately before, measured on the same date by ticket #2323. +7 on the 17,311 below:SharpRuntimeTests_Core_Base6,026 → 6,029,SharpRuntimeTests_Net_Http198 → 200,SharpRuntimeTests_Text_Json296 → 298; three pins inverted in place. #2323 gives a default-constructed exception .NET's fallback message,"Exception of type '{0}' was thrown."(Exception.cs:61,Strings.resx:2333), where this port had an empty string. Both recorded blockers are resolved, and the second dissolves rather than being worked around:{0}isGetType(), which is reflection this port permanently lacks — but .NET computes the fallback lazily only so_messagecan stay null for serialization, and the observable is identical if the constructor stores it, which a hundred subclasses here already do. So{0}is resolved statically, at each site, by the type that knows the answer: no reflection, no new virtual, no layout change, no signature change. Inheriting the base string into subclasses was rejected for the review's own reason — a message naming the wrong type is a lie, where an empty one is merely an absence. The review's premise measurement was wrong: it said exactly one type reaches the base fallback; re-measured across all 103 subclasses, three do, because= defaulton a derived exception default-constructs its base. All three now name themselves, and a guard test catches a future= defaultsubclass rather than letting it be discovered downstream. Downstream is not empty, which is the point of SA-2 condition 5:cnahas five types chaining toSystem::Exception()and six tests asserting an emptywhat(), all named to the line in #2377 rather than edited. It was 17,311 immediately before, measured on the same date by ticket #2360. +3 on the 17,308 below, inSharpRuntimeTests_Net_Http_Headers(431 → 434; two pins inverted, three cases added). #2360 adopts the sixteen lenient HTTP-date formats #2130 deliberately left, so this port accepts what .NET accepts at every date-carrying header. Twenty-one format strings are not twenty-one grammars — they are three shapes crossed with three axes, and transcribing the cross is what made the gaps visible: two cells of it are missing from .NET's list (a two-digit year with a numeric offset, with and without a day-of-week), so they are rejected explicitly and pinned, because the obvious completion of the pattern is exactly the widening with no reference behind it. The zone token iszzztranscribed — sign, one or two hour digits, an optional colon, two minute digits, rejected at 60 — so-0500and-05:00both work. The widening is safe by construction: the lenient arm runs after the three strict ones, so nothing they accept reaches it; a mutation reordering them changes no result, recorded as an equivalence rather than a catch. A correction the first tests needed:Retry-Afterdispatches on the first character, so a digit-leading date cannot reach its date branch — .NET does the same and says so — which makesIf-Rangethe door that sees the whole grammar. Two pre-existing leniencies surfaced and were split out as #2376 rather than bundled: the strictsscanfarms accept an abbreviated weekday on the RFC 850 shape and a three-digit year on IMF-fixdate, and both repairs are narrowings inside a widening ticket. Eight mutations caught, two only after routing the probe around a strict arm that answered first. It was 17,308 immediately before, measured on the same date by ticket #2361. +2 on the 17,306 below, inSharpRuntimeTests_Xml(507 → 509). #2361 closes the two-door asymmetry #2082 left open:XmlDocument::Loadhanded the path straight toLoadFileand never held the bytes, so a document loaded from a file accepted an undeclared entity and then silently rewrote it on save — accepting the reference was recoverable, reinterpreting it as literal text and re-escaping it meant loading and saving changed the document.Loadnow reads the file itself, which is whatLoadFiledoes internally anyway. The failure path deliberately still goes throughLoadFile, because that is what producesXML_ERROR_FILE_NOT_FOUNDand theErrorStr()the message has always carried; reproducing those from anifstreamfailure would be inventing diagnostics. Two mutations are not mutations and are recorded as such: text-vs-binary mode is a no-op on Linux by construction, andParse(c_str())without the length is semantically equivalent — verified by probe across three NUL placements through both doors, all six agreeing, rather than assumed. Three real mutations caught. It was 17,306 immediately before, measured on the same date by ticket #2209. +2 on the 17,304 below, inSharpRuntimeTests_IO_IsolatedStorage(58 → 60). #2209 makesGetFileNames/GetDirectoryNameshonour a directory-qualified pattern —GetFileNames("sub/" "*")used to return nothing — and the ticket's blocker is simply gone:/rvstates the contract in .NET's own comments above each method, andFileSystemEnumerableFactory.NormalizeInputs:45-56splits at the last separator, joins the directory half onto the root and matches only the final segment. The result stays a bare name, viaPath.GetFileName, because the store exists to hide its own root. One deliberate narrowing, on a security boundary: .NET does not confine the pattern — these two doors are the only ones on the type that bypassGetFullPath, so .NET'sGetFileNames("../" "*")escapes the store and lists its parent. This port resolves the directory half through the samefullPath()every other door uses, because reproducing .NET would mean opening a confinement hole to match a reference that has one; SA-8 does not reach it, and a test pins the asymmetry. A second, older divergence is recorded rather than introduced: .NET rejects a rooted pattern outright, while this port strips leading separators at every door, so"/x"has always meant x relative to the store — rejecting it only here would make the type inconsistent with itself. Five mutations, all caught. It was 17,304 immediately before, measured on the same date by ticket #2152. +2 on the 17,302 below, inSharpRuntimeTests_IO_Compression(101 → 103; two pins inverted, four cases added). #2152 makesDeflateStream,GZipStreamandZLibStreamenforce their own mode: aReadon a Compress-mode stream used to return 0, indistinguishable from end-of-stream, so a read loop terminated normally having produced nothing. The order is transcribed, not chosen —ValidateBufferArguments→EnsureDecompressionMode→EnsureNotDisposed(DeflateStream.cs:284-309) — so a stream that is both disposed and in the wrong mode reports the mode, and one existing test needed updating for it. A mutation proved the exception type alone cannot express that ordering:ObjectDisposedExceptionderives fromInvalidOperationException, here as in .NET, so the swapped-order mutation went uncaught until the test asserted the message.Flush()deliberately gets no guard, because .NET's has none, and a test pins the absence rather than leaving a plausible symmetry to be added later. A correction on the way past: #2148's doc-comment recorded that/rvwas absent to narrow the invalid-CompressionModeexception, and/rvnow confirms that choice exactly (DeflateStream.cs:99), so the caveat is replaced by the measurement. It was 17,302 immediately before, measured on the same date by ticket #2362. +1 on the 17,301 below, inSharpRuntimeTests_Core_Base(6,025 → 6,026; three pins inverted, one case added, two stale comments corrected). #2362 ends the unsigned integer parsers' one deliberate deviation, which had stood for about a year: a negative-indicating token is now grammar and the rejection is range, soUInt32::Parse("-1")is anOverflowException(Number.Parsing.cs:157) rather than aFormatException. The old argument was wrong on its own terms, and that is the whole ticket: it held that the only practical effect was which exception type a clearly-invalid input throws, but"-0"is not invalid —Number.Parsing.Common.cs:259-268clears the sign when no nonzero digit and no decimal separator were seen, so .NET returns 0 and this port was rejecting an input .NET accepts. The two rows could not be separated: repairing"-0"alone would have left"-1"diverging, which is worse than the old consistent rule, so grammar, normalisation and rejection landed together. The asymmetry is .NET's —"-0"is 0 and"-0.0"overflows — and it makes #2356's mutation M2 observable for the first time, since that ticket had recorded honestly that the same guard was unobservable in the signed core. Six mutations caught; a seventh is a no-op and is recorded as one — reinstating the old grammar rejection after the leading-token loop changes nothing, because the loop has already consumed the token, the same shape #2138 recorded.TryParsecallers see no change at all. It was 17,301 immediately before, measured on the same date by ticket #2354. +5 on the 17,296 below:SharpRuntimeTests_Text311 → 314 andSharpRuntimeTests_Globalization681 → 683. #2354 leaves one definition of this runtime's UTF-8 scalar decode. The ticket named three copies and there were six —Utf8JsonWriter.cpp(byte-for-byte identical),IdnMapping.cpp, and theUTF8Encoding.cppvariant #2014 recorded as unmovable because it reads a(pointer, end)range rather than astd::string; that range is now a parameter of the shared decode rather than a reason to hold a sixth copy. Zero remain, measured. The three doors really do want three different things, and they differ on exactly one input class — a structurally valid encoding of a non-scalar — so the shared header has two entry points,TryDecodeUtf8Scalar(reports, consuming the sequence's own length) andDecodeUtf8Scalar(substitutesU+FFFDover one byte, as .NET's replacement fallback does); collapsing them would have been a silent behaviour change in whichever door lost.Rune::TryGetRuneAthad no test anywhere in the repository, and that collapsing mutation passed all 17,296 tests then present — two of five mutations were caught only after a new assertion, and both are recorded rather than quietly fixed. The five new cases live in two files becausemodules/textdoes not depend onGlobalization, and a refactor is not a reason to add a public edge: the graph is unchanged at 41/92. −179 lines, no behaviour change at any door. It was 17,296 immediately before, measured on the same date by tickets #1929 and #2375, and confirmed by two consecutive full runs. +6 on the 17,290 below:SharpRuntimeTests_Core_Base6,019 → 6,025 (#1929 added six cases and inverted ten pins in place);SharpRuntimeTests_Netis unchanged at 332, because #2375 rewrote two tests rather than adding any. #1929 widens the date/time parse grammar to .NET's in the two respects the user decided on 2026-08-18: a one- or two-digit month and day, and .NET's fullParseTimeZoneoffset grammar, so"2024-6-15","+8","+2:5","+800"and"+0800"all parse. It is a pure widening — no string that parsed before parses differently or fails now — so it needed no source break, no negative fixture and no migration of callers. Both halves are transcribed: .NET's lexer types a run of one or two digits as aNumberTokenand three or more as aYearNumberToken(DateTimeParse.cs:5593-5605), andParseTimeZonesplits a three- or four-digit run asvalue / 100andvalue % 100(DateTimeParse.cs:552-556) — which is why"+800"and"+0800"agree. Two stops are deliberate and both are pinned: the year is not widened, because .NET reads a short one throughCalendar.ToFourDigitYear's culture-dependent century window; and the ±14 h bound stays onDateTimeOffsetrather than moving into the shared grammar, because that is where .NET applies it —DateTimeparses an offset and discards it, soDateTime::TryParse("…+99")succeeds whereDateTimeOffset::TryParse("…+99")fails. The widening forced a structural repair the ticket did not name:DateTimeOffset::TryParselocated its offset by scanning for a sign from character 10, which is correct only while the date part is exactly ten characters wide —"2024-6-5"is eight — so the three doors now share one grammar instead of three copies that had already drifted. Nine mutations, all caught. #2375 is a test defect the gate caught and it is the reason this reading is trustworthy: twomodules/nettests asserted the resolver's opinion rather than the port's."1.2.3."takes a 13 ms DNS round trip on this container and comes back as1.2.0.3(where"1.2.3"is answered by libc in 0.03 ms), and a reverse lookup of192.0.2.1raisedEAI_AGAINin one gate run while passing five isolated runs. Neither was a regression — both reproduce with the in-flight change stashed — and neither was disabled, weakened or skipped: the first now asksgetaddrinfoitself and requires the port to agree with that independent oracle (the pattern #2351 established for tzdata), and the second asserts what the finding was actually about, termination, since an unbounded mutual recursion can neither return nor throw. The file header's claim that these tests need no network was measured false and is corrected in place. It was 17,290 immediately before, measured on the same date by ticket #2115. +2 on the 17,288 recorded for #2269, inSharpRuntimeTests_Text_Json(three gated pins inverted, three cases added). #2115 makes both inertJsonDocumentOptionsflags real: trailing commas by a scanner that respects string literals and comments and removes a comma only before a closer, duplicates through nlohmann's parser callback — the one place a key is visible before its silent overwrite. The cheaper option was declined on purpose, and a test that looked sufficient was not: the obvious scope test passes even when the key scope never closes, so one mutation went uncaught until a root key reappearing after a nested object was added. Earlier the same date, measured on 2026-08-18 by ticket #2269. +4 on the 17,284 recorded for #2250, #2255 and #2289, inSharpRuntimeTests_Core_Base. #2269 makes all eight integer wrappers validate theirNumberStyles, where they validated nothing —Parse("2A", NumberStyles::HexFloat)used to return hexadecimal 42, a style .NET rejects outright. Thenoexceptproblem was not in the ticket and is the real cost: four of the eightTryParse(style)overloads werenoexcept,TryParsemust throw for an invalid style, and validating only the four that could already throw would have left the port inconsistent with itself. No existing test needed changing, so nothing relied on an invalid style. Earlier the same date: ±0 on the #2250 reading: #2255 and #2289 rewrote existing pins rather than adding. #2255 types theAppContextdata store withstd::any, making both of SR-AUD-102's .NET behaviours reachable — theAPP_CONTEXT_BASE_DIRECTORYoverride andTryGetSwitch's string fallback — where avoid*carried no type and no ownership. Two reference details decide the implementation:BaseDirectoryusesas string, so a non-string entry falls through silently, and the switch parse isbool.TryParse, which rejects"1"and"yes". A process note worth keeping: an intermediate gate run reported a green 17,284 while the build was actually broken, because stale objects were reused; the reading here was taken after a forced full rebuild. #2289 adds the repository's first[[deprecated]], at all five prose-only sites. +1 on the 17,283 below, inSharpRuntimeTests_Core_Base(two gated pins inverted, one case added). #2250 makesAppDomain::IsCompatibilitySwitchSetconsult the registry, where it returnedfalseunconditionally — a switch explicitly set to true still reported as unset. Neither of its two changes could land without the other: aboolcannot distinguish explicitly-false from unset, which is why .NET's isbool?, and keepingnoexceptwhile forwarding to a throwing, mutex-taking call would have meantstd::terminate. It was 17,283 immediately before, measured on the same date by ticket #2299. +2 on the 17,281 below, inSharpRuntimeTests_Core_Base. #2299 makesFunc<void>andConverter<T, void>ill-formed —Func<void>used to compile and was the same type asAction, not merely convertible to it, because an alias template introduces no new type. The finding's second prescription is structurally impossible and is now a declaration: with aliases there is one type, so constraining removes the spelling but cannot create a category. One measured detail is kept: testing for a constrained alias's absence must use a dependent parameter, since gcc evaluates one eagerly in a non-dependentrequiresand errors instead of yielding false. It was 17,281 immediately before, measured on the same date by ticket #2172. +1 on the 17,280 below, inSharpRuntimeTests_Numerics. #2172 makesComplex::Absreturndoubleas .NET's does, and removes the inventedAbsDthat existed only to work around the wrong return type. The break is loud and that is measured — no implicit conversion exists either way, which matters because both spellings computed the same magnitude, so only astatic_asserton the return type could catch it. A checker change had to land first: this is the repository's firstNumericsnegative fixture, and-Wpedanticrejecting__int128made its baseline broken, leaving SA-2's condition 2 unsatisfiable for the whole area;check_negative_consumer_fixtures.pynow takes a named relaxation from a closed set, and-isystemwas tried first and rejected because it silently disarmed two live sites elsewhere. It was 17,280 immediately before, measured on the same date by tickets #2297 and #2298. ±0 on the #2297 reading: #2298 moved its suite frommodules/core/teststomodules/threading/testsand rewrote it, six cases replacing six. #2298 givesThread.NET's six data-slot doors and makesLocalDataStoreSlot's constructor private — the type held onestd::anyshared by every thread, so a write from any thread replaced what every other thread read. The three named doors have three different contracts and are transcribed rather than invented. The slot holds an opaque id rather than the storage, because it lives inCore.BaseandThreadinmodules/threading, which depends on it — the graph is unchanged at 41/92. ThreadSanitizer was attempted and could not run, on a pre-existing incompatibility, so isolation is evidenced by a live two-thread test instead — stated rather than implied. It was 17,279 immediately before, measured on the same date by tickets #2291 and #2295. +1 on the 17,279 below. #2297 makesMarshalByRefObject's constructor protected and addsGetLifetimeService(), whose absence turned an observable runtime diagnostic into a compile error at an unrelated place. The third part is split out as #2374 because it is a vtable change: .NET'sInitializeLifetimeService()is virtual, and this class already has a vtable, so adding it inserts a slot here and in both derived classes — SA-3 excludes that. It must not be added non-virtually as a workaround, which would silently defeat the one thing the .NET member exists for while looking correct at every call site; its absence is pinned through a detection idiom. It was 17,279 immediately before, measured on the same date by tickets #2291 and #2295. ±0 on the #2295 reading, and deliberately so: #2291 rewroteApplicationId's suite rather than adding to it — thirteen narrow cases became six broader ones plus five new contract cases. #2291 takes all four of its review's decisions toward .NET at once, because the review said the fourth cannot be decided first: the name is validated, the public key token is a byte container cloned in and out (aconst&return would have defeated the constructor's own copy),Culture/ProcessorArchitecturearestd::optional, andToString()is .NET's grammar — including two quirks transcribed rather than tidied, a space beforeprocessorArchitecture's=and a token printed even when empty. #2292 closed on the way past:GetHashCodewasnoexceptwhile allocating. It was 17,278 immediately before, measured on the same date by ticket #2276. +1 on the 17,278 below, inSharpRuntimeTests_Core_Base. #2295 makesObsoleteAttribute'sMessage,DiagnosticIdandUrlFormatstd::optional<std::string>, matching .NET'sstring?— an absent and an empty value used to be the same state, and a default attribute compared equal to one built fromstd::string{}. A getter change alone could not have closed it: the boundary was on the way in too. The ticket's own objection to this route is discharged by measurement, not overruled — it said downstream consumers "were not inspected", and they have been:mobile-eggbertzero,cnaonce inside a comment.sizeof112 → 136 under SA-3. It was 17,278 immediately before, measured on the same date by ticket #2276. −5 on the 17,283 below, a documented decrease and the chain's fourth: nine narrowArgIteratorcases replaced by four that assert more. #2276 answers the ticket's question — the members stay instance members, because .NET's are — but the reference settled a larger half it had not asked: every member of .NET'sArgIteratorthrows, and three here returned quietly (End()a no-op,Equals()false,GetHashCode()0), handing a caller a plausible answer where .NET reports an unsupported platform. Three further divergences the ticket did not name are fixed: the exception was the base type,GetNextArgType()'s return type, and a missing overload. It was 17,283 immediately before, measured on the same date by ticket #2281. −15 on the 17,298 below, a documented decrease and the chain's third: #2281 removedSystem::UnitySerializationHolderunder SA-9, taking two suites totalling 15 cases with it. The ticket's own premise is corrected: .NET's type is notinternal— it ispublic sealed, with the comment "Needs to be public to support binary serialization compatibility". The real reason is better: it exists only for .NET Framework BinaryFormatter compatibility, is[Obsolete], and every public member takesSerializationInfo/StreamingContext— infrastructure this project permanently does not implement. It was 17,298 immediately before, measured on the same date by ticket #2334. −8 on the 17,306 below, and it is a documented decrease, the chain's second after #2284's −5: #2334 removedSystem::RuntimeTypeunder SA-9, taking its 8-case test file with it. The removal is exactly that — verified by countingTEST(in the file at its last committed revision — and no other executable's count moved. .NET'sRuntimeTypeisinternal sealed class RuntimeType : TypeInfo, not public API at all and not an enumeration, and the port's six values were its own invention. Removal beat renaming because there was nothing to migrate and, since reflection is a permanent deviation, nothing would ever force the .NET name free again.RuntimeTypeHandleis a real .NET type and is untouched, which the negative fixture asserts. It was 17,306 immediately before, measured on the same date by ticket #2326. +2 on the 17,304 below, inSharpRuntimeTests_Core_Base(6,042 → 6,044). #2326 makesStopwatch::Frequencythe clock's own 1,000,000,000 rather than theTimeSpantick rate, and stopsGetTimestamp()dividing by 100. The old pair was self-consistent and still a defect: every elapsed time was correct, but nothing finer than 100 ns could be resolved. The value is derived fromClock::period, not transcribed — reportingQueryPerformanceFrequencywhile samplingsteady_clockwould be a lie about a different timer — and staysconstexpr, a stronger guarantee than .NET's runtimestatic readonly. The review's warning was correct: twelve tests across two suites needed rescaling, none a regression, and two of them turned out to be aboutdoublerounding at unit scale where the system provider only happened to qualify. It was 17,304 immediately before, measured on the same date by ticket #2324. +2 on the 17,302 below, inSharpRuntimeTests_Core_Base(6,040 → 6,042). #2324 changed no production statement: it declares that event args stayconstto subscribers, and the declaration is a measurement — the repair was fully built and compiled before being reverted. SA-8 does not reach it, because SA-8 covers the port being more permissive than .NET and here it is more restrictive. Dropping the const costs 29 first-party sites, forcesEventArgs::Emptyto stop beingconst(a process-wide mutable global), and trips a deliberate tripwire belonging to #2199 — while buying nothing today, since the only twoEventArgstypes with a settable property are delivered through no handler alias at all. The trigger for revisiting is recorded: the first event that delivers a settable args object. It was 17,302 immediately before, measured on the same date by ticket #2325. +2 on the 17,300 below, inSharpRuntimeTests_Core_Base(6,038 → 6,040). #2325 makesResolveEventHandlerreturnstd::optional<std::string>, so a handler can decline as .NET'sAssembly?does. The empty string could not be borrowed for it — empty already means absent requesting assembly inResolveEventArgs, so a sentinel would have been unenforceable by the type, which is the defect itself. The review's "decide SR-AUD-103 first" is deliberately not taken, and its own reasoning is why: a delegate nothing calls is the cheapest possible moment to fix a signature. The break is asymmetric — a widening for handlers, sincestd::stringconverts implicitly. It was 17,300 immediately before, measured on the same date by ticket #2328. +1 on the 17,299 below, inSharpRuntimeTests_Core_Base(6,037 → 6,038; one pin inverted, one case added). #2328 makesArraySegment::CopyTo(std::vector&, index)reject a short destination instead of resizing it — .NET's body isArray.Copyand .NET arrays cannot grow, so a caller who passed the wrong buffer got a silently enlarged one instead of a diagnostic. It was also the one place in this repository that broke its own convention: twenty of twenty-sixCopyTooverloads already rejected. The review found four tests pinning the resize and there were five — the fifth was caught by the full gate after a filtered run had passed, which is why the gate covers the whole repository. It was 17,299 immediately before, measured on the same date by ticket #2322. +2 on the 17,297 below, inSharpRuntimeTests_Core_Base(6,035 → 6,037). #2322 makesSystem::ValueType's constructor protected, matching .NET'spublic abstract class. Protected rather than abstract, deliberately: a C++ class is abstract only by having a pure virtual, and .NET'sValueType.ToString()has a real body, so making one pure would invent surface the reference lacks. The review's premise was wrong and is corrected — it said the only derivations were two test types, and missed five direct instantiations in a different file, which the compiler found at once. The identityEquals, address hash and literalToStringstay: all three are reflection, and a test pins them as declarations. It was 17,297 immediately before, measured on the same date by ticket #2330. +3 on the 17,294 below, inSharpRuntimeTests_Core_Base(6,032 → 6,035). #2330 makes all eightTupleNarities getter-only, as .NET's are —Tuple::Create(1, 2).Item1 = 99used to compile and stick. The user decided it knowing the cost, which was stated first:t.Item1becomest.getItem1Property()permanently under rule 5. Getter-only means aconstreference — aT&return would satisfy the naming rule while leaving the finding in place.ValueTupleis deliberately untouched, because .NET's two tuple families differ on purpose, and a test pins that boundary. The ticket's own count was wrong: 61 real sites, not 75, since the 16 inSystemTypesRemainingTests.cppare allValueTuple— which is why the migration was driven off the compiler's private-access errors rather than a grep. It was 17,294 immediately before, measured on the same date by ticket #2332. +1 on the 17,293 below, inSharpRuntimeTests_Buffers(628 → 629). #2332 makesSequencePosition's two components private, matching .NET's private readonly fields — the port could only state in a doc-comment the rule .NET enforces in the language. It is an access change, not a layout change:sizeof,alignofand trivial copyability are unchanged, so consumers need no rebuild, and brace initialisation with two arguments still compiles through the constructor. No first-party migration was needed — every other use already went through the accessors. It was 17,293 immediately before, measured on the same date by tickets #2327 and #2339. +2 on the 17,291 below, inSharpRuntimeTests_Core_Base(6,030 → 6,032). #2327 givesArray::MaxLength.NET's0x7FFFFFC7instead ofint.MaxValue— 56 less, and the gap is the GC's allocation ceiling rather than this port's to choose; the assertion that let the divergence survive wasEXPECT_GT(…, 0), which passes for both values. #2339 makesSystem::Attribute's constructor protected, and the copy and move members with it, so the base cannot be reached by a slice — a route the review did not enumerate. The identityEqualsis not repaired and cannot be: .NET compares every instance field, which is reflection, so the deviation is now pinned by a test showing that all forty-six subclasses inherit it. It was 17,291 immediately before, measured on the same date by ticket #2271. +4 on the 17,287 below, inSharpRuntimeTests_Core_Base(6,026 → 6,030). #2271 makesDelegate::Combine/Removerefuse operands of different concrete types, as .NET does, and makes a composed delegate carry the type. The finding's two halves really were inseparable — a guard alone breaks the chained form — but no data member was needed: a multicast's type is the type of its entries, whichCombinekeeps uniform, so it is read from the list andsizeof(Delegate)is unchanged. The ticket expected two fixtures to need rewriting and none did; what the removing mutation breaks is three pre-existingMulticastDelegateTests, which is what shows the derivation is load-bearing. It was 17,287 immediately before, measured on the same date by ticket #2313. +1 on the 17,286 below, inSharpRuntimeTests_Core_Base(6,025 → 6,026; two pins inverted, three cases added). #2313 makesEnvironmentable to express a present-but-empty variable: the getter returnsstd::optional<std::string>andstd::nullopt, not"", is what removes. The deferral's two readings implied opposite work and the reference settles it — only a null value removes, and there is no empty-to-null conversion on the read side, so the port's own doc-comment stated its contract rather than .NET's. The measured downstream split is what makes this safe:cnahas zeroGetsites, so the return-type change breaks nothing, and 96 of its 98Setsites are unaffected — but two change meaning silently, one carrying the comment "empty value deletes it", and both are named to the line in #2366 rather than edited. Fixture set grows to 19 fixtures / 139 sites. It was 17,286 immediately before, measured on the same date by ticket #2246. +3 on the 17,283 below:SharpRuntimeTests_Core_Base6,022 → 6,025 andSharpRuntimeIntegrationTestsunchanged (one pin inverted in place). #2246 removesProperty<T>'s vestigialcachedValue, sosizeof(Property<int>)falls 72 → 64 andsizeof(Property<std::string>)96 → 64 — the size no longer depends onTat all. The member was wrong, not merely unused: a cache in the wrapper can only ever disagree with the storage the getter and setter close over. The half asizeofpin cannot express is the one worth having — every constructor default-initialised it, soThad to be default-constructible, a requirement a getter/setter wrapper never used. #2244 wrote the integration pin for exactly this moment and it is inverted, not deleted. It was 17,283 immediately before, measured on the same date by ticket #2215. +1 on the 17,282 below, inSharpRuntimeTests_Core_Base(6,021 → 6,022; one gated pin replaced by two cases). #2215 guardsArraySegment<T>'s four enumeration doors, so a default segment throws where a range-forused to perform zero iterations silently. It is a public signature change — thenoexceptdrop — and the first landing under SA-10, with all five SA-2 conditions discharged; the negative consumer fixture set grows to 18 fixtures / 135 sites, its fourth new site being the shape that breaks silently (a helper whose ownnoexceptis computed from the door). The guard asks whether the array is present, never whether the count is zero. One mutation is caught as a crash rather than a failure, inherently — it reintroduces the very SEGV the finding recorded, and no test can assert on undefined behaviour. It was 17,282 immediately before, measured on the same date by ticket #2357. +2 on the 17,280 below, inSharpRuntimeTests_Net_WebSockets(103 → 105; one pin inverted, three cases added). #2357 givesClientWebSocket.NET's outer gate —ObjectDisposedExceptionwhen disposed,InvalidOperationExceptionwhen never connected. The ticket's framing was too simple and the reference corrected it: .NET has two layers, and the inner per-operation check raisesWebSocketException(InvalidState)exactly as this port already did, so rewriting it would have replaced a correct exception with a wrong one. No new data member —Abort()callsDispose()in .NET, soInternalStatemaps onto state this class already holds andsizeofstays 424. It was 17,280 immediately before, measured on the same date by ticket #2238. +2 on the 17,278 below, inSharpRuntimeTests_Core_Base(6,019 → 6,021; three pins replaced by five). #2238 makesLazy<T>'sPublicationOnlymatch .NET: the factory runs with no lock held, the first publication wins and every loser discards its value. This reverses a guarantee the class advertised for its whole life, and the user decided it knowing the price, stated first — aPublicationOnlyfactory may now run concurrently and more than once, and a recursive one exhausts the stack instead of raising. The old pin asserted the old contract and hung against the new code, which is the correct signal. One mutation is not caught and the code says so at the site: restoring the reentrancy guard changes nothing, becausecreatingThreadId_is no longer written on that path. It was 17,278 immediately before, measured on the same date by ticket #2320. +5 on the 17,273 below, inSharpRuntimeTests_Core_Base(6,014 → 6,019). #2320 makes POSIXGetFolderPathhonourXDG_CONFIG_HOME/XDG_DATA_HOMEwhen absolute, and honourSpecialFolderOption, which it used to accept and ignore. The default option now verifies —GetFolderPath(Desktop)is""where no such directory exists — which isGetFolderPathCore.Unix.cs:26-47's contract and a behaviour change. The second clause was not a user question: the reference answers it, so it was derived. A test defect had to be fixed on the way: five rows passed only because this machine has~/Desktop, which SA-6 calls a defect in the test, and one asserted the opposite of the new contract. The premise for declining full XDG was wrong and is corrected rather than left standing —ReadXdgDirectorydoes exist, so eight further diverging rows are #2364 rather than a silent widening. It was 17,273 immediately before, measured on the same date by ticket #2138. +2 on the 17,271 below, inSharpRuntimeTests_Net_Sockets(124 → 126; two gated pins replaced by four cases). #2138 makes the sixTcpClient/TcpListener/UdpClientendpoint doors refuse an IPv6 address deliberately, at the door, with .NET's ownArgumentExceptiontext. The finding's wording did not reproduce: SR-AUD-266 called it silently misrepresented and, measured twice, nothing was ever silently narrowed — this was a diagnostic defect. Three refusals existed, none naming the operation the caller attempted, and one of them let aTcpListenerbe fully constructed and only fail atStart(). The two hostname doors are deliberately unchanged because"::1"at a hostname parameter is #2359's question. One mutation is not a mutation —IPAddresscarries its family in a singlebool, so an allow-list and a deny-list are equivalent, and the dead name table the first cut wrote was deleted rather than defended. It was 17,271 immediately before, measured on the same date by ticket #2356. +1 on the 17,270 below, inSharpRuntimeTests_Core_Base(6,013 → 6,014; one gated pin replaced by two cases). #2356 makes an all-zero magnitude parse to0at any exponent —"0E30"was anOverflowException. The recorded answer was wrong, and so was the first correction: the deferral asked whether a non-positive scale overflows and the old pin asserted that a large positive one does, whileNumber.Parsing.Common.cs:259-268discards the scale entirely when no nonzero digit was seen. A middle answer — that the count of zeros matters — is also wrong and is recorded as a trap, because a leading zero skips the same block and so never advances the scale. Two of four mutations are not caught and both are explained rather than papered over: one is unobservable until #2362 lands, the other is semantically identical code. It was 17,270 immediately before, measured on 2026-08-17 by ticket #2043. +1 on the 17,269 below, inSharpRuntimeTests_Net(331 → 332). #2043 makesDns::GetHostAddressesreject0.0.0.0and::withArgumentException, as .NET does at three separate sites. This removes a result that worked, which is why the ticket was split out of #2039 and needed evidence rather than judgement: an unspecified address names every local interface tobindand nothing at all toconnect, so resolving one to itself hands a caller a target it cannot use. The rejection runs before the address-family check, matching the reference's own ordering, and that ordering is asserted. It was 17,269 immediately before, measured on the same date by ticket #2044. +3 on the 17,266 below, all inSharpRuntimeTests_Net(328 → 331). #2044 makesWebUtility::HtmlEncodeencode U+00A0..U+00FF and supplementary scalars as decimal references, closing the asymmetry where the decoder understood more than the encoder could emit. The premise the deferral rested on was wrong: it held that "two HTML encoders in one repository must not be given two different escape sets", and .NET has exactly two, with exactly two different sets, deliberately —HtmlEncoder's allow-list in uppercase hex (#2019) and this narrower decimal one. Each matches its own counterpart; a mutation that "makes them consistent" is caught. It was 17,266 immediately before, measured on the same date by ticket #2106. +1 on the 17,265 below, inSharpRuntimeTests_IO(691 → 692). #2106 givesBinaryData::ToString().NET's UTF-8 decoding, so an ill-formed byte becomes U+FFFD instead of being handed to the caller inside astd::stringthat is not valid UTF-8. The finding's two halves are answered differently on purpose: the aliasing half will not be repaired, because reproducing .NET's wrappingBinaryData(byte[])means holding a borrowed reference in a language with no GC — the CCF-019 defect this programme has spent the session removing. The shared UTF-8 scalar decode moved frommodules/texttoCore.Basesomodules/iocould reach it without a sixth copy or a new public component edge; the bodies are byte-identical,System::Text::detailre-exports both names, and the module graph is unchanged at 41/92. It was 17,265 immediately before, measured on the same date by ticket #2080. +3 on the 17,262 below, all inSharpRuntimeTests_Xml(504 → 507). #2080 givesXmlConvert'sTimeSpanpair the XML Schemadurationform —P1D, not1.00:00:00.0000000— and the deferral's three unanswerable sub-questions are all answered: the year/month factors are .NET's own estimate (365 and 30, and the source says so), the native colon form is no longer accepted, and the exception is alwaysFormatExceptionbecause .NET remaps even its ownOverflowException. Two things surprised the first cut and are recorded:PT.5Sis valid (the test was wrong, not the parser), and one line ofXsdDuration.TryParseis dead in the reference itself. It was 17,262 immediately before, measured on the same date by tickets #2082 and #2083. +6 on the 17,256 below:SharpRuntimeTests_Xml496 → 504 andSharpRuntimeTests_Xml_Linq334 → 332 (two pins replaced by two); no other executable's count moved.XmlDocument::LoadXmlnow rejects an undeclared entity reference and an undeclared namespace prefix, as .NET does. The entity case was worse than acceptance:&nope;was reinterpreted as literal text and re-escaped, so loading and saving a document silently rewrote it. The entity check must run on the raw text — tinyxml2 decodes the predefined five, so&nope;and&nope;are indistinguishable afterwards — and "undeclared" is not "not predefined": the first cut rejected the repository's own billion-laughs pin, which declares two entities and references one.Load(filename)cannot run the entity check and that asymmetry is #2361. It was 17,256 immediately before, measured on the same date by ticket #2072. +5 on the 17,251 below, all inSharpRuntimeTests_Net_Http(193 → 198). #2072 givesHttpClient::parseUrlRFC 3986's userinfo rule —http://user@host/preturned the host"user@host", which went togetaddrinfoas a DNS name and into theHost:header — and stopshttp://[::1]x/psilently discarding thex, so the URL a caller writes and the URL the client connects to can no longer differ without a diagnostic. The last@is the delimiter, per RFC 3986 §3.2.1. Note one mutation was not caught and the redundant check it exposed was deleted rather than defended. It was 17,251 immediately before, measured on the same date by ticket #2130. +4 on the 17,247 below, all inSharpRuntimeTests_Net_Http_Headers(427 → 431). #2130 accepts the two obsolete HTTP-date forms RFC 9110 §5.6.7 requires a recipient to accept, which .NET accepts too — it tries twenty-one formats. The widening uncovered a latent defect:Sun, 06 Nov 94 …was not rejected, it was accepted as the year 94 AD, a silently wrong instant off by nineteen centuries, and no test had noticed. That row is a correction. The two-digit-year window is .NET's (TwoDigitYearMax == 2029), not the naive1900 + yy. The remaining sixteen lenient .NET formats are deliberately not adopted and are pinned as such — #2360. It was 17,247 immediately before, measured on the same date by ticket #2060. +1 on the 17,246 below, inSharpRuntimeTests_Buffers(627 → 628). #2060's two claims came apart:Utf8Parser's signedD/Ggrammar now accepts a leading+because .NET's does (Utf8Parser.Integer.Signed.D.cs:16-31), while unsignedDstill rejects it because .NET's has no sign handling at all — so the "internal inconsistency" with unsignedNis .NET's own and is reproduced deliberately, with a mutation that tidies it away being caught. It was 17,246 immediately before, measured on the same date by ticket #2119. +1 on the 17,245 below, inSharpRuntimeTests_Text_Json(293 → 294). #2119 is a measurement, not a repair, and the answer is partial: the like-for-like re-run ofOwnedTreeLifetimeContractPlan.md's deep-nesting rows shows the parse half gone outright — X28c went from SIGSEGV to 0.032s and X28d is linear — and J19c fixed, while J19d survives: 100,000-deep programmatic construction takes 9.63s against 20,000's 0.365s, which is 26× for 5× the depth, i.e. still quadratic. Its cause isAssignParent's ancestor walk, already owned by #1896. The old pin tested only the parse path and would have gone on passing while J19d stayed broken. It was 17,245 immediately before, measured on the same date by ticket #2005. +2 on the 17,243 below, all inSharpRuntimeTests_Uri(275 → 277). #2005 makesSystem::Uritrim surrounding whitespace before parsing, removing an asymmetry the deferral had recorded rather than tidied: leading whitespace used to make the whole reference relative while trailing whitespace was accepted into the path. The trimmed set is exactly .NET'sUriHelper.IsLWS— space, LF, CR, TAB — deliberately notstd::isspace, and a vertical tab is asserted to still fail. The other half of #2005, a space inside the host, is deliberately left to #2359: it is a narrowing question needing a trace through .NET's host parser, and bundling it with a verified widening is what the original deferral was avoiding. It was 17,243 immediately before, measured on the same date by ticket #1998. +3 on the 17,240 below, all inSharpRuntimeTests_Uri(272 → 275). #1998 makesUriParser::IsKnownSchemereject a malformed scheme withArgumentOutOfRangeExceptioninstead of reporting it as merely unknown — a caller could not previously tell "I do not recognise this scheme" from "that is not a scheme". The deferral was correct and the evidence has arrived: the ticket was blocked on "the same line #1963 sits on", and the reference now confirms SR-AUD-147 exactly. It also addsUri::CheckSchemeName, which is public in .NET and which .NET's ownIsKnownSchemeis defined in terms of. It was 17,240 immediately before, measured on the same date by tickets #2056 and #2057. +2 on the 17,238 below, all inSharpRuntimeTests_Buffers(625 → 627). #2056(a) makes a disposed pool owner throwObjectDisposedExceptioninstead of returning a zero-lengthMemoryindistinguishable from a liveRent(0); half (b) stays pinned as unfixed, and the .NET-shaped null discriminator was deliberately rejected because it would make half (b)'s latent use-after-free deterministic. #2057 makes a defaultReadOnlySequence<T>enumerate no segments wheregetEmpty()enumerates one — .NET's distinction, which astd::vectorcannot express. Both grow by 8 bytes under SA-3;modules/buffersis header-only and nothing outside it includes either type. It was 17,238 immediately before, measured on the same date by tickets #2067 and #2069. +1 on the 17,237 below, inSharpRuntimeTests_Net_Http(192 → 193; two gated pins inverted, three cases added). #2067 makes anHttpRequestMessagesendable once, as .NET has always required — the second send reuses content the first may have consumed — with an atomic claim, andsizeof(HttpRequestMessage)grows 192 → 200 under SA-3 (consumers must rebuild). #2069 bounds the status code to 0..999, which is .NET's domain, not 100..599. This closes themodules/net-httpreview: #2067, #2068, #2069 and #2071 all landed today. It was 17,237 immediately before, measured on the same date by ticket #2071. +5 on the 17,232 below, all inSharpRuntimeTests_Net_Http(187 → 192). #2071 bounds all three response-reading paths withMaxResponseContentBufferSize, defaulting to .NET'sHttpContent.MaxBufferSize(int.MaxValue) rather than a number this port invented — pinned to the exact value. A declaredContent-Lengthis checked before any body byte is read, because a Content-Length is a claim; and a chunk size ofFFFFFFFFFFFFFFFused to be a request to accumulate eighteen exabytes. The knob lives onHttpClientHandlerrather thanHttpClientbecause this port's handler reads eagerly where .NET's streams, withHttpClientforwarding so it keeps .NET's name, default and validation. It was 17,232 immediately before, measured on the same date by ticket #2068. +1 on the 17,231 below, inSharpRuntimeTests_Net_Http(184 → 187, two gated pins inverted and three cases added). #2068 makes HTTP field names case-insensitive, and the premise that blocked it was avoidable: the review said the repair "cannot make the lookup case-insensitive without changing" the public map type, which is true of one implementation and not of the rule —setHeadererases case variants on the way in, sogetHeadersProperty()'s type is untouched and the pinningstatic_asserts are kept. The second half is the security-relevant one: the handler wroteHost,User-Agent,AcceptandConnectionunconditionally before the caller's map, so a caller-setHostwent on the wire twice. It was 17,231 immediately before, measured on the same date by ticket #2095. +2 on the 17,229 below, inSharpRuntimeTests_Net_WebSockets(101 → 103). #2095 enforces fragmentation ordering: a continuation with no message in progress used to be typed from a C++ member initialiser, and a fresh data frame mid-fragment used to make message boundaries server-controllable. The deferral was correct and is now resolvable — .NET rejects both (ManagedWebSocket.cs:1385-1410) and this port uses its messages. A second defect found while fixing it is fixed with it because it is the same state: the tail of a non-final frame reportedendOfMessage = truefrom the buffer's exhaustion. Twoboolmembers fit in padding, sosizeof(ClientWebSocket)stays 424.modules/net-websocketsnow has no open implementation work: #2092, #2093, #2094, #2095 and #2096 all landed today, leaving only theneeds_userparity question #2357. It was 17,229 immediately before, measured on the same date by ticket #2092. +1 on the 17,228 below, inSharpRuntimeTests_Net_WebSockets(100 → 101). #2092 makesWebSocketExceptionkeep the inner exception it used to accept and discard, and the analysis that blocked it was half wrong: the review called the repair a change to a widely derived base "and possibly an object-layout change on every exception type in the repository", butSystem::Exceptionhas carriedinnerException_all along — measured,sizeof(System::Exception),sizeof(Win32Exception)andsizeof(WebSocketException)are byte-identical before and after — and .NET's ownWin32Exceptionhas the very constructor the port was missing. This closes themodules/net-websocketsreview: #2092, #2093, #2094 and #2096 all landed in this session. It was 17,228 immediately before, measured on the same date by tickets #2094 and #2358. +8 on the 17,220 below:SharpRuntimeTests_Net_WebSockets93 → 100 andSharpRuntimeTests_Net_Sockets+1; no other executable's count moved. #2094 makesKeepAliveInterval/KeepAliveTimeoutreal, with .NET's two strategies — the default (a non-positive timeout) is an unsolicited Pong that cannot fault, and only an explicit timeout selects Ping/Pong.sizeof(ClientWebSocket)grows 408 → 424 under SA-3. #2358 is a P1 defect #2094 uncovered and had to fix first:Socket::Sendcalled::send()withoutMSG_NOSIGNAL, so writing to a closed peer raised SIGPIPE and terminated the process — no exception, no return value, exit 141 — reproduced in thirty lines with no WebSocket in them. It was 17,220 immediately before, measured on the same date by ticket #2093. +3 on the 17,217 below, all inSharpRuntimeTests_Net_WebSockets(90 → 93; one gated pin inverted, four cases added). #2093 makes all fiveClientWebSocket*Asyncmembers honour theirCancellationToken, and the transport-level redesign the ticket was blocked on turned out to be nine lines: .NET registersAbort()on the token (ManagedWebSocket.cs:608,789), so cancelling any WebSocket operation aborts the whole WebSocket and no poll loop is needed.modules/net-socketsis untouched. Cancellation therefore destroys the connection — that is .NET's contract and the migration note says so. It also repairs a test-harness defect that took the whole suite down withterminate called without an active exceptionand no failing test name. It was 17,217 immediately before, measured on the same date by ticket #2096. +5 on the 17,212 below, all inSharpRuntimeTests_Net_WebSockets(85 → 90). #2096 removes a data race on four publicClientWebSocketproperties where the finding named one, and stopsDispose()/Abort()destroying the socket underneath a thread still using it. Measured, the defect was worse than recorded: against the pre-repair code all five new tests hang, becauseClose()does not wake a thread already parked inrecv()— so the old code both freed theSocketand left the worker parked.sizeof(ClientWebSocket)grows 360 → 408 under SA-3. It also closes a gap #2088 left: only two of the five*Asyncmembers had joined the liveness boundary. TSan reports the race on the reverted accessors and is clean on the repair; ASan reportsheap-use-after-freewhenConnectAsyncleaves the boundary. It was 17,212 immediately before, measured on the same date by ticket #2175. ±0 on the reading below, and deliberately so: #2175 rewrote #2173's sixteen-case pin file rather than adding to it, so the evidence is four mutations rather than a count. #2175 makesVector2/Vector3/Vector4Normalizedivide unconditionally, matching .NET, so a zero vector now yields NaN in every component with no diagnostic — as doPlane::CreateFromVerticeson a degenerate triangle andMatrix4x4::CreateLookAtwitheye == target, which used to return a silently singular matrix.Plane::Normalizekeeps exactly one guard and it is .NET's DirectXMath overflow mask, not the epsilon fast path the plan believed in; its old< 1e-10fthreshold had no .NET counterpart. Measured: both consumers referenceSystem::Numericsin zero places. It was 17,212 immediately before, measured on the same date by ticket #2268. +8 on the 17,204 below, all inSharpRuntimeTests_Core_Base(6,005 → 6,013). #2268 gives all eight integer wrappersNumberStyles::AllowExponent, whichNumberStyles::Anyhas always included. It was a deferred verification, not an approval, and the reference settles it twice over: the digit-buffer-and-scale model is transcribed, and .NET's own test suite pins the decisive rows — including65E-1as anOverflowException, which is the row the ticket was waiting for. A widening. One deliberate deviation (an all-zero magnitude with a non-positive scale) is filed as #2356 and pinned. Note the fifth mutation was not observable and was verified under UBSan instead. It was 17,204 immediately before, measured on the same date by ticket #2020 — 17,204 run, 17,204 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2020. +4 on the 17,200 below, all inSharpRuntimeTests_Text(307 → 311). #2020 closes CCF-012:CompositeFormat::Parsewas a third hand-written composite-format grammar that skipped everything between the index and the closing brace, so{0,-}parsed cleanly whileString::Formathad rejected it since #1884. All three doors now share one non-renderingSystem::detail::scanCompositeFormat. The finding's second claim is false and stays unimplemented: .NET has two composite-format grammars and the index limit is exactly where they differ, soParsegets no index limit and{1500000}keeps its answer. Adoption also widens —{0 }and friends are now accepted — and a leading space still is not. It was 17,200 immediately before, measured on the same date by ticket #2019 — 17,200 run, 17,200 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2019. +1 on the 17,199 below, inSharpRuntimeTests_Text(306 → 307). #2019 gives the defaultHtmlEncoderandJavaScriptEncoder.NET's Basic Latin allow-list, so a non-ASCII scalar is escaped rather than passed through. The target set was the half the old approval package recorded as unverifiable; the reference settles it atUnicodeRanges.BasicLatin, and the escape forms are transcribed with it.UrlEncoderneeded no change. It was 17,199 immediately before, measured on the same date by ticket #2015 — 17,199 run, 17,199 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2015. +3 on the 17,196 below, all inSharpRuntimeTests_Text(303 → 306). #2015 changed no production statement: it is a decision and its evidence. Public indices, lengths and counts inSystem::Textstay UTF-8 storage bytes, declared in the permanent-deviations list above rather than repaired, becauseSystem::Stringis a UTF-8std::stringthroughout the port and adopting .NET's unit would re-architect every index in it. The argument that makes that faithful rather than merely cheap is measured: .NET'sStringBuilder.Removehas no surrogate-pair guard either, so it splits characters exactly as this port does — a different unit would have moved the hazard, not removed it. It was 17,196 immediately before, measured on the same date by ticket #2017 — 17,196 run, 17,196 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2017. +2 on the 17,194 below, all inSharpRuntimeTests_Text(301 → 303). #2017 makes a configured fallback reach every encoding, not onlyUTF8Encoding— an exception fallback now throws in UTF-16, UTF-32, ASCII and Latin-1 where it used to be silent — and makes a truncated trailing unit reach the fallback instead of vanishing. The default output is byte-identical, which required transcribing .NET'sSetDefaultFallbacksfor the two UTF-X encodings (U+FFFD, not the base's"?"). One limitation is filed rather than smuggled:EncoderFallbacktakes achar, so the unencodable scalar is narrowed — #2355. It was 17,194 immediately before, measured on the same date by ticket #2016 — 17,194 run, 17,194 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2016. +2 on the 17,192 below:SharpRuntimeTests_Text300 → 301 andSharpRuntimeIntegrationTests915 → 916. #2016 takes the byte-order mark out ofGetBytesand puts it in a newGetPreamble(), matching .NET — a mark emitted as payload is concatenated into strings, counted in lengths and written twice by anything that also writes a preamble. The finding named one factory and there were two:BigEndianUnicode()isUnicodeEncoding(true, true)and emitted one as well, so the port was inconsistent with itself.GetPreamble()is deliberately not virtual and not onEncoding, since SA-3 excludes new virtuals on a public base class. It was 17,192 immediately before, measured on the same date by tickets #2014 and #2021 — 17,192 run, 17,192 passed, 0 failed, 0 skipped, measured on 2026-08-17 by tickets #2014 and #2021. +2 on the 17,190 below, all inSharpRuntimeTests_Text(298 → 300). #2014 makesLatin1Encodingconvert scalars rather than UTF-8 storage bytes, soGetBytes(u8"é")is the single bytee9and the whole 0..255 range round-trips; it also factored the UTF-8 decode that existed in five places intoSystem/Text/detail/Utf8Scalar.hpprather than adding a sixth, leaving the three header-inline copies as #2354. #2021 makesEncodingInfo::GetEncoding()resolve its own code page and reject one this runtime does not implement, instead of handing back UTF-8 for everything — an object that reports one code page and behaves as another is worse than one that refuses. It was 17,190 immediately before, measured on the same date by ticket #2013 — 17,190 run, 17,190 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2013. +2 on the 17,188 below, all inSharpRuntimeTests_Text(296 → 298). #2013 makes the seven factory encodings read-only, so installing a fallback onEncoding::UTF8()no longer changes what every other caller in the process decodes — transcribed fromEncoding.cs:485-497, including that the read-only test precedes the null test.sizeof(Encoding)40 → 48 under SA-3, whileUnicodeEncoding/UTF32Encodingare unchanged at 48, their own flags having moved into the base's new padding — a pin written as base + one pointer would have been wrong in both directions.Clone()is deliberately not added: it would be a new virtual on a public base class, which SA-3 excludes. It was 17,188 immediately before, measured on the same date by tickets #2029 and #2030 — 17,188 run, 17,188 passed, 0 failed, 0 skipped, measured on 2026-08-17 by tickets #2029 and #2030. +2 on the 17,186 below, all inSharpRuntimeTests_Diagnostics(225 → 227). #2029 stops~Processblocking for a redirected child's whole lifetime, and keeps the join rather than detaching the readers as its plan proposed — a detached reader appends into the destroyed object's own string, which would trade the defect for a worse one; bounding the wait with a poll slice and a stop flag gets the same promptness safely. The zombie half is deliberately not repaired: .NET reaps process-wide from a SIGCHLD wait state this port cannot have without colliding withPosixSignalRegistration. #2030 makes both captured-output getters returnstd::stringby value under the readers' own lock, since no reference into a buffer another thread appends to can be made safe. Both P1modules/diagnosticstickets are now closed, and with them the last P1 blocked on the CCF-019 family. It was 17,186 immediately before, measured on the same date by ticket #1959 — 17,186 run, 17,186 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #1959. +2 on the 17,184 below:SharpRuntimeTests_Threading464 → 465 andSharpRuntimeIntegrationTests914 → 915. #1959 closes the last async member of CCF-019, and does it by OWNERSHIP rather than by the waiting destructor #2134 established: a detached thread has no owner whose destructor could wait, and athread_localslot has no destructor a caller controls, so holding astd::shared_ptris the boundary — the direct counterpart of the GC reference .NET uses in both places. A public source break in three spellings, landed under SA-2 with all five conditions, includingtest/consumer/threading_borrowed_callback_negative.cpp(the fixture set is now 17 fixtures / 131 sites) and downstream ticket #2353, which records the measurement — zero sites in either consumer. It was 17,184 immediately before, measured on the same date by tickets #2066 and #2088 — 17,184 run, 17,184 passed, 0 failed, 0 skipped, measured on 2026-08-17 by tickets #2066 and #2088. +4 on the 17,180 below:SharpRuntimeTests_Net_Http184 → 186 andSharpRuntimeTests_Net_WebSockets83 → 85. Both apply the liveness boundary #2134 established for the CCF-019 async family — the destructor waits for an in-flight*Asyncbody — with no public signature change. #2088 also repairs the finding's wider half by copying the caller's send buffer, whileReceiveAsyncdeliberately keeps its reference because that parameter is the out-parameter the result is written into. CCF-019 is still not closed: #1959 remains, as do the owned-tree members. It was 17,180 immediately before, measured on the same date by ticket #2134 — 17,180 run, 17,180 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2134. +4 on the 17,176 below, all inSharpRuntimeTests_Net_Sockets(120 → 124). #2134 givesSocket's four*Asyncmembers a liveness boundary: the destructor and move-assignment wait for an in-flight body instead of freeing the storage it is still reading. All four of #2139's pins survive — noenable_shared_from_this,~Socketstillnoexcept, still move-assignable and non-copyable, return types unchanged — andsizeof(Socket)24 → 40 under SA-3. Two implementation facts are worth carrying:shutdown()does not unblockaccept()on Linux, so that one body polls in 50 ms slices instead, and a guard captured in the lambda is released only when the CALLER drops theTaskT, becausestd::asynckeeps the callable alive that long — the first cut deadlocked on exactly the case it was meant to fix. CCF-019 is not closed: #2066, #2088 and #1959 remain. It was 17,176 immediately before, measured on the same date by ticket #1970 — 17,176 run, 17,176 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #1970. +4 on the 17,172 below, all inSharpRuntimeTests_Threading_Tasks(218 → 222). #1970 makesTaskCanceledExceptionown the task it names instead of borrowing a pointer to it, at no public-signature cost: aTaskis a handle over astd::shared_ptr<State>, so a copy of the handle keeps the state alive and keeps observing it, which is .NET's contract rather than a snapshot.sizeof192 → 200 under SA-3, pinned as a relationship. One pre-existing test was inverted: it asserted address identity, which is the borrowing the finding reports and is the wrong analogue of a .NET reference type — the counterpart of the same object for a value-semantics handle is the same state. CCF-019 is not closed: #1959, #2066, #2088 and #2134 remain. It was 17,172 immediately before, measured on the same date by ticket #1979 — 17,172 run, 17,172 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #1979. +4 on the 17,168 below, all inSharpRuntimeTests_Runtime(164 → 168). #1979 stops a non-cancelledSIGTSTP/SIGTTIN/SIGTTOUdelivery suspending the process that merely observed it, and starts invoking a saved non-default disposition. Its design could only propose .NET's rule from a reading with no managed probe, and its own alternative 4 was defer until the reference tree is available — it is, and the derivation corrected the proposal three times: the no-op set is seven signals rather than three, the chaining belongs inside the raw handler rather than in the non-cancelled path, and the default branch restores the savedstruct sigactionrather than imposingSIG_DFL. Its tests re-exec the binary rather than forking, because this module starts its watcher lazily andfork()hands a child the already started flag without the thread. It was 17,168 immediately before, measured on the same date by ticket #2228 — 17,168 run, 17,168 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2228. +3 on the 17,165 below, all inSharpRuntimeTests_Core_Base. #2228 givesGuid::NewGuidthe platform CSPRNG, closing the lasthighfinding inmodules/core(SR-AUD-050). Its blocking question — whether a CSPRNG makes a non-throwing public function able to throw on Emscripten — dissolved on measurement: Emscripten's libc implementsgetentropy()as__wasi_random_get(), backed bycrypto.getRandomValues, and .NET reaches the same source there, so all three platforms have a real CSPRNG and nothing new throws. The function is file-local toGuid.cppbecauseCore.Basecannot depend onSecurity.Cryptography.Random. The finding was invisible to ordinary tests by its own admission; it is now caught by the one property that discriminates — a userspace PRNG has state andfork()duplicates it, so the old code made a parent and child emit the identical GUID. It was 17,165 immediately before, measured on the same date by ticket #2128 — 17,165 run, 17,165 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2128. +7 on the 17,158 below:SharpRuntimeTests_Net_Http_Headers423 → 427 andSharpRuntimeTests_Net_Http181 → 184; no other executable's count moved. #2128 is the P1 request-smuggling shape, and the reference splits it in two. The singleton half lands at the collection: a second value for any of the twenty-two headers whose .NET parser is built withsupportsMultipleValues: falseis rejected withFormatException, soContent-Length: 10,20andHost: a.example,b.exampleare no longer constructible. The TE+CL half lands on the wire, because it cannot land anywhere else —Transfer-Encodingis a request header andContent-Lengtha content header, they live in two collections that cannot see each other, and .NET's collections do not enforce RFC 9112 §6.1 either — soHttpClientHandlersuppresses its own derivedContent-Lengthwhen the caller declaredTransfer-Encodingor supplied aContent-Lengthof their own.TryAddWithoutValidationdeliberately keeps both values, matching .NET, and that is pinned. Four mutations, all caught. It was 17,158 immediately before, measured on the same date by tickets #2040 and #2352 — 17,158 run, 17,158 passed, 0 failed, 0 skipped, measured on 2026-08-17 by tickets #2040 and #2352. +4 on the 17,154 below, all inSharpRuntimeTests_Net(324 → 328); no other executable's count moved. #2040 makesCookieContainer::Addreject an explicitly suppliedDomainthat does not domain-match the request URI's host, and makesCookie's constructors mark their values explicit so the container stops overwriting them — both transcribed fromCookie.VerifyAndSetDefaults, whose rule the plan had only been able to infer. #2352 is the reason to trust the number: three watcher tests waited with a bounded spin (2000yields) rather than a deadline, and one of them failed inside a full-gate run while passing five times out of five in isolation. A gate that is intermittently green is not evidence, so the three now wait on a real deadline;SharpRuntimeTests_IOran 693/693 five consecutive times. It was 17,154 immediately before, measured on the same date by ticket #2346 — 17,154 run, 17,154 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2346 through the complete independent gate. +4 on the 17,150 below: #2346 removed one case and added five,SharpRuntimeTests_IO689 → 693, no other executable's count moved. #2346 took theNotifyFilters→ inotify mapping decision (docs/StandingApprovals.mdSA-7 — 1a, 2a, 3a, 4c, 5b), which is the one deferral inmodules/iothe reference tree could not settle and never will, becauseNotifyFiltersnames Win32ReadDirectoryChangesWnotifications and inotify's event set is not a relabelling of them.modules/ionow has no open implementation work at all. Behaviour changes, all Linux-only and all indocs/Migration-IOLifecycleAndArgumentStrictness.md§8: a filter naming onlyAttributes/CreationTime/Securitystops seeing a content write,LastAccessstarts firing for a read, andFileName/DirectoryNamestop reporting each other's entries. The default filter is byte-identical. Three mutations, all caught. The previous checkpoint's green-gate qualifications still apply and are restated there. It was 17,150 immediately before, measured on the same date by ticket #2351, and that was the first green gate in this repository's history — 17,150 run, 17,150 passed, 0 failed, 0 skipped, measured on 2026-08-17 by ticket #2351 through the complete independent gate, every executable run separately and continuing past failures. Every checkpoint below this one ends with “the gate is not green”; this one does not. Read what that does and does not mean. Two of the three failure sources simply do not occur in this container — the fivePingTestsand theSocketTestsIPv6 case need networking this container has and the previous one did not — so their disappearance is environmental, not a repair. The third was a repair: #2351 rewrote the twoTimeZoneInfoTests.BaseUtcOffsetcases that hard-coded a tzdata version rather than a zone property.Europe/DublinandAfrica/Casablancaare negative-DST zones, and since tzdata 2018a the database marks the LARGER offset standard (Dublin: IST +60 standard, GMT daylight), so on tzdata 2026b the literal expectations failed while the port was right — confirmed againstTimeZoneInfo.Unix.cs:81-93, whose rule this port already implements. Both cases now derive the expectation from the installed tzdata through an independent glibc oracle, and both gained a season-independentEXPECT_NE(baseOffset, daylightOffset); mutation M2 (report the daylight offset as standard) is caught by four cases including the rewritten Dublin one. A green gate here is not a claim that it is green everywhere — a container without networking will still failPingTests. It was 17,147 immediately before, measured on the same date by ticket #2098 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,145 run-and-passed, 2 failed, 0 skipped, +13 on a before figure of 17,134 that was measured in the same container, from the same build tree, by stashing the change and rebuilding — so "no regression" here is measured, not asserted. The +13 is exactly the thirteenTextWrapperClosedStateTestscases #2098 added (SharpRuntimeTests_IO676 → 689; no other executable's count changed). Two things about this reading differ from every reading below it, and both are environmental rather than a repository change. First, the branch: this isnext, which carries 52 portability commits the audit branch does not, including test changes, which is why its pre-change floor is 17,134 rather than the 17,131 recorded below. Second, the container: the six inherited failures described below do not occur here — networking and IPv6 work, so all fivePingTestsand theSocketTestscase pass — and two different ones do:TimeZoneInfoTests.BaseUtcOffset_Dublin_StandardIsZeroWithAPositiveDaylightandBaseUtcOffset_AllYearDaylightZoneUsesItsStandardReversion, both caused by tzdata 2026b, which expressesEurope/Dublinwith a negative DST offset (standardIST+60, daylightGMT) where the tests expect standardGMT+0. They are inherited, environment-caused, were neither disabled, weakened, skipped nor recategorised, and the gate is not green. #2098 landed Approval IO-1 (docs/StandingApprovals.mdSA-3): the four text wrappers now enforce their closed state,sizeof(StringWriter)grows 384 → 392 with the other three free, and half of SR-AUD-337 turned out not to reproduce against .NET — aleaveOpenStreamWriteris never marked disposed upstream either, so that asymmetry withStreamReaderis now reproduced and pinned rather than "repaired". It was 17,131 immediately before, measured on 2026-08-12 by ticket #2347 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,123 passed, 6 failed, 2 skipped (17,123 + 6 + 2 = 17,131), +8 on the 17,123 below — exactly the eightWatcherReconfigurationFixturecases #2347 added (SharpRuntimeTests_IO668 → 676, no other executable's count changed), so no regression anywhere. #2347 removes a real crash: all threeFileSystemWatcherreconfiguring members joined the watcher thread unconditionally, and handlers run on that thread, so a handler calling any of them self-joined (std::system_error, code 35) and — because handler invocation had notry/catch— reachedstd::terminate(re-measured live: SIGABRT, exit 134).EnableRaisingEvents = falsefrom a handler is now permitted with deferred teardown, matching .NET;PathandNotifyFilterare rejected withInvalidOperationExceptionbecause both re-arm the very watch the calling thread is dispatching from and/rvcannot settle .NET's semantics there; and an exception escaping any handler now reachesErrorinstead of ending the process. ThreadSanitizer caught a race the first cut introduced — the identity check readwatchThread_from the watcher thread while another thread re-armed it — now answered from athread_localmarker, withenabled_/selfStopPending_atomic;sizeof/alignofmeasured 216/8 before and after, so there is no layout change. TSan clean over five runs; five mutations, all caught. This also correctsdocs/SystemIONamespaceReviewPlan.md's claim thatmodules/iohad no implementation-ready autonomous work left: needs a design pass is not not implementation-ready. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,123 immediately before, measured on the same date by ticket #2350 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,115 passed, 6 failed, 2 skipped (17,115 + 6 + 2 = 17,123), +15 on the 17,108 below — exactly the fifteenXLinqNameValidationTestscases #2350 added (SharpRuntimeTests_Xml_Linq319 → 334, no other executable's count changed), so no regression anywhere. #2350 closes the name-grammar half of the two-door asymmetry #2201 deliberately left open: the Xml.Linq direct serialisers emitted element and attribute names the sibling writer door has rejected since #2076. Measured, 26 of 37 probed names disagreed between the two doors — at the element door and the attribute door — where the finding named three. The repair validates the resolved qualified nameResolveStartTagproduces, neverXName::ToString()'s Clark notation, which no door emits; it reuses the shippedXmlConvert::VerifyNamerather than growing a second grammar, and it holds the boundary #2196 and #2200 already hold — validate at the serialisation doors, not at construction, so constructing, storing and mutating an invalidXNameall stay legal. The narrowing is measurably empty: 12 of 12 parsed trees survive the writer door, so no parsed document is affected, and 0 of 607 first-party name-literal sites are rejected. The one genuine narrowing — a leading colon — is documented indocs/Migration-XmlLinqNameValidation.md, matching what #2076 recorded for the writer door. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,108 immediately before, measured on the same date by ticket #2201 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,100 passed, 6 failed, 2 skipped (17,100 + 6 + 2 = 17,108), +18 on the 17,090 below — exactly the eighteenXLinqNulRejectionTestscases #2201 added (SharpRuntimeTests_Xml_Linq301 → 319, no other executable's count changed), so no regression anywhere. #2201 is the mirror image of #2085: the writer door truncated at an embedded NUL because the value crossedc_str(), while the Xml.Linq direct serialisers, which have no such boundary, emitted it. Measured, nine doors emitted where the finding named two; all nine now reject, reusing #2085's singledetail::ContainsNuldetector and its policy. Every value without a NUL is byte-identical, and the non-Charcharacters other than NUL are still emitted — that is #2349'sCheckCharactersdecision and #2201 deliberately does not make it. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,090 immediately before, measured on the same date by ticket #2200 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,082 passed, 6 failed, 2 skipped (17,082 + 6 + 2 = 17,090), +18 on the 17,072 below — exactly the eighteenXLinqDocTypeSerializationTestscases #2200 added (SharpRuntimeTests_Xml_Linq283 → 301, no other executable's count changed), so no regression anywhere. #2200 is the Xml.Linq half of #2084:XDocumentTypehas two DOCTYPE doors andWriteToalready delegated toXmlWriter::WriteDocType, so onlySerializeTostill held a copy of the pre-#2084 concatenation. It now reuses the shareddetail::SelectExternalIdDelimiter/detail::ExternalIdLiteralTerminatesDeclarationrather than growing a second definition; measured, the two doors disagreed on eleven of eighteen probed declarations before and none after, and every already-valid declaration keeps its bytes. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,072 immediately before, measured on the same date by ticket #2085 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,064 passed, 6 failed, 2 skipped (17,064 + 6 + 2 = 17,072), +4 on the 17,068 below — exactly the fourXmlWriterValidationTestscases #2085 added (SharpRuntimeTests_Xml494 → 498, no other executable's count changed), so no regression anywhere. #2085 stops an embedded NUL silently truncating writer content: measured, six doors lost data where the finding named three, all through the samestd::string::c_str()boundary into tinyxml2'sconst char*API. Content without a NUL is byte-identical, including tab/CR/LF and multi-byte UTF-8. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,068 immediately before, measured on the same date by ticket #2084 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,060 passed, 6 failed, 2 skipped (17,060 + 6 + 2 = 17,068), +11 on the 17,057 below — exactly the elevenXmlWriterValidationTestscases #2084 added (SharpRuntimeTests_Xml483 → 494, no other executable's count changed), so no regression anywhere. #2084 repairs the two DOCTYPEExternalIDliterals at both producers (XmlWriter::WriteDocTypeand the second door the finding never named,XmlDocument::CreateDocumentType); every value not containing"keeps its output byte-for-byte, while apublicIdcontaining", asystemIdcontaining both quotes, a>or a non-XML character are now rejected instead of silently truncated. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,057 immediately before, measured on the same date by ticket #2104 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,049 passed, 6 failed, 2 skipped (17,049 + 6 + 2 = 17,057), +8 on the 17,049 below — exactly the eight pins #2104 added (SharpRuntimeTests_IO660 → 668, no other executable's count changed), so no regression anywhere. #2104 is a documentation-and-pins ticket that changed no production statement: one descriptor pin for plan §6.2's second measured positive, two for #2105's observable half and five for #2106, all add-only. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,049 immediately before, measured on the same date by tickets #2344 and #2345 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,041 passed, 6 failed, 2 skipped (17,041 + 6 + 2 = 17,049), +16 on the 17,033 below — exactly the sixteenWatcherReconfigurationFixturecases those two tickets added (SharpRuntimeTests_IO644 → 660, no other executable's count changed), so no regression anywhere. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised, so the gate is not green. It was 17,033 immediately before, measured on the same date by ticket #2099 through the complete independent gate — every executable run separately, continuing past failures. That reading is 17,025 passed, 6 failed, 2 skipped (17,025 + 6 + 2 = 17,033), +9 on the 17,024 below — exactly the nineClosedFileStreamFixturecases #2099 added (SharpRuntimeTests_IO635 → 644, no other executable's count changed), so no regression anywhere. The six failures are the same inherited environment-caused ones described below and were neither disabled, weakened, skipped nor recategorised. It was 17,024 immediately before, measured on the same date by tickets #1986 and #1985 through the complete independent gate — every executable run separately, continuing past failures, because both repository runners stop at the first failing executable and therefore cannot produce a whole-repository total. That reading is 17,016 passed, 6 failed, 2 skipped (17,016 + 6 + 2 = 17,024), +3 on #2341's 17,021 — exactly the threePosixSignalTestscases those two tickets added (SharpRuntimeTests_Runtime161 → 164, no other executable's count changed), so no regression anywhere. It was 17,021 immediately before, measured on the same date by ticket #2341, whose reading was 17,013 passed, 6 failed, 2 skipped (17,013 + 6 + 2 = 17,021), +7 on #2318's 17,014 — exactly the sevenThreadingMonitorRecursionTestscases #2341 added. Across both checkpoints the six are the same inherited environment-caused failures — fivePingTeststracked by #1962 and oneSocketTestscase that needs IPv6 this container does not provide — and are not regressions, not disabled and not recategorised. The 38th executable isSharpRuntimeTests_IO_IsolatedStorage, added by themodules/io-isolated-storagebatch (#2203–#2206), which took the gate from 16,505/37 to 16,563/38. It was 17,014 immediately before, measured by ticket #2318 on the same date. This paragraph's chain was last rebuilt at 15,071/37 by ticket #1932 on 2026-08-01; every reading between 15,071 and 17,014 is recorded checkpoint by checkpoint at the top ofNEXT.md(#2318) — including the chain's only decrease, #2284's documented −5 — and is not restated here. The remainder of this note is #1932's own record, preserved verbatim. It was 15,058 immediately before, measured by tickets #1927, #1928, approved #1929 rows 5–6, #1880 and #1875 on the same date; it was 15,024 immediately before, measured by ticket #1897 — the approved option B of Group E indocs/RemainingApprovalDecisions.md§E.1, which makesSystem::Text::Json::Nodes::JsonNode::Parsebuild its tree iteratively — on 2026-07-31. #1897 is fully compatible: no accepted input, emitted text, public signature, object layout, vtable or exception specification changed, and it deliberately does not apply a depth bound, soJsonNode::Parsestill accepts text that .NET and this module's ownJsonDocument::Parsereject beyondDefaultMaxDepth = 64(documented in theParsedoc-comment, pinned by two tests, and reopenable only as the still-unapproved option A). It was 14,998 immediately before, measured by tickets #1854/#1858/#1862/#1863/#1865/#1879/#1884 — the approved Groups A-D ofdocs/RemainingApprovalDecisions.md— on the same date. That batch is behaviour-incompatible by design in four places, all documented:Decimal::Parsereads,as a group separator (docs/Migration-DecimalCommaGroupSeparator.md), the four date/time parsers reject text they used to accept,String::Formatadopts .NET's brace and alignment grammar, andSingle/DoubleToString(value, format)emit differentE/N/Gtext. It was 14,920 immediately before, measured by tickets #1921-#1924 (the #1919 public-representation containers, which closed the #1912 Collections comparison-contract family) on the same date; it was 14,890 before that, measured by tickets #1913-#1918 and #1920 (the #1912 family) on the same date; it was 14,815 immediately before, measured by tickets #1904-#1910 (CCF-010) on the same date, and 14,745 before that, measured by tickets #1901/#1902 (CCF-009). This paragraph was last revised at 14,113/36 by ticket #1832; the readings between the two are recorded batch by batch inNEXT.md, which the intervening remediation batches kept as the live floor, and are not restated here. The remainder of this note is #1832's own record, preserved verbatim. The verified baseline is 14,113 tests across 36 component executables and one integration executable, measured by ticket #1832 on 2026-07-29 through the full repository gate (an incremental build was correct for it and for #1805/#1806/#1807/#1808/#1809/#1810/#1811/#1812/#1813/#1817/#1818/#1819/#1820/#1821/#1822/#1825/#1826/#1830/#1832: all twenty changed only.cppbodies, inline header bodies, test bodies and header doc-comments, with no layout or public-signature change — #1814 additionally declared one new public component edge,Net.Http.Json→Core.Base, taking the graph from 90 to 91 edges and requiring the generated catalogue to be regenerated). It read 14,106 after #1830, 14,098 after #1826, 14,091 after #1813, 14,077 after #1825, 14,070 after #1808, 14,060 after #1809, 14,046 after #1821, 14,041 after #1822, 14,033 after #1820, 14,025 after #1819, 14,021 after #1818, 14,014 after #1817, 14,002 after #1816, 13,994 after #1814, 13,987 after #1812, 13,979 after #1811, 13,970 after #1810, 13,958 after #1807, 13,948 after #1806, 13,937 after #1805 and 13,923 after #1789 earlier the same day — that figure came from a fresh configuration and a clean-first rebuild, which #1789's object-layout change made mandatory — 13,880 after #1788, 13,840 after #1791, 13,790 after #1802, and 13,538 before that, having fallen behind several remediation tickets that each added permanent regressions. This floor should be raised as new tests are added and lowered only with an explicit, documented reason.
- Push only to
feature/work. Never push todevelopormaster, and never create tags, without explicit per-action user approval. - SPDX header on every project source/header —
// SPDX-License-Identifier: MIT+ copyright + .NET attribution. Vendored sources retain their upstream headers; Markdown uses an HTML SPDX comment where one is present. - Property naming: always
getXxxProperty()/setXxxProperty(). Exception: indexers (C#this[key]equivalents) usegetItem()/setItem(), notgetItemProperty()/setItemProperty()— a deliberate, consistent convention for the parameterized-property case, applied across every indexer in the codebase. - Namespace syntax:
namespace System::Collections::Generic {(C++17 nested form). - Use
SharpRuntime::intcs, notintin public APIs that mirror .NETintparameters. - No LINQ in the code this project writes — use
std::rangesin ported bodies instead. This does not mean the tree contains no LINQ surface, and the distinction is worth stating here because the two look like a contradiction otherwise:modules/core/include/System/Linq.hppis a 508-lineSystem::LinqprovidingWhere,Select,FirstOrDefaultand ~17 more overstd::vector<T>. It exists as a compatibility surface for ported C#/XNA call sites, not as a licence to use those operators in new code. Measured 2026-08-19: a strict search forSystem::Linq::finds zero uses in this repository's production code, zero incnaand zero inmobile-eggbert— its only users are its own two test files. So it is a capability offered in advance of a caller; removing it would be a public-surface decision rather than a cleanup. - No merge to master or tags without explicit per-action user approval.
- No broad header refactor — naming conventions touch 449+ files and would break CNA.
- Copy doc-comments from .NET source — when porting a type, if the
.NETsource (/rv/tmp/runtime/src/libraries/) has XML doc comments and the sharp-runtime header has none, copy them as Doxygen/** */comments where the meaning translates cleanly to C++. - At most two parallel compilation jobs. See "Build-resource policy" below. This is binding on every build, rebuild, sanitizer build, probe, fixture, and test script in this repository, permanently and for all future work. The ceiling was four until 2026-07-28, when the user lowered it to three, and two from 2026-08-01; historical ticket records that state a four- or three-job measurement describe what was correct under the then-current rule and are not retro-edited.
- Push immediately after every commit. A commit is not finished until it is on the
remote. Run
git push -u origin <current-branch>as the next action after eachgit commit— do not batch commits up for a later push, and do not end a turn with the local branch ahead of its upstream. This was introduced on 2026-08-09 after 108 commits (three to nine days old, +42,818/−1,051 across 277 files) were found sitting unpushed onclaude/remediation-batch-1804-namespace-b1yjh5; the container that held them is ephemeral, so that backlog was one reclaim away from being lost. The rule does not relax rules 3 and 9: the push target is the session's designated working branch, anddevelop,masterand tags still need explicit per-action user approval. If a push fails on a network error, retry up to four times with exponential backoff (2s, 4s, 8s, 16s); if it still fails, say so explicitly rather than leaving the commit silently unpushed. - Standing approvals live in
docs/StandingApprovals.md— read it before recording any ticket asblockedon an approval. On 2026-08-17 the user granted four: SA-1 commit and push post-audit work directly tonext(rules 3 and 9 are otherwise unchanged —develop,masterand tags still need per-action approval); SA-2 a public source break may land with a migration note, a per-spelling negative consumer fixture, a #1773-shaped downstream ticket, the full gate, and a measured impact report against the localcnaandmobile-eggbertcheckouts — which may be read but never edited without a per-action instruction; SA-3 a private data member may be added to or removed from a public type without a fresh ask when no vtable/mangled-symbol/signature/noexceptchange is involved, the before/aftersizeofis pinned by a layout test, a migration note records the full-consumer-rebuild requirement, and the full gate runs — a vtable or base-class change still asks, and #1888, #1889 and #1896 stay declined; SA-4 Unicode tables are derived from .NET's own generated data in/rvat UCD 16.0, cross-checked against Perl 15.0.0 and Python 15.1.0, version pinned until an explicit bump ticket. On 2026-08-18 the user granted three more: SA-8 where this port publishes a mutable or public representation and .NET's is private, readonly or absent, match .NET and migrate the first-party sites — granted against the recommendation to split the family by hazard, and knowing thatt.Item1becomest.getItem1Property()permanently under rule 5; SA-9 a type that exists only because .NET has one, and whose content is permanently out of scope, wears .NET's public shape with bodies that throw — which authorises the newThreaddata-slot API thatLocalDataStoreSlotneeds; SA-10 a public signature change (return type,noexcept, nullability,[[deprecated]]) is a public source break and lands under SA-2's five conditions, which nine tickets had wrongly treated as covered only by SA-3. On 2026-08-20 the user also granted SA-15:DateTimereaches a timezone through an abstraction inCore.Baserather than by movingTimeZoneInfo(measured the dearer shape — not header-only, two exception types, a 270-line private POSIX header, and tzdata under everyCore.Baseconsumer), with the caveat accepted that .NET'sToLocalTime()takes no argument, so the source must come from either a hidden registration hook or an explicit overload — a deviation either way, to be recorded as one; an unrecognised culture name is decided by a syntactic BCP-47 check rather than .NET's invariant-mode rule, which accepts only""and"und"and would reject"de-DE", and that difference must be stated in the header rather than implied; and SA-3 is extended to cover vtable and base-class changes, under five conditions of which the fourth — enumerating everycatchclause whose meaning changes — exists because a reparenting is invisible to a layout pin. That unblocks #1980 G-3 and #1997 A-4; #1888, #1889 and #1896 stay declined. On 2026-08-20 the user granted SA-14, three decisions that unblock the date/time chain rooted at #1940 (five tickets): the provider reaches the parser by movingDateTimeFormatInfointoCore.Baseand adding overloads on top — measured at two files and zero changed include lines, against 34 files across eight modules for the new-component shape the ticket's wording implied; the culture-concurrency defect is repaired separately and first, with .NET's two-property model (thread_localcurrent culture plus a process-wideDefaultThreadCurrentCulturefallback), because a plainthread_localwould silently remove the process-wide setting this port has today; and an unrecognised culture name throwsCultureNotFoundExceptionfrom both doors. Two premises on #1940's own record were re-measured and found wrong — the exception type does exist here and is already thrown from the LCID path, and an unknown name does not "resolve to invariant" but produces an object that claims to be the requested culture and behaves as invariant. On 2026-08-20 the user also granted SA-16, three decisions that close the date/time zone chain:DateTime's style overloads take the zone as a parameter, which is #1941 phase 2's own precedent rather than a second answer to one question -- the alternatives were declined on the record, since accepting only the stamping styles would make a legal .NET style illegal here and a registration hook is the hidden global state #1940 already refused;DateTimeOffset::ParseExactgets an offset token plus the zone for the no-offset default, on the measurement that an offset is not a time zone and a format carrying one needs no zone database at all; andRoundtripKindis made real in BOTH directions, the parse side setting the kind from a zone token andXmlConvert::ToStringemitting the marker as .NET'sXsdDateTimedoes -- knowingly changing that member's output, which is why it was asked, and which makes #1945's declaration pin fail and be inverted rather than deleted. That document also records the environment facts those approvals rest on:/rv/tmp/runtimeis present in this container (a .NET 11 preview snapshot, so a behaviour read from it must be reported as .NET 11's, not as timeless parity), and so are both downstream consumers. A ticket whose only recorded gate is "/rvabsent" or "downstream consumers may not be inspected" is therefore not blocked here; re-verify before treating it as such.
This section is permanent and binding for all work in this repository, by any contributor and by any future Claude Code session, on every ticket — not only the ticket that introduced it. It has two halves: a CPU ceiling and the pre-existing SSD-saving rules. Both must be obeyed together.
-
Every compilation, link, build, rebuild, sanitizer build, consumer fixture, compile probe, dependency build, CMake configure step that compiles, and test script that performs compilation internally may use at most two parallel jobs / two CPU cores.
-
Use commands equivalent to:
cmake --build <dir> --parallel 2 ninja -C <dir> -j2 make -C <dir> -j2 ctest --test-dir <dir> -j2
or any lower value.
-
Never use unrestricted or automatically detected parallelism. All of the following are forbidden:
- a bare
ninjainvocation, which defaults to every CPU plus two; -jwith no number, which is unbounded;--parallelwith no explicit maximum, which uses all detected cores;$(nproc),nproc,sysctl -n hw.ncpu,getconf _NPROCESSORS_ONLN, or any equivalent core-count substitution;std::thread::hardware_concurrency()— or any runtime core count — used to choose a build-job count;CMAKE_BUILD_PARALLEL_LEVEL,MAKEFLAGS,NINJA_STATUS-adjacent environment variables, or CI defaults left to expand to all cores;- any script that defaults to "all available cores" when no job count is supplied.
- a bare
-
When a repository script compiles internally, pass whatever argument or environment variable constrains it to two jobs (for example
CMAKE_BUILD_PARALLEL_LEVEL=2,MAKEFLAGS=-j2, or the script's own job-count parameter), and record in the ticket that the constraint was applied. -
If a build script cannot currently be limited to two jobs, fix the script first, or use a bounded alternative (a direct
cmake --build … --parallel 2on the same targets). Do not run the unbounded script "just this once".scripts/job_count_policy.pyis the single resolver used byscripts/check_selective_components.sh,scripts/local_ci_check.sh, andscripts/check_negative_consumer_fixtures.py. Precedence is explicit--jobs, thenSHARP_RUNTIME_BUILD_JOBS, then the safe default 2; only 1 or 2 is accepted, and wrappers export the resolved value to nested helpers. Local CI still passes the resolved value explicitly to the fixture checker. -
The two-job limit applies even when the machine has more CPU cores. Core count is not a licence to raise it.
-
Fewer than two jobs is always allowed and is preferred whenever a target is memory-heavy (sanitizer or template-heavy translation units): drop to
-j1rather than risking swap or an OOM kill. -
Exceeding two jobs requires new explicit user approval, per action. A previous approval never carries over to another command, another ticket, or another session.
-
Every final ticket report must list:
- every build directory used;
- the maximum parallel job count actually used;
- any script that required special handling (an argument, an environment variable, or a bounded substitute) to enforce the limit.
Why this matters: repeated from-scratch builds wrote 3.5 TB to this SSD in five days (measured 2026-07-28), on top of the earlier ~270 GB / two-day scratchpad measurement. That is real, irreversible flash wear, not just wasted time. Every avoided full rebuild is avoided write endurance spent. Reuse an existing build directory whenever it is usable — a full reconfigure/rebuild is the exception that must be justified, never the default.
-
There is a FIXED, CLOSED set of build directory names. Never invent another one, and never suffix one with a ticket number, a date, a branch, or a topic. The complete list:
Directory Purpose build/the default build and the repository gate build-modular/the modular/selective component build build-asan/,build-ubsan/,build-tsan/sanitizer trees build-probe/every ticket's throwaway probes, ABI experiments, shims and sweeps build-consumer/every ticket's standalone consumer fixture build-tmp/repository-local TMPDIRformktemp-based scriptscmake-build-debug/the IDE tree Directories like
build-probe-1794/,build-consumer-1785/,build-asan-sortedset/are the mistake this rule exists to stop: measured on 2026-07-28, twenty-one such one-shot directories held 441 MB and guaranteed that nothing in them was ever reused. A ticket separates its own work by file name prefix inside the shared directory (build-probe/1797_probe1_escapes.cpp), never by a new directory. -
Delete a ticket's probe artefacts once its evidence is transcribed into the design record. The design document is the durable evidence; the binaries are not. Sanitizer probe binaries reach hundreds of megabytes each.
-
Prefer incremental builds. Do not clean, delete, or reconfigure a build tree unless it is genuinely broken or the configuration genuinely changed; document any such build and why.
-
Retain
ccachewherever it is already configured, and do not retrofit it where doing so would force an unnecessary full recompilation. -
Never create a build tree under
/tmp,/var/tmp, or/dev/shm, including the per-session scratchpad. Redirectmktemp-based scripts through a repository-localTMPDIR(this repository usesbuild-tmp/). -
Remove large disposable binaries once their results are recorded, and never delete a build directory another session may still be using.
The current full build/test baseline is Linux/GCC; the complete production-only All graph is
also warning-clean under Clang 19.1.7 and is enforced locally and in CI. Post-modular MinGW-w64 GCC
14-win32/CMake 3.31.6 and Emscripten 5.0.7/CMake 3.31.6 library builds both
compile the All graph and a selective Text.Json graph without
GoogleTest/runtime execution. Real downstream Apple Clang/Xcode 15.4 builds
drove the portability fixes from 1d22a7b2 through b797928f. The
repository's tracked CI is Ubuntu-only, so do not describe Windows,
Emscripten, or macOS as having the same current test coverage as Linux.
Unsupported runtime operations must still compile. They should throw
PlatformNotSupportedException clearly rather than fail the build or silently
degrade.
Current platform-limited areas include:
| Subsystem | Implemented runtime platforms | Explicit limitation |
|---|---|---|
System::Net::Sockets |
Windows and POSIX | Socket operations throw on Emscripten. |
System::IO::RandomAccess |
Windows and POSIX | Operations throw on Emscripten. |
System::AppDomain base directory |
Windows, macOS, Linux/POSIX | Emscripten uses the virtual-FS-relative ./ fallback. |
System::TimeZoneInfo |
Windows and POSIX | Emscripten provides UTC/local fallback and rejects system-zone lookup. |
System::Diagnostics::Process |
POSIX | Operations throw on Windows/Emscripten. |
PosixSignal/PosixSignalRegistration |
POSIX | Registration throws on Windows/Emscripten. |
NetworkInterface enumeration |
Linux | Enumeration/query operations throw elsewhere. |
FileSystemWatcher |
Linux/inotify | Enabling events throws elsewhere. |
System::Decimal, System::Int128, and System::UInt128 require a compiler-provided 16-byte
__int128/unsigned __int128 type. This is a compiler-capability dependency, not an OS-name
test: x86_64 GCC/Clang and x86_64 MinGW GCC provide it, while MSVC and i686 MinGW GCC do not.
The public SHARP_RUNTIME_HAS_NATIVE_INT128 macro is always 0 or 1; CMake sets it from an
actual compile probe and SharpRuntimeHelper.hpp provides the same __SIZEOF_INT128__-based
fallback for non-CMake consumers.
When the macro is 0, the three direct type headers reject inclusion with a clear diagnostic and
the library omits only Decimal.cpp. Otherwise-portable types remain available, with only their
native-128-dependent members absent: Int64::BigMul(long,long), the 64-bit Math::BigMul
overload, 128-bit BitConverter/BinaryPrimitives overloads, BinaryReader::ReadDecimal,
IConvertible/DBNull::ToDecimal, and the Decimal XmlConvert overloads. This is the supported
i686 MinGW compile/link boundary used by CNA's Glide backend; it does not claim Decimal or
Int128/UInt128 support on that compiler.
The lack of native 128-bit integers is a known, accepted, permanent limitation — never hide it with a hand-rolled representation or reduced semantics. The 2026-07-11 decision remains that the risk and complexity of a from-scratch implementation outweigh its benefit.
| Type | Requires | Availability |
|---|---|---|
System::Decimal |
16-byte unsigned __int128 |
SHARP_RUNTIME_HAS_NATIVE_INT128 == 1 |
System::Int128 |
16-byte __int128 |
SHARP_RUNTIME_HAS_NATIVE_INT128 == 1 |
System::UInt128 |
16-byte unsigned __int128 |
SHARP_RUNTIME_HAS_NATIVE_INT128 == 1 |
- POSIX includes (
<unistd.h>,<sys/socket.h>, etc.) must not appear in public.hppheaders. - Platform-specific code belongs in
.cppfiles guarded by#ifdef _WIN32/#elif defined(__EMSCRIPTEN__)/#else(POSIX). - On unsupported platforms, throw
System::PlatformNotSupportedExceptionwith a clear message — never silently fail. - Emscripten builds must compile without errors even when the feature is unavailable at runtime.
- ✅ DONE — implemented, tested, compiles clean on the verified native baseline, no known bugs.
⚠️ PARTIAL — compiles and mostly works but has known, documented API or behavior gaps.⚠️ PLATFORM-LIMITED — compiles across the intended toolchains but some operations are available only on named platforms and throw explicitly elsewhere.⚠️ STUB — API surface exists, bodies are no-ops or throwNotImplementedException.
sharp-runtime and .NET will naturally differ — C++ has no GC, no IL, no runtime reflection, and no delegate infrastructure. The goal is maximum practical parity: the public API, method semantics, default values, error messages, and algorithmic behaviour should match .NET as closely as C++ allows.
Known permanent deviations (not bugs, not TODO):
- Reflection (
System::Type,System::Activator,Enum.GetNames/GetValues, etc.) — completely out of scope. Stubs are the correct end state. - GC (
System::GC) — all methods are no-ops. Memory is managed by RAII /std::shared_ptr. - Delegates — three tiers, not one blanket rule (verified 2026-07-11 while auditing ticket 72,
since the previous one-line version of this bullet was itself inaccurate for two of the three):
the majority of delegate-shaped types (
Action,Func,EventHandler, and most*EventHandler/*Callbackaliases across the codebase) are bareusing X = std::function<...>;aliases — single-target only, no multicast, noBeginInvoke/EndInvoke(async delegate invocation is out of scope entirely, matching .NET's own removal of the pattern). ButSystem::Delegate(modules/core/include/System/Delegate.hpp) is a real multicast delegate base class with workingCombine/Remove/RemoveAll/GetInvocationList, andSystem::MulticastAction<Args...>(modules/core/include/System/MulticastAction.hpp) is a purpose-built multicast event-field type with+=/=and reentrancy-safe snapshot invocation — both genuinely support multicast where a ported type needs it.Delegate::DynamicInvokealways throwsNotImplementedException(no late-boundobject[]invocation equivalent in C++) in all three tiers. - Serialization (
[Serializable],SerializationInfo) — ignored; not needed for game code. - P/Invoke / interop — out of scope.
- The unit of every public index, length and count is a UTF-8 storage byte, where .NET's is a
UTF-16 code unit — decided by ticket #2015 (SR-AUD-290, SR-AUD-296) on 2026-08-17 and
declared, not repaired.
System::Stringis a UTF-8std::stringthroughout this runtime, soEncoding::GetCharCountof U+1F600 is4where .NET reports2, andStringBuilder'sLength/Insert/Remove/CopyToindex bytes. Adopting .NET's unit is not a change toSystem::Text; it is a re-architecture of every index in the port. What makes this the faithful adaptation rather than merely the cheap one is that a byte index into UTF-8 is the exact analogue of a code-unit index into UTF-16, including the ability to split a character: .NET'sStringBuilder.Removevalidates only the numeric range (StringBuilder.cs:1024-1042), with no surrogate-pair guard, so it can leave a lone surrogate and an ill-formed string exactly as this port can leave an ill-formed UTF-8 sequence. Adopting .NET's unit would have moved that hazard to a different character, not removed it. Pinned byTextUnitContractTests.Decl2015_*, including that the unit is consistent across the component — a mixture would be far worse than either unit consistently applied. - Unicode normalization —
StringNormalizationExtensions::IsNormalizedreturnstrueandNormalizereturns its argument unchanged for every input. Decided by ticket #2338 on 2026-08-19 and declared, not repaired — and the point is that this is not a divergence: it is exactly .NET's behaviour in invariant globalization mode, which says so in its own comment ("In Invariant mode we assume all characters are normalized because we don't support any linguistic operations on strings",Normalization.cs:11-40). .NET has no normalization tables of its own — it delegates to ICU on Unix and NLS on Windows, andCharUnicodeInfoData.cs, the source of record SA-4 names, carries zero decomposition, combining-class, composition-exclusion or quick-check data. Two alternatives were offered and declined: own UCD tables plus a UAX #15 implementation (size L-to-XL, and a second Unicode version to keep in step with SA-4's 16.0), and an ICU dependency, which is the shape this list already declines for cryptography — "a large new external dependency" — so taking it would reverse a standing decision rather than make a new one. Measured: zero call sites incnaand inmobile-eggbert; the only in-repository uses are the type's own tests. What a caller must read into it:truemeans this runtime performs no linguistic normalization, not that the string is in the requested form. The form is validated on every platform, becauseCheckNormalizationFormruns before the invariant shortcut (#2386). Pinned byStringNormalizationTests.Decl2338_*. - tzdata rule structures —
TimeZoneInfo::GetAdjustmentRules()returns an empty array andHasSameRules()therefore cannot distinguish two zones that share a base offset and a DST flag (America/New_YorkandAmerica/Havanareport as same-rule zones where .NET reportsfalse). Decided by ticket #2185 on 2026-08-19 and declared, not repaired, on two measurements. It is not closable by sampling libc at any granularity — two zones can agree on every sampled instant and still differ in rule, so no finer sampling turns offsets into rules; closing it needs tzdata's own structures (the TZif POSIX-TZ footer, or the full transition table with its per-era type records), i.e. a TZif reader, which is out of scope for this port. And the failure is one-directional: this method can only ever be too permissive, never too strict, so a caller using it as a necessary condition is correct and one using it as a sufficient condition is not. The layout cost an earlier design measured (sizeof(TimeZoneInfo)160 → 184) is real but is not what blocks it and is not paid. Pinned byTimeZoneInfoTests.Decl2185_*. - Symmetric/asymmetric cryptography, X.509 certificates, TLS (
System.Security.Cryptography'sAes*/RSA*/EC*/ChaCha20Poly1305/CryptoStream,System.Security.Cryptography.X509Certificates,System.Net.Security'sSslStreamand friends) — out of scope by explicit decision (2026-07-07): implementing this correctly needs either a large new external dependency (OpenSSL/mbedTLS) or a hand-rolled, security-critical implementation, neither of which is worth it for game code. Hash algorithms (MD5/SHA*/HMAC/PBKDF2— no key material, no confidentiality guarantees to get wrong) are already ported and remain in scope; they are not affected by this deviation.
When a method cannot be meaningfully implemented (e.g. it requires reflection), it should throw System::NotImplementedException with a comment explaining why — never silently return a wrong value.
A type may be marked ported only when all of the following hold:
- A header under the owning module's
include/System/.../*.hppexists with the full public API: all public methods, constructors, properties, and operators that appear in the .NETref/surface file. - Properties follow
getXxxProperty()/setXxxProperty()naming. - Complex types have a
.cppbody file; simple types may be header-only. - No method body is a bare
throw NotImplementedException()stub — those are STUB, not ported.
SharpRuntime::intcs(notint) for public API parameters that mirror .NETint.- Namespace opened with C++17 nested syntax:
namespace System::Collections::Generic {. - No LINQ — use
std::rangesinstead. - POSIX-only internals are in
.cppfiles behind#ifdef, not in public.hppheaders.
- Doxygen
/** */block comments on every public type and method. - Comments copied/adapted from .NET XML doc-comments in
/rv/tmp/runtime/src/libraries/where available.
Every .hpp and .cpp file starts with:
// SPDX-License-Identifier: MIT
// Copyright (c) Robert Vokac and contributors
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)- Compare the C++ implementation in sharp-runtime against the reference C# source in
/rv/tmp/runtime/src/libraries/. - All non-trivial method bodies must match the .NET logic (algorithm, edge-case handling, error conditions).
- Verify that default messages, constants, and HResult/error codes match the .NET source where applicable.
- Discrepancies must be either fixed or explicitly documented as intentional deviations.
cmake --build build --parallel 2— zero errors, zero warnings.
scripts/run_component_tests.sh build— all component and integration tests pass exactly once (no failures, no crashes).- At least basic GoogleTest coverage exists for the ported type's key methods.
- Complex types:
.hppdeclaration +.cppbody. Move bodies to.cppwhen a header grows unwieldy. - Simple types: header-only is fine.
- CMake: component-specific
CONFIGURE_DEPENDSglobs discovermodules/*/{src,tests}/*.cpp;scripts/validate_module_boundaries.pyvalidates every implementation/header owner and public/private/test dependency. Every module declares its include root, sources, tests, dependencies, and platform setup inmodules/<module>/CMakeLists.txt. - Component boundaries: internal code depends on narrow physical targets
(
Core.Base,Collections.Core, etc.), never theCore,Collections, orAllcompatibility umbrellas. Public-header edges arePUBLIC_DEPENDENCIES, implementation-only edges arePRIVATE_DEPENDENCIES, and test-only edges areTEST_DEPENDENCIES.BlockingCollection<T>belongs toCollections.Blocking; do not add itsThreadingrequirements back toCollections.Coreor weaken the Text.Json isolation fixture. - Vendored libs: GoogleTest, nlohmann/json, tinyxml2, miniz, all under
vendor/. Never commit binaries. Files undervendor/are third-party source unmodified from upstream and are exempt from this project's SPDX-header, doc-comment, andgetXxxProperty()/namespace-syntax naming rules — those rules apply only to moduleinclude/,src/, andtests/trees. - Templates: deferred
inlinedefinitions after forward declarations to resolve circular includes. - Collection mutation counters: a collection with a fail-fast enumerator must hold its
counter as
System::Collections::detail::MutationCounterand its enumerator must snapshotdetail::MutationVersion(System/Collections/detail/MutationCounter.hpp). Never a bareintcs—++on a signed counter is undefined behaviour atINTCS_MAX, and the implicitly declared assignment operator would transplant the source's counter into the destination, leaving an enumerator apparently valid over storage the assignment destroyed. Both defects existed in fourteen collections and are recorded with reproductions indocs/CollectionVersionCounterSweep.md.detail::NarrowMutationCounterhas no user left and must not gain one. It was the 32-bit counter for the two types whose measured layout had no room for eight bytes; ticket #1788 movedLinkedList<T>off it (growingsizeof(LinkedList<T>)40 → 48) and ticket #1789 movedBitArrayoff it (growingsizeof(BitArray::Enumerator)32 → 40), each under its own explicit user approval, so no collection retains a 2^32 enumerator-snapshot ABA horizon. The alias survives only as history and as the second instantiation the counter tests pin.SortedSet<T>keeps its ownulongcscounter inside the sharedStateits live views co-own (ticket #1786) — do not migrate it. When a collection's counter is widened, its enumerator's snapshot must be widened in the same change: a narrow snapshot compared against a wide counter is a silent truncation that leaves the alias in place while the code claims otherwise. - Test-only access seams: a class template that a production header declares inside
namespace SharpRuntime::Testingand never defines (CollectionVersionAccess,SortedSetVersionAccess) may be defined in exactly one file, and every suite that needs it must include that file. For the collection mutation counters that file ismodules/collections/tests/support/CollectionVersionSeam.hpp; add a new collection there, once, through itsSHARP_RUNTIME_COLLECTION_VERSION_SEAMmacro. Never writetemplate<> struct CollectionVersionAccess<…> { … }in a test translation unit. Five suites did, in two divergent families, and two token-different definitions of one class in one program is a one-definition-rule violation that is ill-formed with no diagnostic required: measured on 2026-07-29, swapping two object files on the link line changed the answer a correctly written suite got, andld,-flto -Wodr, ASan withdetect_odr_violation=2and UBSan all said nothing (docs/CollectionVersionTestSeamDesign.md, ticket #1800).scripts/check_version_seam_odr.pyenforces this and runs inscripts/local_ci_check.sh; never define a seam inmodules/*/includeormodules/*/src, because that would make it reachable from a consumer and break the consumer-side fixture that pins it —test/consumer/collections_mutation_version_negative.cppforCollectionVersionAccess(2 sites, #1787/#1801) andtest/consumer/collections_sorted_set_version_negative.cppforSortedSetVersionAccess(15 sites, #1803). Every seam needs both checks — they catch different mutations. Ticket #1804 (2026-07-30) closed one earlier gap in the seam checker: giving a seam's primary template a body in a public header used to make it stop being discovered as a seam, socheck_version_seam_odr.pyexited 0 while one of two seams silently vanished; the checker now surfaces a defined primary and rejects it as a seam defined in a production tree (docs/CollectionVersionTestSeamDesign.md§15). The consumer fixture is still required for a mutation the checker cannot see — making a collection's private counter public — which is caught only by compilation (docs/NegativeConsumerFixtureValidation.md§18.4 row four). A seam added by a future ticket must therefore gain atest/consumer/*_negative.cppsite too, not only a single definition site. Note also what neither check can express: a consumer that reopensnamespace SharpRuntime::Testingand writes its own explicit specialisation does get the access the friend declaration grants, for both seams; that is well-formed ISO C++, is unsupported, and is recorded in §18.5 rather than assumed away. - Negative consumer fixtures: a
test/consumer/*_negative.cppproves that a spelling a ticket outlawed is rejected by the compiler. It must carry a// NEGATIVE-FIXTURE: component=<Component>directive, an#ifndef SHARP_RUNTIME_NEGATIVE_SITE / #define … 0 / #endifprelude, and one#if SHARP_RUNTIME_NEGATIVE_SITE == Nguard per negative site, each holding exactly one// NEGATIVE(<kebab-id>): <expected diagnostic fragment>marker (further alternatives on following// | <fragment>lines). Site numbers must be1..N; the#elsebranch is where the migrated spelling goes. With no site selected the file must compile with zero diagnostics — that clean baseline is what lets a per-site verdict be attributed to its own source, so add(void)x;wherever disabling a site orphans a local.scripts/check_negative_consumer_fixtures.pycompiles the baseline plus each site separately (-fsyntax-only,-Wall -Wextra -Wpedantic -Werror, at most two jobs, include directories derived from the CMake component metadata) and runs inscripts/local_ci_check.sh. Never assert only that a whole fixture fails to compile: one broken line hides every other line, and a whole-file check reported a false pass while one of eleven claims had silently become legal again (docs/NegativeConsumerFixtureValidation.md, ticket #1801).
plan.sqlite3 (table task) tracks indexed .NET types from dotnet/runtime.
Rows started with an empty status; the current maintainer snapshot is fully
classified. Full workflow detail lives in prompt.md — this is the summary:
- For each type where status is
''ortodo(System-namespace types first), look up what it does in/rv/tmp/runtime/src/libraries/and classify it without asking the user:- Port it → check if the file exists in sharp-runtime, review against the full checklist, port or fix, then set
status = 'ported'and commit. - Out of scope / irrelevant → set
status = 'ignore', and setoutofscope = 1for permanent-deviation categories (reflection, GC internals, P/Invoke, serialization infra, etc.) oroutofscope = 0otherwise. - Genuinely ambiguous → set
status = 'tobedecided'rather than guessing; the user reviews these by hand later.
- Port it → check if the file exists in sharp-runtime, review against the full checklist, port or fix, then set
- Keep processing items back-to-back — do not stop between items to ask for confirmation.
Valid statuses written by the current workflow are '' (unset), todo,
ported, ignore, and tobedecided. The database also contains legacy
ignored rows; treat them as classified and do not rename them mechanically.
in_progress does not exist — porting happens directly with no
intermediate state.
State lives in plan.sqlite3 + git history, not conversation memory, so this process resumes cleanly after any context reset — just re-open prompt.md and continue from Step 1.
# Build
cmake --build build --parallel 2
# Run all tests
scripts/run_component_tests.sh build
# Errors/warnings only
cmake --build build --parallel 2 2>&1 | grep -E "error:|warning:" | grep -v "^#"
# Run a specific suite
./build/SharpRuntimeTests_Net_Sockets --gtest_filter="TcpClient*"